wzh 3 лет назад
Родитель
Сommit
f640c3a0b2
61 измененных файлов с 12490 добавлено и 2 удалено
  1. 2 0
      .env.dev
  2. 2 0
      .env.release
  3. 2 0
      .gitignore
  4. 6 2
      README.md
  5. 35 0
      index.html
  6. 29 0
      package.json
  7. BIN
      public/汇很多-员工PC.ico
  8. 240 0
      src/App.vue
  9. 19 0
      src/apis/cloudLogin.js
  10. 24 0
      src/apis/config.js
  11. 331 0
      src/apis/fetch.js
  12. BIN
      src/assets/blue-circle.png
  13. BIN
      src/assets/icon-player.png
  14. BIN
      src/assets/login-back.png
  15. BIN
      src/assets/login-modal.png
  16. BIN
      src/assets/logo.png
  17. BIN
      src/assets/ship-red-icon.png
  18. BIN
      src/assets/three.png
  19. BIN
      src/assets/user.png
  20. BIN
      src/assets/white-logo.png
  21. 118 0
      src/components/Aside.vue
  22. 224 0
      src/components/Certs.vue
  23. 37 0
      src/components/Footer.vue
  24. 279 0
      src/components/Header.vue
  25. 199 0
      src/components/PicTimeline.vue
  26. 79 0
      src/components/RemoteSearch.vue
  27. 1 0
      src/components/Table.vue
  28. 135 0
      src/components/Uploader.vue
  29. BIN
      src/images/顺发999.png
  30. 52 0
      src/main.js
  31. 177 0
      src/router/index.js
  32. 40 0
      src/store/index.js
  33. 195 0
      src/styles/index.css
  34. 33 0
      src/utils/downloadBlobFile.js
  35. 13 0
      src/utils/utils.js
  36. 341 0
      src/views/agencyManage/agencyCompanyDetail.vue
  37. 256 0
      src/views/agencyManage/agencyCompanyList.vue
  38. 565 0
      src/views/cargoOwnerManage/cargoOwnerCompanyDetail.vue
  39. 256 0
      src/views/cargoOwnerManage/cargoOwnerCompanyList.vue
  40. 9 0
      src/views/cargoOwnerManageOld/cargoOwnerAdd.vue
  41. 298 0
      src/views/cargoOwnerManageOld/cargoOwnerDetail.vue
  42. 289 0
      src/views/cargoOwnerManageOld/cargoOwnerList.vue
  43. 160 0
      src/views/index/AgencyList.vue
  44. 234 0
      src/views/index/AgencySubAccountList.vue
  45. 193 0
      src/views/index/Blockchain.vue
  46. 17 0
      src/views/index/Index.vue
  47. 232 0
      src/views/index/Login.vue
  48. 343 0
      src/views/index/Versions.vue
  49. 307 0
      src/views/index/mediaCheck.vue
  50. 256 0
      src/views/portsManage/portsList.vue
  51. 256 0
      src/views/portsManage/sailingSchedule.vue
  52. 821 0
      src/views/shipInfo/shipDetail.vue
  53. 184 0
      src/views/shipInfo/shipList.vue
  54. 452 0
      src/views/shipOwnerManage/shipOwnerDetail.vue
  55. 271 0
      src/views/shipOwnerManage/shipOwnerList.vue
  56. 193 0
      src/views/toolManage/blockChain.vue
  57. 343 0
      src/views/toolManage/versions.vue
  58. 114 0
      src/views/voyage/voyageAdd.vue
  59. 2849 0
      src/views/voyage/voyageDetail.vue
  60. 937 0
      src/views/voyage/voyageList.vue
  61. 42 0
      vite.config.js

+ 2 - 0
.env.dev

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

+ 2 - 0
.env.release

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

+ 2 - 0
.gitignore

@@ -28,3 +28,5 @@ build/Release
 # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
 node_modules
 
+dist
+

+ 6 - 2
README.md

@@ -1,3 +1,7 @@
-# dataCenter
+# Vue 3 + Vite
 
-江苏汇很多数据中心
+This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
+
+## Recommended IDE Setup
+
+- [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar)

+ 35 - 0
index.html

@@ -0,0 +1,35 @@
+<!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>
+    <script src="https://imgcache.qq.com/qcloud/cloudbase-js-sdk/1.6.0/cloudbase.full.js"></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>

+ 29 - 0
package.json

@@ -0,0 +1,29 @@
+{
+  "name": "datacenter",
+  "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",
+    "unplugin-auto-import": "^0.5.11",
+    "unplugin-vue-components": "^0.17.18",
+    "vite": "^2.6.4",
+    "sass": "^1.47.0"
+  }
+}

BIN
public/汇很多-员工PC.ico


+ 240 - 0
src/App.vue

@@ -0,0 +1,240 @@
+<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% - 120px);
+  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;
+  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;
+}
+
+.mr30 {
+  margin-right: 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;
+}
+
+.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 };

+ 331 - 0
src/apis/fetch.js

@@ -0,0 +1,331 @@
+import { $http } from "./config";
+export default {
+  // 员工登录
+  staffLogin(data) {
+    return $http("/staff/login", data);
+  },
+
+  // 获取用户列表 货主/船东
+  getUserList(data) {
+    return $http("user/backstage/list", data);
+  },
+
+  // 添加用户
+  addUser(data) {
+    return $http("user/backstage/add", data);
+  },
+
+  // 添加船东
+  addShipOwner(data) {
+    return $http("user/backstage/saveShipOwner", data);
+  },
+
+  // 获取用户详情
+  getUserDetail(data) {
+    return $http("user/backstage/details", data);
+  },
+
+  // 更新用户详情
+  updateUserDetail(data) {
+    return $http("/user/backstage/update", data);
+  },
+
+  // 获取航次列表
+  getVoyageList(data) {
+    return $http("voyage/list", data);
+  },
+
+  // 获取航次详情
+  getVoyageDetail(data) {
+    return $http("/voyage/detail", data);
+  },
+
+  // 更新航次
+  updateVoyage(data) {
+    return $http("/voyage/backstage/update", data);
+  },
+
+  // 完成航次
+  finishVoyage(data) {
+    return $http("/voyage/backstage/finish", data);
+  },
+
+  // 根据船名/MMSI/船东手机号获取船舶用户信息(员工端添加航次选择船)
+  getUserInfoAndShipInfo(data) {
+    return $http("ship/backstage/userShipInfo", data);
+  },
+
+  // 添加航次
+  addVoyage(data) {
+    return $http("voyage/backstage/add", data);
+  },
+
+  // 获取媒体列表
+  getMediaList(data) {
+    return $http("/media/backstage/list", data);
+  },
+
+  // 审核媒体文件
+  auditMedia(data) {
+    return $http("/media/backstage/audit", data);
+  },
+
+  // 标记媒体文件
+  markMedia(data) {
+    return $http("/media/backstage/markMedia", data);
+  },
+
+  // 模糊搜索用户
+  searchUser(data) {
+    return $http("/user/backstage/search", data);
+  },
+
+  // 获取船舶列表
+  getShipList(data) {
+    return $http("/ship/backstage/list", data);
+  },
+
+  // 更新船舶信息
+  updateShip(data) {
+    return $http("/ship/backstage/update", data);
+  },
+
+  // 更新船东信息
+  updateShipOwner(data) {
+    return $http("/user/backstage/update", data);
+  },
+
+  // 获取船舶详情
+  getShipDetail(data) {
+    return $http("/ship/backstage/detail", data);
+  },
+
+  // 船舶查询
+  searchShip(data) {
+    return $http("ship/backstage/search", data);
+  },
+
+  // 根据shipId获取船东列表
+  getShipOwnerListByShipId(data) {
+    return $http("/user/backstage/shopOwnerlist", data);
+  },
+
+  // 添加卸货记录
+  addDischarge(data) {
+    return $http("/voyage/addDischarge", data);
+  },
+
+  // 删除卸货记录
+  deleteDischarge(data) {
+    return $http("/voyage/deleteDischarge", data);
+  },
+
+  // 导出excel
+  exportExcel(data) {
+    return $http("/voyage/exportExcel", data);
+  },
+
+  // 获取卸货列表
+  getDischargeList(data) {
+    return $http("/voyage/getDischargeList", data);
+  },
+
+  // 修改卸货记录
+  updateDischarge(data) {
+    return $http("/voyage/updateDischarge", data);
+  },
+
+  // 获取未拍照航次
+  getUnphotographNotice() {
+    return $http("/voyage/backstage/notice");
+  },
+
+  // 计算预计到港时间
+  calExpectedArrivalTime(data) {
+    return $http("/voyage/calExpectedArrivalTime", data);
+  },
+
+  // 删除运单
+  deleteWaybill(data) {
+    return $http("/voyage/deleteWaybill", data);
+  },
+
+  // 上传运单
+  updateVoyageWaybill(data) {
+    return $http("/voyage/updateVoyageWaybill", data);
+  },
+
+  // 获取港口列表
+  getCol(data) {
+    return $http("/port/backstage/getCol", data);
+  },
+
+  // 取消航次
+  cancelVoyage(data) {
+    return $http("/voyage/backstage/cancel", data);
+  },
+
+  // 添加汽车装货记录
+  addTruckLoadRecord(data) {
+    return $http("/voyage/addCarLoadRecord", data);
+  },
+
+  // 获取汽车装货记录
+  getTruckLoadRecord(data) {
+    return $http("/voyage/getCarLoadRecordList", data);
+  },
+
+  // 删除汽车装货记录
+  deleteTruckLoadRecord(data) {
+    return $http("/voyage/deleteCarLoadRecord", data);
+  },
+
+  // 更新汽车装货记录
+  updateTruckLoadRecord(data) {
+    return $http("/voyage/updateCarLoadRecord", data);
+  },
+
+  // 分配单据
+  distribute(data) {
+    return $http("/shipownerUploadFile/distribute", data);
+  },
+
+  // ocr识别
+  ocr(data) {
+    return $http("/shipownerUploadFile/ocr", data);
+  },
+
+  // 用户选择
+  getUserSelect(data) {
+    return $http("/user/backstage/select", data);
+  },
+
+  // 添加提货单
+  addLab(data) {
+    return $http("/voyage/addLab", data);
+  },
+
+  // 获取提货单
+  getLabList(data) {
+    return $http("/voyage/getLabList", data);
+  },
+
+  // 更新提货单
+  updateLab(data) {
+    return $http("/voyage/updateLab", data);
+  },
+
+  // 删除提货单
+  deleteLab(data) {
+    return $http("/voyage/deleteLab", data);
+  },
+
+  // 获取港口天气列表
+  getPortWeatherList(data) {
+    return $http("/voyage/getPortWeatherList", data);
+  },
+
+  // 获取超期航次提醒
+  getLongDaysInPort(data) {
+    return $http("/voyage/cargo/longDaysInPort", data);
+  },
+
+  // 获取区块链列表
+  getBlockChainList(data) {
+    return $http("/block/voyage/list", data);
+  },
+
+  // 航次上链
+  upBlockChain(data) {
+    return $http("/block/voyage/up", data);
+  },
+
+  // 插入卸货港
+  addNewPort(data) {
+    return $http("/voyage/backstage/addNewPort", data);
+  },
+
+  // 代理列表
+  getAgencyList(data) {
+    return $http("/proxy/list", data);
+  },
+
+  // 代理子账户列表
+  getAgencySubAccountList(data) {
+    return $http("/proxy/account/list", data);
+  },
+
+  // 添加代理子账户
+  addAgencySubAccount(data) {
+    return $http("/proxy/account/add", data);
+  },
+
+  // 获取货主公司列表
+  getCargoOwnerCompanyList(data) {
+    return $http("/cargoOwner/list", data);
+  },
+
+  // 获取货主公司详情
+  getCargoOwnerCompanyDetail(data) {
+    return $http("/cargoOwner/detail", data);
+  },
+
+  // 添加货主公司
+  addCargoOwnerCompany(data) {
+    return $http("/cargoOwner/add", data);
+  },
+
+  // 获取货主公司账号列表
+  getCargoOwnerAccountList(data) {
+    return $http("/cargoOwner/detail/account/list", data);
+  },
+
+  // 添加货主公司账号
+  addCargoOwnerAccount(data) {
+    return $http("/cargoOwner/detail/account/add", data);
+  },
+
+  // 添加代理公司
+  addAgencyCompany(data) {
+    return $http("/proxy/add", data);
+  },
+
+  // 获取代理公司列表
+  getAgencyCompanyList(data) {
+    return $http("/proxy/list", data);
+  },
+
+  // 获取代理公司详情
+  getAgencyCompanyDetail(data) {
+    return $http("/proxy/detail", data);
+  },
+
+  // 货主公司详情-代理公司列表
+  getAgencyCompanyByCargoOwnerCompany(data) {
+    return $http("/cargoOwner/detail/proxy/list", data);
+  },
+  // 货主公司添加账号-关联货种
+  getCargoList(data) {
+    return $http("/cargo/account/add/cargo/select", data);
+  },
+
+  // 关联代理公司-select
+  getAgencySelect(data) {
+    return $http("/cargoOwner/detail/proxy/select", data);
+  },
+
+  // 货主公司详情-关联代理;代理公司详情-关联货主公司
+  relateCargoAgency(data) {
+    return $http("/proxy/cargoOwner/add", data);
+  },
+
+  // 代理公司详情-货主公司列表
+  getCargoOwnerCompanyByAgencyCompany(data) {
+    return $http("/proxy/detail/cargoOwner/list", data);
+  },
+
+  // 关联货主公司-select
+  getCargoOwnerCompanySelect(data) {
+    return $http("/proxy/detail/cargoOwner/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


+ 118 - 0
src/components/Aside.vue

@@ -0,0 +1,118 @@
+<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 menu" :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"
+        @click="changeIndex(son.path)"
+      >
+        {{ son.name }}
+      </el-menu-item>
+    </el-sub-menu>
+  </el-menu>
+</template>
+<script>
+import { ref } from "vue";
+export default {
+  setup() {
+    let defaultActive = ref();
+    function changeIndex(path) {
+      defaultActive.value = path;
+    }
+    let menu = [
+      {
+        icon: "el-icon-s-unfold",
+        title: "货主信息",
+        items: [
+          {
+            path: "/cargoOwnerManage/cargoOwnerCompanyList",
+            name: "货主公司列表",
+          },
+        ],
+      },
+      {
+        icon: "el-icon-s-unfold",
+        title: "代理信息",
+        items: [
+          {
+            path: "/agencyManage/agencyCompanyList",
+            name: "代理公司列表",
+          },
+        ],
+      },
+      {
+        icon: "el-icon-s-unfold",
+        title: "船东信息",
+        items: [
+          {
+            path: "/shipOwnerManage/shipOwnerList",
+            name: "船东列表",
+          },
+        ],
+      },
+      {
+        icon: "el-icon-s-unfold",
+        title: "船舶信息",
+        items: [
+          {
+            path: "/shipManage/shipList",
+            name: "船舶列表",
+          },
+        ],
+      },
+      {
+        icon: "el-icon-s-unfold",
+        title: "港口信息",
+        items: [
+          {
+            path: "/portsManage/portsList",
+            name: "港口列表",
+          },
+          {
+            path: "/portsManage/sailingSchedule",
+            name: "航期维护",
+          },
+        ],
+      },
+      {
+        icon: "el-icon-s-unfold",
+        title: "航次区块链",
+        items: [
+          {
+            path: "/toolManage/blockChain",
+            name: "航次上链",
+          },
+        ],
+      },
+      {
+        icon: "el-icon-s-unfold",
+        title: "版本日志",
+        items: [
+          {
+            path: "/toolManage/versions",
+            name: "版本日志",
+          },
+        ],
+      },
+    ];
+
+    return {
+      changeIndex,
+      defaultActive,
+      menu,
+    };
+  },
+};
+</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>

+ 279 - 0
src/components/Header.vue

@@ -0,0 +1,279 @@
+<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">
+      <div
+        @click="dialogVisible = true"
+        class="pointer"
+        style="padding-top: 6px"
+      >
+        <el-badge
+          :hidden="isNewMessage.length == 0"
+          :value="isNewMessage.length"
+          class="mr30"
+        >
+          <el-icon :size="size" color="#00a9dc">
+            <BellFilled />
+          </el-icon>
+        </el-badge>
+      </div>
+      <img class="user-icon" src="../assets/user.png" alt="" />
+      <div class="user">{{ userName }}</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>
+    <el-dialog v-model="dialogVisible" title="拍照通知" width="30%">
+      <el-table :data="tableData[currentTableIndex - 1]" border>
+        <el-table-column align="center" type="index" />
+        <el-table-column align="center" property="shipName" label="船名" />
+        <el-table-column align="center" property="status" label="状态" />
+      </el-table>
+      <el-pagination
+        style="text-align: right; margin-top: 20px"
+        @current-change="pageChange"
+        background
+        layout="prev, pager, next"
+        :total="total"
+      >
+      </el-pagination>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button type="primary" @click="dialogVisible = false">
+            确定
+          </el-button>
+        </span>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+<script>
+import store from "../store";
+import router from "../router";
+import api from "../apis/fetch";
+import { onMounted, ref } from "vue";
+import { BellFilled } from "@element-plus/icons";
+import _ from "lodash";
+import { AnonymousLogin, tcb } from "../apis/cloudLogin";
+const db = tcb.database();
+const v = db.collection("huihenduo_versions");
+const __ = db.command;
+
+export default {
+  components: {
+    BellFilled,
+  },
+  setup() {
+    let userName = localStorage.staffName;
+    function quit() {
+      localStorage.removeItem("staffPhone");
+      localStorage.removeItem("id");
+      localStorage.removeItem("status");
+      localStorage.removeItem("userType");
+      localStorage.removeItem("staffName");
+      store.commit("changeLogin", false);
+      router.push({ path: "/login" });
+    }
+    let dialogVisible = ref(false);
+    let isNewMessage = ref([]);
+    function isWithinTime(t0 = new Date()) {
+      let t1 = _.cloneDeep(t0);
+      let t2 = _.cloneDeep(t0);
+      t1.setHours(8);
+      t1.setMinutes(30);
+      t1.setSeconds(0);
+      t2.setHours(9);
+      t2.setMinutes(30);
+      t2.setSeconds(0);
+      let t00 = t0.getTime();
+      let t11 = t1.getTime();
+      let t22 = t2.getTime();
+      return t00 > t11 && t00 < t22;
+    }
+    function spArr(arr, num) {
+      //arr是你要分割的数组,num是以几个为一组
+      let newArr = []; //首先创建一个新的空数组。用来存放分割好的数组
+      for (let i = 0; i < arr.length; ) {
+        //注意:这里与for循环不太一样的是,没有i++
+        newArr.push(arr.slice(i, (i += num)));
+      }
+      return newArr;
+    }
+    let tableData = ref([]);
+    let currentTableIndex = ref(1);
+    let total = ref(0);
+    function pageChange(c) {
+      currentTableIndex.value = c;
+    }
+    async function getUnphotographNotice() {
+      let { data } = await api.getUnphotographNotice();
+      if (data.status == 0) {
+        for (let i of data.result) {
+          isNewMessage.value.push({
+            shipName: i,
+            status: "未拍照",
+          });
+        }
+        total.value = isNewMessage.value.length;
+        tableData.value = spArr(isNewMessage.value, 10);
+      } else {
+        isNewMessage.value = 0;
+      }
+    }
+    onMounted(() => {
+      // getUnphotographNotice();
+      // setInterval(async () => {
+      //   if (isWithinTime()) {
+      //     getUnphotographNotice();
+      //   }
+      // }, 2 * 60 * 1000);
+      cloudLogin();
+    });
+    let timelineData = ref([]);
+    async function cloudLogin() {
+      await AnonymousLogin();
+      getAbledVersions();
+    }
+    async function getAbledVersions() {
+      let res = await v
+        .aggregate()
+        .match({ deleted: __.neq(true) })
+        .sort({
+          createTime: -1,
+        })
+        .limit(10)
+        .end();
+      timelineData.value = res.data;
+    }
+
+    const size = 20;
+    return {
+      size,
+      quit,
+      userName,
+      isNewMessage,
+      tableData,
+      dialogVisible,
+      currentTableIndex,
+      total,
+      pageChange,
+      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;
+}
+
+.title {
+  font-size: 21px;
+  font-family: PingFangSC-Regular, PingFang SC;
+  font-weight: 400;
+  color: #ffffff;
+  margin-left: 26px;
+}
+
+.user-icon {
+  width: 18px;
+  height: 18px;
+  margin-right: 16px;
+}
+
+.user,
+.quit,
+.log {
+  font-size: 14px;
+  font-family: PingFangSC-Medium, PingFang SC;
+  font-weight: 500;
+  color: #ffffff;
+  cursor: pointer;
+  margin-right: 16px;
+}
+
+.log {
+  margin-right: 4px;
+}
+
+.quit {
+  margin-left: 26px;
+}
+
+.log-card p {
+  font-size: 10px;
+}
+</style>

+ 199 - 0
src/components/PicTimeline.vue

@@ -0,0 +1,199 @@
+<template>
+  <div class="pic-container">
+    <div v-for="(item, index) in list" :key="item" class="pic-main">
+      <div :class="index % 2 == 0 ? 'box' : 'box bottom-box'">
+        <div class="card-note">
+          {{ item.userName }} 拍摄于
+          <br />
+          {{ item.createTime }}
+        </div>
+        <div class="media-box" style="position: relative">
+          <el-image
+            v-if="item.mediaType == 1"
+            style="width: 100%; height: 100%"
+            fit="contain"
+            :src="item.downloadUrl"
+            :preview-src-list="previewSrcList"
+          ></el-image>
+          <video
+            style="width: 100%; height: 100%"
+            v-else
+            :src="item.downloadUrl"
+          ></video>
+          <img
+            @click="openVideoModal(item.downloadUrl)"
+            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 class="checkbox-group df aic jcsa">
+          <el-checkbox
+            @change="auditMedia(item.id, 1, index, item.mediaType)"
+            :model-value="item.audit == 1"
+            label="通过"
+          ></el-checkbox>
+          <el-checkbox
+            @change="auditMedia(item.id, 2, index, item.mediaType)"
+            :model-value="item.audit == 2"
+            label="未通过"
+          ></el-checkbox>
+        </div>
+      </div>
+      <div :class="index % 2 == 0 ? 's-line' : 's-line top210px'"></div>
+      <div class="point"></div>
+      <div class="l-line" v-if="index + 1 != list.length"></div>
+    </div>
+    <el-dialog
+      v-model="videoModal"
+      title="视频审核"
+      width="20%"
+      :before-close="videoClose"
+    >
+      <video
+        autoplay
+        controls
+        style="width: 100%; height: 100%"
+        :src="currentUrl"
+      ></video>
+    </el-dialog>
+  </div>
+</template>
+<script>
+import { onMounted, ref, watch } from "vue";
+import store from "../store";
+import { useStore } from "vuex";
+import api from "../apis/fetch";
+export default {
+  setup(props, ctx) {
+    let currentUrl = ref("");
+    let videoModal = ref(false);
+    function openVideoModal(url) {
+      currentUrl.value = url;
+      videoModal.value = true;
+    }
+
+    async function auditMedia(mediaId, a, index, mediaType) {
+      //   console.log(mediaId, a, index, mediaType);
+      let res = await api.auditMedia({
+        mediaId,
+        audit: a,
+      });
+    }
+    onMounted(() => {});
+    return {
+      currentUrl,
+      videoModal,
+      openVideoModal,
+      auditMedia,
+      list,
+    };
+  },
+};
+</script>
+<style scoped>
+* {
+  --box-width: 200px;
+}
+
+.pic-container {
+  width: 100%;
+  height: 100%;
+  box-sizing: border-box;
+  display: flex;
+  padding: 20px;
+  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: 1px solid #dddddd;
+  transition: all 0.5s;
+  background: #fff;
+  z-index: 10;
+}
+
+.s-line {
+  position: absolute;
+  left: 100px;
+  top: 242px;
+  height: 20px;
+  border-left: 2px dashed;
+  box-sizing: border-box;
+  border-color: #97caf6;
+}
+.point {
+  position: relative;
+  left: 93px;
+  top: 258px;
+  width: 16px;
+  height: 16px;
+  background-image: url(../images/blue-circle.png);
+}
+
+.l-line {
+  position: relative;
+  bottom: 30px;
+  left: 111px;
+  top: 249px;
+  height: 3px;
+  width: 100px;
+  background-color: #0094fe;
+}
+
+.bottom-box {
+  top: 290px;
+}
+.top210px {
+  top: 270px;
+}
+
+.box:hover {
+  transform: scale(1.2);
+}
+
+.media-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;
+}
+
+.media-box {
+  width: 100%;
+  height: 100px;
+  margin-top: 20px;
+}
+
+.checkbox-group {
+  width: 200px;
+  height: 50px;
+  margin-top: 20px;
+}
+</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>

BIN
src/images/顺发999.png


+ 52 - 0
src/main.js

@@ -0,0 +1,52 @@
+import { createApp } from "vue";
+import ElementPlus from "element-plus";
+import "element-plus/dist/index.css";
+import App from "./App.vue";
+import router from "./router";
+import store from "./store";
+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("Certs", Certs);
+app.component("Uploader", Uploader);
+app.component("RemoteSearch", RemoteSearch);
+
+router.beforeEach(async (to, from, next) => {
+  let id = localStorage.id;
+  if (id) {
+    store.commit("changeLogin", true);
+    if (0 === to.matched.length) {
+      next("/cargoOwnerManage/cargoOwnerCompanyList");
+    } else if (to.path == "/login" || to.path == "/") {
+      next("/cargoOwnerManage/cargoOwnerCompanyList");
+    } else {
+      next();
+    }
+  } else {
+    localStorage.removeItem("staffPhone");
+    localStorage.removeItem("id");
+    localStorage.removeItem("status");
+    localStorage.removeItem("userType");
+    localStorage.removeItem("staffName");
+    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.config.globalProperties.check = () => {
+  console.log("check");
+};
+
+app.use(router).use(ElementPlus).use(store).mount("#app");

+ 177 - 0
src/router/index.js

@@ -0,0 +1,177 @@
+import {
+  createWebHistory,
+  createWebHashHistory,
+  createMemoryHistory,
+  createRouter,
+} from "vue-router";
+
+const router = createRouter({
+  history: createWebHashHistory(),
+  routes: [
+    {
+      path: "/",
+      name: "index",
+      component: () => import("../views/index/Index.vue"),
+    },
+    {
+      path: "/login",
+      name: "login",
+      meta: {
+        title: "登录",
+      },
+      component: () => import("../views/index/Login.vue"),
+    },
+    {
+      path: "/cargoOwnerManage/cargoOwnerCompanyDetail",
+      name: "cargoOwnerCompanyDetail",
+      meta: {
+        title: "货主详情",
+      },
+      component: () =>
+        import("../views/cargoOwnerManage/cargoOwnerCompanyDetail.vue"),
+    },
+    {
+      path: "/cargoOwnerManage/cargoOwnerCompanyList",
+      name: "cargoOwnerCompanyList",
+      meta: {
+        title: "货主列表",
+      },
+      component: () =>
+        import("../views/cargoOwnerManage/cargoOwnerCompanyList.vue"),
+    },
+    {
+      path: "/shipInfo/shipList",
+      name: "shipList",
+      meta: {
+        title: "船舶列表",
+      },
+      component: () => import("../views/shipInfo/shipList.vue"),
+    },
+    {
+      path: "/shipInfo/shipDetail",
+      name: "shipDetail",
+      meta: {
+        title: "船舶详情",
+      },
+      component: () => import("../views/shipInfo/shipDetail.vue"),
+    },
+    {
+      path: "/shipOwnerManage/shipOwnerDetail",
+      name: "shipOwnerDetail",
+      meta: {
+        title: "船东详情",
+      },
+      component: () => import("../views/shipOwnerManage/shipOwnerDetail.vue"),
+    },
+    {
+      path: "/shipOwnerManage/shipOwnerList",
+      name: "shipOwnerList",
+      meta: {
+        title: "船东列表",
+      },
+      component: () => import("../views/shipOwnerManage/shipOwnerList.vue"),
+    },
+
+    {
+      path: "/voyage/voyageAdd",
+      name: "voyageAdd",
+      meta: {
+        title: "添加航次",
+      },
+      component: () => import("../views/voyage/voyageAdd.vue"),
+    },
+
+    {
+      path: "/voyage/voyageDetail",
+      name: "voyageDetail",
+      meta: {
+        title: "航次详情",
+      },
+      component: () => import("../views/voyage/voyageDetail.vue"),
+    },
+
+    {
+      path: "/voyage/voyageList",
+      name: "voyageList",
+      meta: {
+        title: "航次列表",
+      },
+      component: () => import("../views/voyage/voyageList.vue"),
+    },
+
+    {
+      path: "/index/mediaCheck",
+      name: "mediaCheck",
+      meta: {
+        title: "鉴图/视频",
+      },
+      component: () => import("../views/index/mediaCheck.vue"),
+    },
+    {
+      path: "/agencyList",
+      name: "agencyList",
+      meta: {
+        title: "代理列表",
+      },
+      component: () => import("../views/index/AgencyList.vue"),
+    },
+    {
+      path: "/agencySubAccountList",
+      name: "agencySubAccountList",
+      meta: {
+        title: "代理子账户列表",
+      },
+      component: () => import("../views/index/AgencySubAccountList.vue"),
+    },
+    {
+      path: "/agencyManage/agencyCompanyList",
+      name: "agencyCompanyList",
+      meta: {
+        title: "代理公司列表",
+      },
+      component: () => import("../views/agencyManage/agencyCompanyList.vue"),
+    },
+    {
+      path: "/agencyManage/agencyCompanyDetail",
+      name: "agencyCompanyDetail",
+      meta: {
+        title: "代理公司详情",
+      },
+      component: () => import("../views/agencyManage/agencyCompanyDetail.vue"),
+    },
+    {
+      path: "/portsManage/portsList",
+      name: "portsList",
+      meta: {
+        title: "港口列表",
+      },
+      component: () => import("../views/portsManage/portsList.vue"),
+    },
+    {
+      path: "/portsManage/sailingSchedule",
+      name: "sailingSchedule",
+      meta: {
+        title: "航期维护",
+      },
+      component: () => import("../views/portsManage/sailingSchedule.vue"),
+    },
+    {
+      path: "/toolManage/blockChain",
+      name: "blockChain",
+      meta: {
+        title: "汇很多科技区块链",
+      },
+      component: () => import("../views/toolManage/blockChain.vue"),
+    },
+    {
+      path: "/toolManage/versions",
+      name: "versions",
+      meta: {
+        title: "版本日志管理",
+      },
+      component: () => import("../views/toolManage/versions.vue"),
+    },
+  ],
+});
+
+export default router;

+ 40 - 0
src/store/index.js

@@ -0,0 +1,40 @@
+import { createStore } from "vuex";
+
+console.log(import.meta.env.VITE_PROJECT_ENV);
+let baseurl = import.meta.env.VITE_BASEURL;
+const uploadUrl = `${baseurl}cos/upload`;
+const wayBillUrl = `${baseurl}voyage/uploadVoyageWayBill`;
+const fydi = `${baseurl}fydi/upload`;
+
+const store = createStore({
+  state: {
+    isLogin: false,
+    firstTitle: "",
+    secondTitle: "",
+    currentMenuItem: "/cargoOwnerManage/cargoOwnerCompanyList",
+    baseurl,
+    uploadUrl,
+    wayBillUrl,
+    fydi,
+    versions: [],
+  },
+  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;
+    },
+    setVersions(state, data) {
+      state.versions = data;
+    },
+  },
+});
+
+export default store;

+ 195 - 0
src/styles/index.css

@@ -0,0 +1,195 @@
+
+.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;
+}
+
+.mt40{
+  margin-top: 40px;
+}
+
+.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;
+}
+
+

+ 33 - 0
src/utils/downloadBlobFile.js

@@ -0,0 +1,33 @@
+import axios from "axios";
+function downloadBlobFile(url, data, name, type) {
+  return new Promise((resolve, reject) => {
+    axios({
+      method: type,
+      url,
+      responseType: "blob",
+      data,
+    })
+      .then((res) => {
+        let blob = new Blob([res.data], {
+          type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8",
+        });
+        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,
+        });
+      })
+      .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 };

+ 341 - 0
src/views/agencyManage/agencyCompanyDetail.vue

@@ -0,0 +1,341 @@
+<template>
+  <div class="line-container-p24">
+    <i class="el-icon-arrow-left"></i>
+    <div
+      class="dib go-back ml8"
+      @click="router.replace('/agencyManage/agencyCompanyList')"
+    >
+      返回代理公司列表
+    </div>
+  </div>
+  <div class="container-title">代理信息</div>
+  <div class="line-container-p24 df aic">
+    <div class="normal-label">代理名称</div>
+    <div class="show-input">{{ proxyName }}</div>
+    <div class="normal-label">联系人</div>
+    <div class="show-input">{{ contactName }}</div>
+    <div class="normal-label">联系人手机号</div>
+    <div class="show-input">{{ contactPhone }}</div>
+  </div>
+  <div class="container-title">货主信息</div>
+  <div class="line-container-p24">
+    <div style="display: flex; justify-content: space-between">
+      <div style="display: flex">
+        <el-input
+          placeholder="公司名称/联系人/手机号"
+          prefix-icon="el-icon-search"
+          v-model="term2"
+          clearable
+          style="width: 330px"
+        ></el-input>
+        <div class="seach-btn" @click="getCargoOwnerCompanyByAgencyCompany">
+          查询
+        </div>
+      </div>
+      <div class="cargo-owner-add" @click="dialogFormVisible2 = true">
+        关联货主公司
+      </div>
+      <el-dialog
+        title="关联货主公司"
+        v-model="dialogFormVisible2"
+        @closed="resetForm2"
+      >
+        <template v-slot:default>
+          <el-form
+            :model="ruleForm2"
+            :rules="rules2"
+            ref="form2"
+            label-width="110px"
+            label-position="left"
+          >
+            <el-form-item prop="cargoOwnerId" label="货主公司">
+              <RemoteSearch
+                api="getCargoOwnerCompanySelect"
+                v-model="cargoOwnerCompanyStr"
+                placeholder="公司名称/联系人/手机号"
+                @selectItem="selectCargoOwner($event)"
+                class="mb10"
+              ></RemoteSearch>
+            </el-form-item>
+          </el-form>
+        </template>
+        <template v-slot:footer>
+          <div class="dialog-footer">
+            <el-button @click="resetForm2">取 消</el-button>
+            <el-button type="primary" @click="relateCargoAgency(ruleForm2)">
+              确认关联
+            </el-button>
+          </div>
+        </template>
+      </el-dialog>
+    </div>
+    <div style="margin-top: 24px">
+      <el-table :data="tableData2" stripe style="width: 100%">
+        <el-table-column
+          type="index"
+          label="序号"
+          min-width="80"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="proxyName"
+          label="公司名称"
+          min-width="120"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="contactName"
+          label="联系人"
+          min-width="120"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="contactPhone"
+          label="联系人手机号"
+          min-width="160"
+          align="center"
+        ></el-table-column>
+      </el-table>
+      <div style="width: 100%; text-align: right; margin-top: 43px">
+        <el-pagination
+          background
+          layout="prev, pager, next"
+          :total="total2"
+          @current-change="pageChange2"
+        ></el-pagination>
+      </div>
+    </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 { useRoute } from "vue-router";
+import api from "../../apis/fetch";
+import { subTimeStr } from "../../utils/utils";
+
+const route = useRoute();
+let proxyName = ref();
+let contactName = ref();
+let contactPhone = ref();
+async function getAgencyCompanyDetail() {
+  let res = await api.getAgencyCompanyDetail({
+    proxyId: route.query.id,
+  });
+  if (res.data.status == 0) {
+    proxyName.value = res.data.result.proxyName;
+    contactName.value = res.data.result.contactName;
+    contactPhone.value = res.data.result.contactPhone;
+  } else {
+    console.log(res);
+  }
+}
+
+let tableData2 = ref([]);
+let term2 = ref("");
+let currentPage2 = ref(1);
+let total2 = ref(0);
+let dialogFormVisible2 = ref(false);
+let form2 = ref(null);
+
+async function getCargoOwnerCompanyByAgencyCompany() {
+  tableData2.value = [];
+  let res = await api.getCargoOwnerCompanyByAgencyCompany({
+    proxyId: route.query.id,
+    currentPage: currentPage2.value,
+    size: 10,
+    term: term2.value,
+  });
+  term2.value = "";
+  if (res.data.status == 0) {
+    tableData2.value = res.data.result;
+    total2.value = res.data.total;
+  }
+}
+let cargoOwnerCompanyStr = ref("");
+const ruleForm2 = ref({
+  cargoOwnerId: "",
+});
+const rules2 = ref({
+  cargoOwnerId: [
+    { required: true, message: "请选择货主公司", trigger: "blur" },
+  ],
+});
+
+function selectCargoOwner(item) {
+  ruleForm2.value.cargoOwnerId = item.key;
+  console.log(ruleForm2.value);
+}
+
+function resetForm2() {
+  dialogFormVisible2.value = false;
+  cargoOwnerCompanyStr.value = "";
+  form2.value.resetFields();
+}
+
+function pageChange2(e) {
+  currenrPage2.value = e;
+  getCargoOwnerCompanyByAgencyCompany();
+}
+
+async function relateCargoAgency() {
+  form2.value.validate(async (valid) => {
+    if (valid) {
+      let { cargoOwnerId } = ruleForm2.value;
+
+      let res = await api.relateCargoAgency({
+        proxyId: route.query.id,
+        cargoOwnerId,
+      });
+      console.log(res);
+      if (res.data.status == 0) {
+        ElNotification.success({
+          title: "添加成功",
+          duration: 2000,
+          message: `${res.data.msg}`,
+          type: "success",
+        });
+        resetForm2();
+        getCargoOwnerCompanyByAgencyCompany();
+      } else {
+        ElNotification.error({
+          title: "失败",
+          duration: 3000,
+          message: res.data.msg,
+        });
+      }
+    } else {
+      return false;
+    }
+  });
+}
+
+onMounted(() => {
+  getAgencyCompanyDetail();
+  getCargoOwnerCompanyByAgencyCompany();
+});
+</script>
+<style scoped>
+.go-back {
+  font-size: 16px;
+  font-family: PingFangSC-Medium, PingFang SC;
+  font-weight: 500;
+  color: #333d43;
+  line-height: 100%;
+  cursor: pointer;
+}
+
+.normal-label {
+  font-size: 14px;
+  font-family: PingFangSC-Regular, PingFang SC;
+  font-weight: 400;
+  color: #353a42;
+  margin-right: 10px;
+}
+
+.show-input {
+  width: 200px;
+  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: 103px;
+  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;
+}
+
+.right-radius {
+  border-top-right-radius: 19px;
+  border-bottom-right-radius: 19px;
+}
+.currentbtn {
+  background: #1486f9;
+  color: #fff;
+}
+
+.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: 15px;
+  cursor: pointer;
+  box-sizing: border-box;
+}
+
+.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: 15px;
+  cursor: pointer;
+  box-sizing: border-box;
+}
+
+.cargo-owner-add {
+  width: 120px;
+  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;
+  margin-right: 20px;
+}
+
+: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;
+}
+</style>

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

@@ -0,0 +1,256 @@
+<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="width: 330px"
+        ></el-input>
+        <div class="seach-btn" @click="getAgencyCompanyList">查询</div>
+      </div>
+      <div class="cargo-owner-add" @click="dialogFormVisible = true">
+        添加代理公司
+      </div>
+      <el-dialog title="添加代理公司" v-model="dialogFormVisible">
+        <template v-slot:default>
+          <el-form
+            :model="ruleForm"
+            :rules="rules"
+            ref="form"
+            label-width="110px"
+            label-position="left"
+          >
+            <el-form-item prop="companyName" label="代理公司名称">
+              <el-input
+                style="width: 280px"
+                v-model="ruleForm.companyName"
+              ></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>
+        </template>
+        <template v-slot:footer>
+          <div class="dialog-footer">
+            <el-button @click="resetForm">取 消</el-button>
+            <el-button type="primary" @click="addAgencyCompany(ruleForm)">
+              确 定
+            </el-button>
+          </div>
+        </template>
+      </el-dialog>
+    </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="companyName"
+          label="代理公司名称"
+          min-width="120"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="contactName"
+          label="联系人"
+          min-width="120"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="contactPhone"
+          label="联系人手机号"
+          min-width="160"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="createTime"
+          label="入驻时间"
+          min-width="200"
+          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">
+            <el-button
+              @click="agencyCompanyDetail(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>
+  </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 { subTimeStr } from "../../utils/utils";
+
+let currentPage = ref(1);
+let term = ref("");
+let tableData = ref([]);
+let total = ref(0);
+let dialogFormVisible = ref(false);
+let form = ref(null);
+const ruleForm = ref({
+  companyName: "",
+  contactName: "",
+  contactPhone: "",
+});
+const rules = ref({
+  companyName: [
+    { required: true, message: "请填写代理公司名称", trigger: "blur" },
+  ],
+  contactName: [{ required: true, message: "请填写联系人", trigger: "blur" }],
+  contactPhone: [
+    { required: true, message: "请填写手机号", trigger: "blur" },
+    { min: 11, max: 11, message: "请正确填写手机号", trigger: "blur" },
+  ],
+});
+async function getAgencyCompanyList() {
+  tableData.value = [];
+  let res = await api.getAgencyCompanyList({
+    currentPage: currentPage.value,
+    size: 10,
+    term: term.value,
+  });
+  term.value = "";
+  if (res.data.status == 0) {
+    tableData.value = res.data.result;
+    total.value = res.data.total;
+  }
+}
+function resetForm() {
+  dialogFormVisible.value = false;
+
+  form.value.resetFields();
+}
+async function addAgencyCompany() {
+  form.value.validate(async (valid) => {
+    if (valid) {
+      let { companyName, contactName, contactPhone } = ruleForm.value;
+      let res = await api.addAgencyCompany({
+        companyName,
+        contactName,
+        contactPhone,
+      });
+      console.log(res);
+      if (res.data.status == 0) {
+        ElNotification.success({
+          title: "添加成功",
+          duration: 0,
+          message: `${companyName}:${res.data.msg}`,
+          type: "success",
+        });
+        resetForm();
+        getAgencyCompanyList();
+      } else {
+        ElNotification.error({
+          title: "失败",
+          duration: 3000,
+          message: res.data.msg,
+        });
+      }
+    } else {
+      return false;
+    }
+  });
+}
+
+async function agencyCompanyDetail(id) {
+  router.push({
+    path: "/agencyManage/agencyCompanyDetail",
+    query: {
+      id,
+    },
+  });
+}
+function pageChange(e) {
+  currentPage.value = e;
+  getAgencyCompanyList();
+}
+onMounted(() => {
+  getAgencyCompanyList();
+});
+</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: 15px;
+  cursor: pointer;
+  box-sizing: border-box;
+}
+
+.cargo-owner-add {
+  width: 120px;
+  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;
+  margin-right: 20px;
+}
+
+: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;
+}
+</style>

+ 565 - 0
src/views/cargoOwnerManage/cargoOwnerCompanyDetail.vue

@@ -0,0 +1,565 @@
+<template>
+  <div class="line-container-p24">
+    <i class="el-icon-arrow-left"></i>
+    <div
+      class="dib go-back ml8"
+      @click="router.replace('/cargoOwnerManage/cargoOwnerCompanyList')"
+    >
+      返回货主列表
+    </div>
+  </div>
+  <div class="container-title">货主信息</div>
+  <div class="line-container-p24 df aic">
+    <div class="normal-label">货主名称</div>
+    <div class="show-input">{{ cargoOwnerName }}</div>
+    <div class="normal-label">联系人</div>
+    <div class="show-input">{{ contactName }}</div>
+    <div class="normal-label">联系人手机号</div>
+    <div class="show-input">{{ contactPhone }}</div>
+  </div>
+  <div class="container-title">账号信息</div>
+  <div class="line-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="width: 330px"
+        ></el-input>
+        <div class="seach-btn" @click="getCargoOwnerAccountList">查询</div>
+      </div>
+      <div class="cargo-owner-add" @click="dialogFormVisible = true">
+        添加账号
+      </div>
+      <el-dialog
+        title="添加账号"
+        v-model="dialogFormVisible"
+        @closed="resetForm"
+      >
+        <template v-slot:default>
+          <el-form
+            :model="ruleForm"
+            :rules="rules"
+            ref="form"
+            label-width="110px"
+            label-position="left"
+          >
+            <el-form-item label="货主公司名称">
+              {{ cargoOwnerName }}
+            </el-form-item>
+            <el-form-item prop="userName" label="姓名">
+              <el-input
+                style="width: 280px"
+                v-model="ruleForm.userName"
+              ></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="password" label="密码">
+              <el-input
+                style="width: 280px"
+                v-model="ruleForm.password"
+              ></el-input>
+            </el-form-item>
+            <!-- <el-form-item prop="cargo" label="关联货种">
+              <el-select
+                v-model="ruleForm.cargo"
+                placeholder="请选择"
+                style="width: 280px"
+              >
+                <el-option
+                  v-for="item in options"
+                  :key="item.value"
+                  :label="item.label"
+                  :value="item.key"
+                />
+              </el-select>
+            </el-form-item> -->
+          </el-form>
+        </template>
+        <template v-slot:footer>
+          <div class="dialog-footer">
+            <el-button @click="resetForm">取 消</el-button>
+            <el-button type="primary" @click="addCargoOwnerAccount(ruleForm)">
+              确认添加
+            </el-button>
+          </div>
+        </template>
+      </el-dialog>
+    </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="userName"
+          label="姓名"
+          min-width="120"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="phone"
+          label="手机号"
+          min-width="120"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="password"
+          label="密码"
+          min-width="160"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="accountStatus"
+          label="账户类型"
+          min-width="160"
+          align="center"
+        >
+          <template v-slot="scope">
+            {{ scope.row.accountStatus == 1 ? "主账户" : "子账户" }}
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop="cargo"
+          label="关联货种"
+          min-width="160"
+          align="center"
+        >
+          <template v-slot="scope">
+            {{ scope.row.cargo == "ALL" ? "全部" : "货种" }}
+          </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>
+  <div class="container-title">代理信息</div>
+  <div class="line-container-p24">
+    <div style="display: flex; justify-content: space-between">
+      <div style="display: flex">
+        <el-input
+          placeholder="公司名称/联系人/手机号"
+          prefix-icon="el-icon-search"
+          v-model="term2"
+          clearable
+          style="width: 330px"
+        ></el-input>
+        <div class="seach-btn" @click="getAgencyCompanyByCargoOwnerCompany">
+          查询
+        </div>
+      </div>
+      <div class="cargo-owner-add" @click="dialogFormVisible2 = true">
+        关联代理公司
+      </div>
+      <el-dialog
+        title="关联代理公司"
+        v-model="dialogFormVisible2"
+        @closed="resetForm2"
+      >
+        <template v-slot:default>
+          <el-form
+            :model="ruleForm2"
+            :rules="rules2"
+            ref="form2"
+            label-width="110px"
+            label-position="left"
+          >
+            <el-form-item prop="proxyId" label="代理公司:">
+              <RemoteSearch
+                api="getAgencySelect"
+                v-model="agencyStr"
+                @selectItem="selectAgency($event)"
+                class="mb10"
+              ></RemoteSearch>
+            </el-form-item>
+          </el-form>
+        </template>
+        <template v-slot:footer>
+          <div class="dialog-footer">
+            <el-button @click="resetForm2">取 消</el-button>
+            <el-button type="primary" @click="relateCargoAgency(ruleForm2)">
+              确认关联
+            </el-button>
+          </div>
+        </template>
+      </el-dialog>
+    </div>
+    <div style="margin-top: 24px">
+      <el-table :data="tableData2" stripe style="width: 100%">
+        <el-table-column
+          type="index"
+          label="序号"
+          min-width="80"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="agencyName"
+          label="代理公司名称"
+          min-width="120"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="contactName"
+          label="联系人"
+          min-width="120"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="contactPhone"
+          label="联系人手机号"
+          min-width="160"
+          align="center"
+        ></el-table-column>
+      </el-table>
+      <div style="width: 100%; text-align: right; margin-top: 43px">
+        <el-pagination
+          background
+          layout="prev, pager, next"
+          :total="total2"
+          @current-change="pageChange2"
+        ></el-pagination>
+      </div>
+    </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 { useRoute } from "vue-router";
+import api from "../../apis/fetch";
+import { subTimeStr } from "../../utils/utils";
+
+const route = useRoute();
+let cargoOwnerName = ref();
+let contactName = ref();
+let contactPhone = ref();
+async function getCargoOwnerCompanyDetail() {
+  let res = await api.getCargoOwnerCompanyDetail({
+    cargoOwnerId: route.query.id,
+  });
+  if (res.data.status == 0) {
+    cargoOwnerName.value = res.data.result.cargoOwnerName;
+    contactName.value = res.data.result.contactName;
+    contactPhone.value = res.data.result.contactPhone;
+  } else {
+    console.log(res);
+  }
+}
+
+let tableData = ref([]);
+let term = ref("");
+let currentPage = ref(1);
+let total = ref(0);
+let dialogFormVisible = ref(false);
+let form = ref(null);
+
+async function getCargoOwnerAccountList() {
+  tableData.value = [];
+  let res = await api.getCargoOwnerAccountList({
+    cargoOwnerId: route.query.id,
+    currentPage: currentPage.value,
+    size: 10,
+    term: term.value,
+  });
+  term.value = "";
+  if (res.data.status == 0) {
+    tableData.value = res.data.result;
+    total.value = res.data.total;
+  }
+}
+
+const ruleForm = ref({
+  userName: "",
+  phone: "",
+  password: "",
+  cargo: "",
+});
+const rules = ref({
+  userName: [{ required: true, message: "请填写姓名", trigger: "blur" }],
+  phone: [
+    { required: true, message: "请填写手机号", trigger: "blur" },
+    { min: 11, max: 11, message: "请正确填写手机号", trigger: "blur" },
+  ],
+  password: [{ required: false, message: "请填写密码", trigger: "blur" }],
+  cargo: [{ required: true, message: "请选择货种", trigger: "blur" }],
+});
+
+let options = ref([]);
+
+async function getCargoList() {
+  let res = await api.getCargoList({
+    cargoOwnerId: route.query.id,
+  });
+  options.value = res.data.result;
+}
+
+function resetForm() {
+  dialogFormVisible.value = false;
+
+  form.value.resetFields();
+}
+
+function pageChange(e) {
+  currenrPage.value = e;
+  getCargoOwnerAccountList();
+}
+
+async function addCargoOwnerAccount() {
+  form.value.validate(async (valid) => {
+    if (valid) {
+      let { userName, phone, password, cargo } = ruleForm.value;
+
+      let res = await api.addCargoOwnerAccount({
+        cargoOwnerId: route.query.id,
+        userName,
+        phone,
+        password,
+        cargo,
+      });
+      console.log(res);
+      if (res.data.status == 0) {
+        ElNotification.success({
+          title: "添加成功",
+          duration: 2000,
+          message: `${userName}:${res.data.msg}`,
+          type: "success",
+        });
+        resetForm();
+        getCargoOwnerAccountList();
+      } else {
+        ElNotification.error({
+          title: "失败",
+          duration: 3000,
+          message: res.data.msg,
+        });
+      }
+    } else {
+      return false;
+    }
+  });
+}
+
+let tableData2 = ref([]);
+let term2 = ref("");
+let currentPage2 = ref(1);
+let total2 = ref(0);
+let dialogFormVisible2 = ref(false);
+let form2 = ref(null);
+
+async function getAgencyCompanyByCargoOwnerCompany() {
+  tableData2.value = [];
+  let res = await api.getAgencyCompanyByCargoOwnerCompany({
+    cargoOwnerId: route.query.id,
+    currentPage: currentPage2.value,
+    size: 10,
+    term: term2.value,
+  });
+  term2.value = "";
+  if (res.data.status == 0) {
+    tableData2.value = res.data.result;
+    total2.value = res.data.total;
+  }
+}
+let agencyStr = ref("");
+const ruleForm2 = ref({
+  proxyId: "",
+});
+const rules2 = ref({
+  proxyId: [{ required: true, message: "请选择代理公司", trigger: "blur" }],
+});
+
+function selectAgency(item) {
+  ruleForm2.value.proxyId = item.key;
+}
+
+function resetForm2() {
+  dialogFormVisible2.value = false;
+  agencyStr.value = "";
+  form2.value.resetFields();
+}
+
+function pageChange2(e) {
+  currenrPage2.value = e;
+  getAgencyCompanyByCargoOwnerCompany();
+}
+
+async function relateCargoAgency() {
+  form2.value.validate(async (valid) => {
+    if (valid) {
+      let { proxyId } = ruleForm2.value;
+
+      let res = await api.relateCargoAgency({
+        cargoOwnerId: route.query.id,
+        proxyId,
+      });
+      console.log(res);
+      if (res.data.status == 0) {
+        ElNotification.success({
+          title: "添加成功",
+          duration: 2000,
+          message: `${res.data.msg}`,
+          type: "success",
+        });
+        resetForm2();
+        getAgencyCompanyByCargoOwnerCompany();
+      } else {
+        ElNotification.error({
+          title: "失败",
+          duration: 3000,
+          message: res.data.msg,
+        });
+      }
+    } else {
+      return false;
+    }
+  });
+}
+
+onMounted(() => {
+  getCargoOwnerCompanyDetail();
+  getCargoOwnerAccountList();
+  getAgencyCompanyByCargoOwnerCompany();
+  // getCargoList();
+});
+</script>
+<style scoped>
+.go-back {
+  font-size: 16px;
+  font-family: PingFangSC-Medium, PingFang SC;
+  font-weight: 500;
+  color: #333d43;
+  line-height: 100%;
+  cursor: pointer;
+}
+
+.normal-label {
+  font-size: 14px;
+  font-family: PingFangSC-Regular, PingFang SC;
+  font-weight: 400;
+  color: #353a42;
+  margin-right: 10px;
+}
+
+.show-input {
+  width: 200px;
+  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: 103px;
+  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;
+}
+
+.right-radius {
+  border-top-right-radius: 19px;
+  border-bottom-right-radius: 19px;
+}
+.currentbtn {
+  background: #1486f9;
+  color: #fff;
+}
+
+.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: 15px;
+  cursor: pointer;
+  box-sizing: border-box;
+}
+
+.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: 15px;
+  cursor: pointer;
+  box-sizing: border-box;
+}
+
+.cargo-owner-add {
+  width: 120px;
+  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;
+  margin-right: 20px;
+}
+
+: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;
+}
+</style>

+ 256 - 0
src/views/cargoOwnerManage/cargoOwnerCompanyList.vue

@@ -0,0 +1,256 @@
+<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="width: 330px"
+        ></el-input>
+        <div class="seach-btn" @click="getCargoOwnerCompanyList">查询</div>
+      </div>
+      <div class="cargo-owner-add" @click="dialogFormVisible = true">
+        添加货主公司
+      </div>
+      <el-dialog title="添加货主公司" v-model="dialogFormVisible">
+        <template v-slot:default>
+          <el-form
+            :model="ruleForm"
+            :rules="rules"
+            ref="form"
+            label-width="110px"
+            label-position="left"
+          >
+            <el-form-item prop="companyName" label="货主公司名称">
+              <el-input
+                style="width: 280px"
+                v-model="ruleForm.companyName"
+              ></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>
+        </template>
+        <template v-slot:footer>
+          <div class="dialog-footer">
+            <el-button @click="resetForm">取 消</el-button>
+            <el-button type="primary" @click="addCargoOwnerCompany(ruleForm)">
+              确 定
+            </el-button>
+          </div>
+        </template>
+      </el-dialog>
+    </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="companyName"
+          label="货主公司名称"
+          min-width="120"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="contactName"
+          label="联系人"
+          min-width="120"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="contactPhone"
+          label="联系人手机号"
+          min-width="160"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="createTime"
+          label="入驻时间"
+          min-width="200"
+          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">
+            <el-button
+              @click="cargoOwnerCompanyDetail(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>
+  </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 { subTimeStr } from "../../utils/utils";
+
+let currentPage = ref(1);
+let term = ref("");
+let tableData = ref([]);
+let total = ref(0);
+let dialogFormVisible = ref(false);
+let form = ref(null);
+const ruleForm = ref({
+  companyName: "",
+  contactName: "",
+  contactPhone: "",
+});
+const rules = ref({
+  companyName: [
+    { required: true, message: "请填写货主公司名称", trigger: "blur" },
+  ],
+  contactName: [{ required: true, message: "请填写联系人", trigger: "blur" }],
+  contactPhone: [
+    { required: true, message: "请填写手机号", trigger: "blur" },
+    { min: 11, max: 11, message: "请正确填写手机号", trigger: "blur" },
+  ],
+});
+async function getCargoOwnerCompanyList() {
+  tableData.value = [];
+  let res = await api.getCargoOwnerCompanyList({
+    currentPage: currentPage.value,
+    size: 10,
+    term: term.value,
+  });
+  term.value = "";
+  if (res.data.status == 0) {
+    tableData.value = res.data.result;
+    total.value = res.data.total;
+  }
+}
+function resetForm() {
+  dialogFormVisible.value = false;
+
+  form.value.resetFields();
+}
+async function addCargoOwnerCompany() {
+  form.value.validate(async (valid) => {
+    if (valid) {
+      let { companyName, contactName, contactPhone } = ruleForm.value;
+      let res = await api.addCargoOwnerCompany({
+        companyName,
+        contactName,
+        contactPhone,
+      });
+      console.log(res);
+      if (res.data.status == 0) {
+        ElNotification.success({
+          title: "添加成功",
+          duration: 0,
+          message: `${companyName}:${res.data.msg}`,
+          type: "success",
+        });
+        resetForm();
+        getCargoOwnerCompanyList();
+      } else {
+        ElNotification.error({
+          title: "失败",
+          duration: 3000,
+          message: res.data.msg,
+        });
+      }
+    } else {
+      return false;
+    }
+  });
+}
+
+async function cargoOwnerCompanyDetail(id) {
+  router.push({
+    path: "/cargoOwnerManage/cargoOwnerCompanyDetail",
+    query: {
+      id,
+    },
+  });
+}
+function pageChange(e) {
+  currentPage.value = e;
+  getCargoOwnerCompanyList();
+}
+onMounted(() => {
+  getCargoOwnerCompanyList();
+});
+</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: 15px;
+  cursor: pointer;
+  box-sizing: border-box;
+}
+
+.cargo-owner-add {
+  width: 120px;
+  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;
+  margin-right: 20px;
+}
+
+: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;
+}
+</style>

+ 9 - 0
src/views/cargoOwnerManageOld/cargoOwnerAdd.vue

@@ -0,0 +1,9 @@
+<template>cargoOwnerAdd</template>
+<script>
+export default {
+  setup() {
+    return {};
+  },
+};
+</script>
+<style scoped></style>

+ 298 - 0
src/views/cargoOwnerManageOld/cargoOwnerDetail.vue

@@ -0,0 +1,298 @@
+<template>
+  <div class="line-container-p24">
+    <i class="el-icon-arrow-left"></i>
+    <div
+      class="dib go-back ml8"
+      @click="router.replace('/cargoOwnerManage/cargoOwnerCompanyList')"
+    >
+      返回货主列表
+    </div>
+  </div>
+  <div class="container-title">货主信息</div>
+  <div class="line-container-p24 df aic">
+    <div class="normal-label">货主名称</div>
+    <div class="show-input">{{ userName }}</div>
+
+    <div class="normal-label">联系人</div>
+    <div class="show-input">{{ contactName }}</div>
+
+    <div class="normal-label">联系人手机号</div>
+    <div class="show-input">{{ userPhone }}</div>
+  </div>
+  <div class="container-title">航次信息</div>
+  <div class="line-container-p24">
+    <div class="df aic">
+      <div
+        @click="changeVoyageType(1)"
+        :class="
+          currentbtn
+            ? 'currentbtn radio-btns left-radius'
+            : 'radio-btns left-radius'
+        "
+      >
+        执行中航次
+      </div>
+      <div
+        @click="changeVoyageType(2)"
+        :class="
+          currentbtn
+            ? ' radio-btns right-radius'
+            : 'radio-btns right-radius currentbtn'
+        "
+        style="margin-right: 40px"
+      >
+        历史航次
+      </div>
+      <el-input
+        placeholder="请输入货主名称/联系人/联系人手机号"
+        prefix-icon="el-icon-search"
+        v-model="term"
+        style="width: 330px"
+        clearable
+      ></el-input>
+      <div class="seach-btn" @click="getVoyageList">查询</div>
+    </div>
+
+    <el-table :data="tableData" stripe style="width: 100%; margin-top: 20px">
+      <el-table-column
+        type="index"
+        label="序号"
+        min-width="80"
+        align="center"
+      ></el-table-column>
+      <el-table-column
+        prop="voyageName"
+        label="航次名称"
+        min-width="160"
+        align="center"
+      ></el-table-column>
+      <el-table-column
+        prop="loadDiscPort"
+        label="装货港-卸货港"
+        min-width="200"
+        align="center"
+      ></el-table-column>
+      <el-table-column
+        prop="startEndTime"
+        label="开始时间-结束时间"
+        min-width="200"
+        align="center"
+      ></el-table-column>
+      <el-table-column
+        prop="cargo"
+        label="货种"
+        min-width="100"
+        align="center"
+      ></el-table-column>
+
+      <el-table-column
+        prop="tons"
+        label="吨位(吨)"
+        min-width="100"
+        align="center"
+      ></el-table-column>
+
+      <el-table-column
+        prop="transStatus"
+        label="船舶状态"
+        min-width="100"
+        align="center"
+      ></el-table-column>
+
+      <el-table-column
+        prop="createTime"
+        label="备注"
+        min-width="100"
+        align="center"
+      ></el-table-column>
+      <el-table-column label="操作" min-width="80" align="center">
+        <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>
+import { ref, h, reactive, toRefs, onMounted } from "vue";
+import { ElNotification, ElMessageBox, ElMessage } from "element-plus";
+import store from "../../store";
+import router from "../../router";
+import { useRoute } from "vue-router";
+import api from "../../apis/fetch";
+
+export default {
+  setup() {
+    const route = useRoute();
+    let currentbtn = ref(true);
+    let userName = ref();
+    let contactName = ref();
+    let userPhone = ref();
+    let status = ref(1);
+    let term = ref("");
+    let currentPage = ref(1);
+    const tableData = ref();
+    async function getUserDetail() {
+      let res = await api.getUserDetail({
+        userId: route.query.userId,
+      });
+      if (res.data.status == 0) {
+        userName.value = res.data.result.userName;
+        contactName.value = res.data.result.contactName;
+        userPhone.value = res.data.result.userPhone;
+      } else {
+        console.log(res);
+      }
+    }
+
+    async function getVoyageList() {
+      tableData.value = [];
+      let res = await api.getVoyageList({
+        cargoOwnerId: route.query.userId,
+        shipId: 0,
+        status: status.value,
+        term: term.value,
+        currentPage: currentPage.value,
+        size: 10,
+      });
+      if (res.data.status == 0) {
+        tableData.value = res.data.result;
+      } else {
+        console.log(res);
+      }
+    }
+    function changeVoyageType(s) {
+      currentPage.value = 1;
+      currentbtn.value = s == 1;
+      status.value = s;
+      getVoyageList();
+    }
+
+    function pageChange(e) {
+      currentPage.value = e;
+      getVoyageList();
+    }
+
+    function voyageDetail(id) {
+      router.push({
+        path: "/voyage/voyageDetail",
+        query: {
+          id,
+        },
+      });
+    }
+
+    onMounted(() => {
+      getUserDetail();
+      getVoyageList();
+    });
+
+    return {
+      userName,
+      contactName,
+      userPhone,
+      status,
+      term,
+      currentPage,
+      currentbtn,
+      changeVoyageType,
+      getVoyageList,
+      voyageDetail,
+      pageChange,
+      tableData,
+      router,
+    };
+  },
+};
+</script>
+<style scoped>
+.go-back {
+  font-size: 16px;
+  font-family: PingFangSC-Medium, PingFang SC;
+  font-weight: 500;
+  color: #333d43;
+  line-height: 100%;
+  cursor: pointer;
+}
+
+.normal-label {
+  font-size: 14px;
+  font-family: PingFangSC-Regular, PingFang SC;
+  font-weight: 400;
+  color: #353a42;
+  margin-right: 10px;
+}
+
+.show-input {
+  width: 200px;
+  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: 103px;
+  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;
+}
+
+.right-radius {
+  border-top-right-radius: 19px;
+  border-bottom-right-radius: 19px;
+}
+.currentbtn {
+  background: #1486f9;
+  color: #fff;
+}
+
+.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: 15px;
+  cursor: pointer;
+  box-sizing: border-box;
+}
+</style>

+ 289 - 0
src/views/cargoOwnerManageOld/cargoOwnerList.vue

@@ -0,0 +1,289 @@
+<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="width: 330px"
+        ></el-input>
+        <div class="seach-btn" @click="getCargoOwnerList">查询</div>
+      </div>
+      <div class="cargo-owner-add" @click="dialogFormVisible = true">
+        添加货主
+      </div>
+      <el-dialog title="添加货主" v-model="dialogFormVisible">
+        <template v-slot:default>
+          <el-form
+            :model="ruleForm"
+            :rules="rules"
+            ref="form"
+            label-width="110px"
+            label-position="left"
+          >
+            <el-form-item prop="userName" label="货主名称">
+              <el-input
+                style="width: 280px"
+                v-model="ruleForm.userName"
+              ></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="userPhone" label="联系人手机号">
+              <el-input
+                style="width: 280px"
+                v-model="ruleForm.userPhone"
+              ></el-input>
+            </el-form-item>
+          </el-form>
+        </template>
+        <template v-slot:footer>
+          <div class="dialog-footer">
+            <el-button @click="resetForm">取 消</el-button>
+            <el-button type="primary" @click="addCargoOwner(ruleForm)">
+              确 定
+            </el-button>
+          </div>
+        </template>
+      </el-dialog>
+    </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="userName"
+          label="货主"
+          min-width="120"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="contactName"
+          label="联系人"
+          min-width="120"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="password"
+          label="密码"
+          min-width="120"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="userPhone"
+          label="联系人手机号"
+          min-width="160"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="createTime"
+          label="入驻时间"
+          min-width="200"
+          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">
+            <el-button
+              @click="cargoOwnerDetail(scope.row.userId, 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>
+  </div>
+</template>
+<script>
+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 { subTimeStr } from "../../utils/utils";
+
+export default {
+  setup() {
+    let currentPage = ref(1);
+    let term = ref("");
+    let tableData = ref([]);
+    let total = ref(0);
+    let dialogFormVisible = ref(false);
+    let form = ref(null);
+    const ruleForm = reactive({
+      ruleForm: {
+        userName: "",
+        contactName: "",
+        userPhone: "",
+      },
+    });
+    const rules = reactive({
+      rules: {
+        userName: [
+          { required: true, message: "请填写货主名称", trigger: "blur" },
+        ],
+        contactName: [
+          { required: true, message: "请填写联系人", trigger: "blur" },
+        ],
+        userPhone: [
+          { required: true, message: "请填写手机号", trigger: "blur" },
+          { min: 11, max: 11, message: "请正确填写手机号", trigger: "blur" },
+        ],
+      },
+    });
+    async function getCargoOwnerList() {
+      tableData.value = [];
+      let res = await api.getUserList({
+        identity: 2,
+        currentPage: currentPage.value,
+        size: 10,
+        term: term.value,
+      });
+      if (res.data.status == 0) {
+        tableData.value = res.data.result;
+        total.value = res.data.total;
+      }
+    }
+    function resetForm() {
+      dialogFormVisible.value = false;
+
+      form.value.resetFields();
+    }
+    async function addCargoOwner() {
+      form.value.validate(async (valid) => {
+        if (valid) {
+          let { userName, contactName, userPhone } = ruleForm.ruleForm;
+          let res = await api.addUser({
+            identity: 2,
+            userName,
+            contactName,
+            userPhone,
+            pwdStrategy: 1,
+          });
+          console.log(res);
+          if (res.data.status == 0) {
+            ElNotification.success({
+              title: "添加成功",
+              duration: 0,
+              message: `${userName}:${res.data.msg}`,
+              type: "success",
+            });
+            resetForm();
+            getCargoOwnerList();
+          } else {
+            ElNotification.error({
+              title: "失败",
+              duration: 3000,
+              message: res.data.msg,
+            });
+          }
+        } else {
+          return false;
+        }
+      });
+    }
+
+    async function cargoOwnerDetail(userId) {
+      router.push({
+        path: "/cargoOwnerManage/cargoOwnerDetail",
+        query: {
+          userId,
+        },
+      });
+    }
+    function pageChange(e) {
+      currentPage.value = e;
+      getCargoOwnerList();
+    }
+    getCargoOwnerList();
+    onMounted(() => {});
+    return {
+      currentPage,
+      term,
+      tableData,
+      total,
+      getCargoOwnerList,
+      addCargoOwner,
+      cargoOwnerDetail,
+      pageChange,
+      resetForm,
+      dialogFormVisible,
+      form,
+      ...toRefs(ruleForm),
+      ...toRefs(rules),
+      subTimeStr,
+    };
+  },
+};
+</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: 15px;
+  cursor: pointer;
+  box-sizing: border-box;
+}
+
+.cargo-owner-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;
+  margin-right: 20px;
+}
+
+: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;
+}
+</style>

+ 160 - 0
src/views/index/AgencyList.vue

@@ -0,0 +1,160 @@
+<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"
+        ></el-input>
+        <div class="seach-btn" @click="getAgencyList">查询</div>
+      </div>
+    </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="userName"
+          label="代理名称"
+          min-width="120"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="phone"
+          label="手机号"
+          min-width="160"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="createTime"
+          label="入驻时间"
+          min-width="200"
+          align="center"
+        ></el-table-column>
+        <el-table-column label="操作" min-width="80" align="center">
+          <template v-slot="scope">
+            <el-button
+              @click="agencySubAccountList(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>
+  </div>
+</template>
+<script>
+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";
+
+export default {
+  setup() {
+    let currentPage = ref(1);
+    let term = ref("");
+    let tableData = ref([]);
+    let total = ref(0);
+    async function getAgencyList() {
+      tableData.value = [];
+      let res = await api.getAgencyList({
+        currentPage: currentPage.value,
+        size: 10,
+        term: term.value,
+      });
+      if (res.data.status == 0) {
+        tableData.value = res.data.result;
+        total.value = res.data.total;
+      }
+    }
+
+    async function agencySubAccountList(id) {
+      router.push({
+        path: "/agencySubAccountList",
+        query: {
+          id,
+        },
+      });
+    }
+    function pageChange(e) {
+      currentPage.value = e;
+      getAgencyList();
+    }
+    getAgencyList();
+    onMounted(() => {});
+    return {
+      currentPage,
+      term,
+      tableData,
+      total,
+      getAgencyList,
+      agencySubAccountList,
+      pageChange,
+    };
+  },
+};
+</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;
+}
+
+.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;
+}
+</style>

+ 234 - 0
src/views/index/AgencySubAccountList.vue

@@ -0,0 +1,234 @@
+<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"
+        ></el-input>
+        <div class="seach-btn" @click="getAgencySubAccountList">查询</div>
+      </div>
+      <div class="cargo-owner-add" @click="dialogFormVisible = true">
+        添加代理子账户
+      </div>
+      <el-dialog
+        title="添加代理子账户"
+        v-model="dialogFormVisible"
+        @closed="resetForm"
+      >
+        <template v-slot:default>
+          <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>
+        </template>
+        <template v-slot:footer>
+          <div class="dialog-footer">
+            <el-button @click="resetForm">取 消</el-button>
+            <el-button type="primary" @click="addAgencySubAccount(ruleForm)">
+              确 定
+            </el-button>
+          </div>
+        </template>
+      </el-dialog>
+    </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="userName"
+          label="代理子账户名称"
+          min-width="120"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="phone"
+          label="手机号"
+          min-width="160"
+          align="center"
+        ></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>
+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 { useRoute } from "vue-router";
+
+export default {
+  setup() {
+    const route = useRoute();
+    let dialogFormVisible = ref(false);
+    let form = ref(null);
+    let ruleForm = reactive({
+      ruleForm: {
+        name: "",
+        phone: "",
+      },
+    });
+    async function resetForm() {
+      dialogFormVisible.value = false;
+      form.value.resetFields();
+    }
+    const rules = reactive({
+      rules: {
+        name: [
+          { required: true, message: "请填写代理子账户名称", trigger: "blur" },
+        ],
+        phone: [
+          { required: true, message: "请填写手机号", trigger: "blur" },
+          { min: 11, max: 11, message: "请正确填写手机号", trigger: "blur" },
+        ],
+      },
+    });
+    async function addAgencySubAccount() {
+      form.value.validate(async (valid) => {
+        if (valid) {
+          let { name, phone } = ruleForm.ruleForm;
+          let res = await api.addAgencySubAccount({
+            proxyId: route.query.id,
+            name,
+            phone,
+          });
+          console.log(res);
+          if (res.data.status == 0) {
+            ElNotification.success({
+              title: "添加成功",
+              duration: 0,
+              message: `${name}:${res.data.msg}`,
+              type: "success",
+            });
+            resetForm();
+            getAgencySubAccountList();
+          } else {
+            ElNotification.error({
+              title: "失败",
+              duration: 3000,
+              message: res.data.msg,
+            });
+          }
+        } else {
+          return false;
+        }
+      });
+    }
+    let currentPage = ref(1);
+    let term = ref("");
+    let tableData = ref([]);
+    let total = ref(0);
+    async function getAgencySubAccountList() {
+      tableData.value = [];
+      let res = await api.getAgencySubAccountList({
+        currentPage: currentPage.value,
+        size: 10,
+        term: term.value,
+        proxyId: route.query.id,
+      });
+      if (res.data.status == 0) {
+        tableData.value = res.data.result;
+        total.value = res.data.total;
+      }
+    }
+
+    function pageChange(e) {
+      currentPage.value = e;
+      getAgencySubAccountList();
+    }
+    getAgencySubAccountList();
+    onMounted(() => {});
+    return {
+      currentPage,
+      term,
+      tableData,
+      total,
+      getAgencySubAccountList,
+      pageChange,
+      dialogFormVisible,
+      ...toRefs(ruleForm),
+      resetForm,
+      addAgencySubAccount,
+      dialogFormVisible,
+      form,
+      ...toRefs(rules),
+    };
+  },
+};
+</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;
+}
+
+.cargo-owner-add {
+  width: 140px;
+  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;
+}
+</style>

+ 193 - 0
src/views/index/Blockchain.vue

@@ -0,0 +1,193 @@
+<template>
+  <div class="full-container-p24">
+    <div style="display: flex; justify-content: space-between">
+      <div style="display: flex">
+        <el-input
+          placeholder="货主名称/船名/MMSI"
+          prefix-icon="el-icon-search"
+          v-model="term"
+          clearable
+          style="height: 32px; width: 330px; line-height: 32px"
+        ></el-input>
+        <div class="seach-btn" @click="getBlockChainList">查询</div>
+      </div>
+    </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="voyageName"
+          label="航次名称"
+          min-width="160"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="shipName"
+          label="船名"
+          min-width="160"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="shipMmsi"
+          label="MMSI"
+          min-width="120"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="loadPort"
+          label="装货港"
+          min-width="160"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="dischargeProt"
+          label="卸货港"
+          min-width="120"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="loadTons"
+          label="载货吨位(吨)"
+          min-width="160"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="onChainTime"
+          label="上链时间"
+          min-width="200"
+          align="center"
+        ></el-table-column>
+        <el-table-column label="操作" min-width="80" align="center">
+          <template v-slot="scope">
+            <span v-if="scope.row.hash">已上链</span>
+            <el-button
+              v-else
+              @click="upBlockChain(scope.row.voyageId, scope.$index)"
+              type="primary"
+              size="small"
+              :loading="upLoading && currentIndex == scope.$index"
+            >
+              上链
+            </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 { 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";
+
+let currentPage = ref(1);
+let term = ref("");
+let tableData = ref();
+let total = ref();
+async function getBlockChainList() {
+  tableData.value = [];
+  let res = await api.getBlockChainList({
+    currentPage: currentPage.value,
+    size: 10,
+    term: term.value,
+  });
+  if (res.data.status == 0) {
+    tableData.value = res.data.result;
+    total.value = res.data.total;
+  }
+}
+
+let upLoading = ref(false);
+let currentIndex = ref(-1);
+
+async function upBlockChain(voyageId, index) {
+  upLoading.value = true;
+  currentIndex.value = index;
+  let res = await api.upBlockChain({
+    voyageId,
+  });
+  if (res.data.status == 0) {
+    ElNotification.success({
+      title: "成功",
+      duration: 2000,
+      message: res.data.msg,
+    });
+    getBlockChainList();
+  } else {
+    ElNotification.error({
+      title: "失败",
+      duration: 2000,
+      message: res.data.msg,
+    });
+  }
+  upLoading.value = false;
+  currentIndex.value = -1;
+}
+function pageChange(e) {
+  currentPage.value = e;
+  getBlockChainList();
+}
+onMounted(() => {
+  getBlockChainList();
+});
+</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;
+}
+
+.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;
+}
+</style>

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

@@ -0,0 +1,17 @@
+<template>
+  index
+  <div v-for="i in arr" :key="i">
+    <p>{{ i }}</p>
+  </div>
+</template>
+<script>
+export default {
+  data() {
+    return {
+      arr: ["a", "b", "c"],
+    };
+  },
+  methods: {},
+  created() {},
+};
+</script>

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

@@ -0,0 +1,232 @@
+<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>
+    </div>
+
+    <div @click="goBeian" class="copyright">
+      Copyright © 2021 河南省汇很多科技有限公司 豫ICP备 2021029101 号
+    </div>
+  </div>
+</template>
+<script>
+import { ref, reactive, toRefs } 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) => {});
+    }
+    async function login() {
+      // let res = await cloudConfig.doc("18ed09686196068205eeb77612d641c6").get();
+      // let { version } = res.data[0];
+      // localStorage.setItem("version", version);
+      form.value.validate(async (valid) => {
+        if (valid) {
+          let { phone, password } = ruleForm.ruleForm;
+          let res = await api.staffLogin({
+            phone,
+            password: md5(password).toUpperCase(),
+          });
+          if (res.data.status == 0) {
+            ElNotification.success({
+              title: "成功",
+              duration: 2000,
+              message: res.data.msg,
+              type: "success",
+            });
+            let { id, staffName, staffPhone, status } = res.data.result;
+            localStorage.setItem("id", id);
+            localStorage.setItem("staffName", staffName);
+            localStorage.setItem("staffPhone", staffPhone);
+            localStorage.setItem("status", status);
+            localStorage.setItem("userType", 2);
+            store.commit("changeLogin", true);
+            router.replace({ path: "/cargoOwnerManage/cargoOwnerCompanyList" });
+          } 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;
+  width: 105px;
+  background: url(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?sign=22b9335300bbef8d04da1b9b75589f7e&t=1634706935);
+  background-size: contain;
+  background-repeat: no-repeat;
+}
+
+.title-mid {
+  font-size: 25px;
+  color: #e4e4e4;
+  margin: 0 12px;
+}
+
+.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>

+ 343 - 0
src/views/index/Versions.vue

@@ -0,0 +1,343 @@
+<template>
+  <div class="mt10 df aic jcsb">
+    <div>
+      <el-button type="primary" class="mb20 mr30" @click="isModalVisable = true"
+        >添加版本记录</el-button
+      >
+      <el-popover placement="bottom" trigger="hover" :width="240">
+        <div style="width: 100%; height: 80vh; overflow-y: scroll">
+          <el-timeline>
+            <el-timeline-item
+              v-for="item in store.state.versions"
+              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-button> 新功能日志</el-button>
+          </el-badge>
+        </template>
+      </el-popover>
+    </div>
+    <div>
+      <!-- <el-button @click="">返回版本记录列表</el-button>
+      <el-button class="mr20 ml20" type="warning" @click="trash"
+        >回收站</el-button
+      > -->
+    </div>
+
+    <el-dialog
+      title="版本管理"
+      width="500px"
+      v-model="isModalVisable"
+      @closed="cancel"
+    >
+      <el-form
+        ref="ruleFormRef"
+        :model="ruleForm"
+        :rules="rules"
+        class="demo-ruleForm"
+        label-width="100px"
+        label-position="left"
+      >
+        <el-form-item label="版本号" prop="version">
+          <el-input style="width: 240px" v-model="ruleForm.version"></el-input>
+        </el-form-item>
+        <el-form-item label="发布时间" prop="timer">
+          <el-date-picker
+            style="width: 240px"
+            v-model="ruleForm.timer"
+            type="date"
+            placeholder="请选择日期"
+            format="YYYY/MM/DD"
+            value-format="YYYY/MM/DD"
+          >
+          </el-date-picker>
+        </el-form-item>
+        <hr class="mb20" />
+        <el-form-item
+          v-for="(item, index) in ruleForm.remarks"
+          :label="'发布内容 ' + parseInt(index + 1) + ' :'"
+          prop="remarks"
+        >
+          <div class="df aic jcsb">
+            <el-input
+              type="textarea"
+              style="width: 240px"
+              v-model="item.text"
+              autosize
+            ></el-input>
+            <el-button size="small" @click="deleteRemark(index)" type="danger"
+              >删除</el-button
+            >
+          </div>
+        </el-form-item>
+        <div class="df aic jcfe">
+          <el-button
+            size="small"
+            type="primary"
+            @click="ruleForm.remarks.push({})"
+            >添加一条记录</el-button
+          >
+        </div>
+        <hr class="mt20 mb20" />
+        <div class="df aic jcfe">
+          <el-button @click="cancel(ruleFormRef)">取消</el-button>
+          <el-button type="primary" @click="add(ruleFormRef)">提交</el-button>
+        </div>
+      </el-form>
+    </el-dialog>
+  </div>
+  <el-table :data="versions">
+    <el-table-column
+      type="index"
+      label="序号"
+      min-width="120"
+      align="center"
+    ></el-table-column>
+    <el-table-column
+      prop="version"
+      label="版本号"
+      min-width="100"
+      align="center"
+    ></el-table-column>
+    <el-table-column
+      prop="timer"
+      label="发布时间"
+      min-width="100"
+      align="center"
+    ></el-table-column>
+    <el-table-column label="版本内容" min-width="150" align="center">
+      <template v-slot="scope">
+        <div
+          style="width: 100%; text-align: left"
+          v-for="(item, index) in scope.row.remarks"
+        >
+          {{ index + 1 + ". " + item.text }}
+        </div>
+      </template>
+    </el-table-column>
+    <el-table-column label="状态" min-width="150" align="center">
+      <template v-slot="scope">
+        <el-switch
+          v-model="scope.row.disabled"
+          active-color="#13ce66"
+          :active-value="false"
+          :inactive-value="true"
+          inactive-color="#ff4949"
+          @change="changeSwitch($event, scope.row._id)"
+        />
+      </template>
+    </el-table-column>
+    <el-table-column label="操作" min-width="150" align="center">
+      <template v-slot="scope">
+        <el-button
+          @click="detail(scope.row, scope.$index)"
+          type="primary"
+          size="small"
+        >
+          修改
+        </el-button>
+        <el-button
+          @click="deleteItem(scope.row._id, scope.$index)"
+          type="danger"
+          size="small"
+        >
+          删除
+        </el-button>
+      </template>
+    </el-table-column>
+  </el-table>
+  <div style="width: 100%; text-align: right; padding: 43px 0">
+    <el-pagination
+      background
+      layout="prev, pager, next"
+      :total="total"
+      :page-size="pageSize"
+      @current-change="pageChange"
+    ></el-pagination>
+  </div>
+</template>
+
+<script setup>
+import { reactive, ref, onMounted } from "vue";
+import { AnonymousLogin, tcb } from "apis/cloudLogin";
+import _ from "lodash";
+import { ElMessage, ElMessageBox } from "element-plus";
+import store from "../../store";
+const db = tcb.database();
+const v = db.collection("huihenduo_versions");
+const __ = db.command;
+
+let versions = ref([]);
+let isModalVisable = ref(false);
+let ruleFormRef = ref(null);
+let currentPage = ref(1);
+let pageSize = ref(10);
+let currentVersionId = ref(null);
+let total = ref(0);
+
+function detail(item1) {
+  let item = _.cloneDeep(item1);
+  let { _id, version, timer, remarks } = item;
+  currentVersionId.value = _id;
+  ruleForm.value = { version, timer, remarks };
+
+  isModalVisable.value = true;
+}
+
+async function deleteItem(_id) {
+  ElMessageBox.confirm("确认删除版本日志?", "Warning", {
+    confirmButtonText: "删除",
+    cancelButtonText: "取消",
+    type: "warning",
+  })
+    .then(async () => {
+      let res = await v.doc(_id).update({
+        deleted: true,
+      });
+      getAllVersions();
+    })
+    .catch(() => {
+      ElMessage({
+        type: "info",
+        message: "取消删除",
+      });
+    });
+}
+
+async function changeSwitch(disabled, id) {
+  let res = await v.doc(id).update({
+    disabled,
+  });
+  getAllVersions();
+}
+
+function deleteRemark(index) {
+  ruleForm.value.remarks.splice(index, 1);
+}
+
+async function add() {
+  ruleFormRef.value.validate(async (valid) => {
+    if (valid) {
+      let res;
+      for (let i in ruleForm.value.remarks) {
+        if (!ruleForm.value.remarks[i].text) {
+          ruleForm.value.remarks.splice(i, 1);
+        }
+      }
+      if (currentVersionId.value) {
+        res = await v.doc(currentVersionId.value).update({
+          ...ruleForm.value,
+          updateTime: db.serverDate(),
+        });
+      } else {
+        res = await v.add({
+          ...ruleForm.value,
+          disabled: false,
+          createTime: db.serverDate(),
+        });
+      }
+
+      getAllVersions();
+      cancel();
+    } else {
+      console.log("未提交", ruleForm.value);
+    }
+  });
+}
+
+async function getAllVersions(listType) {
+  let res1 = await v.where({ deleted: __.neq(true) }).count();
+  total.value = res1.total;
+  let res = await v
+    .aggregate()
+    .match({ deleted: __.neq(true) })
+    .sort({
+      createTime: -1,
+    })
+    .skip((currentPage.value - 1) * pageSize.value)
+    .limit(pageSize.value)
+    .end();
+  versions.value = res.data;
+  getAbledVersions();
+}
+
+let abledVersions = ref([]);
+async function getAbledVersions() {
+  let res = await v
+    .aggregate()
+    .match({ deleted: __.neq(true) })
+    .sort({
+      createTime: -1,
+    })
+    .limit(10)
+    .end();
+  abledVersions.value = res.data;
+  store.commit("setVersions", abledVersions.value);
+}
+
+function cancel() {
+  isModalVisable.value = false;
+  ruleForm.value = {
+    version: "",
+    timer: "",
+    remarks: [{}, {}, {}],
+  };
+  currentVersionId.value = null;
+}
+
+let ruleForm = ref({
+  version: "",
+  timer: "",
+  remarks: [{}, {}, {}],
+});
+
+const rules = reactive({
+  version: [
+    {
+      required: true,
+      message: "请填写版本号",
+      trigger: "blur",
+    },
+  ],
+  timer: [
+    {
+      required: true,
+      message: "请填写发布时间",
+      trigger: "blur",
+    },
+  ],
+});
+async function init() {
+  await AnonymousLogin();
+  getAllVersions();
+}
+
+function pageChange(e) {
+  currentPage.value = e;
+  getAllVersions();
+}
+
+onMounted(() => {
+  init();
+});
+</script>
+
+<style scoped>
+</style>

+ 307 - 0
src/views/index/mediaCheck.vue

@@ -0,0 +1,307 @@
+<template>
+  <div class="df aic">
+    <div
+      @click="changeMediaStatus(0)"
+      :class="
+        audit == 0
+          ? 'currentbtn radio-btns left-radius'
+          : 'radio-btns left-radius'
+      "
+    >
+      待审核
+    </div>
+    <div
+      @click="changeMediaStatus(1)"
+      :class="audit == 1 ? ' radio-btns currentbtn' : 'radio-btns '"
+      style="border-right: none; border-left: none"
+    >
+      审核通过
+    </div>
+    <div
+      @click="changeMediaStatus(2)"
+      :class="
+        audit == 2
+          ? ' radio-btns right-radius currentbtn'
+          : 'radio-btns right-radius '
+      "
+      style="margin-right: 40px"
+    >
+      审核未通过
+    </div>
+  </div>
+  <div class="container-title">图片/视频</div>
+  <div class="line-container-p24 df aic ffw">
+    <el-card
+      style="
+        width: 240px;
+        height: 360px;
+        margin-left: 20px;
+        margin-bottom: 15px;
+      "
+      v-for="(item, index) in media"
+      :key="item"
+      shadow="hover"
+    >
+      <div class="card-note">
+        {{ item.shipName }} 拍摄于
+        <br />
+        {{ item.createTime }}
+      </div>
+      <div class="media-box" style="position: relative">
+        <el-image
+          v-if="item.mediaType == 1"
+          style="width: 100%; height: 100%"
+          fit="contain"
+          :src="item.downloadUrl"
+          :preview-src-list="previewSrcList"
+        ></el-image>
+        <video
+          style="width: 100%; height: 100%"
+          v-else
+          :src="item.downloadUrl"
+        ></video>
+        <img
+          @click="openVideoModal(item.downloadUrl, index, item.id, item.audit)"
+          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 class="checkbox-group df aic jcsa">
+        <el-checkbox
+          @change="auditMedia(item.id, 1, index, item.mediaType)"
+          :model-value="item.audit == 1"
+          label="通过"
+        ></el-checkbox>
+        <el-checkbox
+          @change="auditMedia(item.id, 2, index, item.mediaType)"
+          :model-value="item.audit == 2"
+          label="未通过"
+        ></el-checkbox>
+      </div>
+    </el-card>
+    <div style="width: 100%; text-align: right; margin-top: 43px">
+      <el-pagination
+        background
+        layout="prev, pager, next"
+        :total="total"
+        @current-change="pageChange"
+      />
+    </div>
+  </div>
+
+  <el-dialog
+    v-model="videoModal"
+    title="视频审核"
+    width="30%"
+    :before-close="videoClose"
+  >
+    <video
+      autoplay
+      controls
+      style="width: 100%; height: 100%"
+      :src="currentUrl"
+    ></video>
+    <template #footer>
+      <span class="dialog-footer">
+        <el-button type="primary" @click="dialogAuditVideo(1)">通过</el-button>
+        <el-button type="warning" @click="dialogAuditVideo(2)">
+          未通过
+        </el-button>
+      </span>
+    </template>
+  </el-dialog>
+</template>
+<script>
+import { ref, toRefs, reactive } from "vue";
+import { ElNotification } from "element-plus";
+import api from "../../apis/fetch";
+export default {
+  setup() {
+    let checkedBox = ref();
+    let audit = ref(0);
+    let currentPage = ref(1);
+    let media = ref([]);
+    let photos = ref([]);
+    let videoes = ref([]);
+    let previewSrcList = ref([]);
+    let total = ref();
+
+    async function getMediaList() {
+      let res = await api.getMediaList({
+        audit: audit.value,
+        currentPage: currentPage.value,
+        size: 10,
+      });
+      if (res.data.status == 0) {
+        total.value = res.data.total;
+        media.value = res.data.result;
+        for (let i of media.value) {
+          if (i.mediaType == 1) {
+            previewSrcList.value.push(i.downloadUrl);
+          }
+        }
+      } else {
+        console.log(res);
+      }
+    }
+
+    function pageChange(e) {
+      currentPage.value = e;
+      getMediaList();
+    }
+
+    function changeMediaStatus(s) {
+      currentPage.value = 1;
+      photos.value = [];
+      videoes.value = [];
+      audit.value = s;
+      getMediaList();
+    }
+
+    async function auditMedia(mediaId, a, index, mediaType) {
+      if (a == audit.value) return;
+      let res = await api.auditMedia({
+        mediaId,
+        audit: a,
+      });
+      if (res.data.status == 0) {
+        videoModal.value = false;
+        ElNotification({
+          title: res.data.msg,
+          message: `${a == 1 ? "通过" : "未通过"}`,
+          type: `${a == 1 ? "success" : "info"}`,
+        });
+        media.value.splice(index, 1);
+        if (mediaType == 1) {
+          previewSrcList.value.splice(index, 1);
+        }
+      } else {
+        ElNotification({
+          title: "Error!",
+          message: res.data.msg,
+          type: "error",
+        });
+      }
+      console.log(res);
+    }
+    let videoModal = ref(false);
+    let currentIndex = ref();
+    let currentId = ref();
+    let currentAudit = ref();
+    let currentUrl = ref();
+    function openVideoModal(url, i, id, a) {
+      currentIndex.value = i;
+      currentId.value = id;
+      currentAudit.value = a;
+      currentUrl.value = url;
+      videoModal.value = true;
+    }
+
+    function dialogAuditVideo(a) {
+      console.log(a);
+      console.log(currentAudit.value);
+      if (a == currentAudit.value) {
+        videoModal.value = false;
+      } else {
+        auditMedia(currentId.value, a, currentIndex.value, 2);
+      }
+    }
+
+    function videoClose() {}
+    getMediaList();
+
+    return {
+      audit,
+      getMediaList,
+      changeMediaStatus,
+      photos,
+      videoes,
+      checkedBox,
+      previewSrcList,
+      auditMedia,
+      media,
+      videoModal,
+      openVideoModal,
+      dialogAuditVideo,
+      currentUrl,
+      total,
+      pageChange,
+    };
+  },
+};
+</script>
+<style scoped>
+.radio-btns {
+  height: 38px;
+  width: 103px;
+  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;
+}
+
+.right-radius {
+  border-top-right-radius: 19px;
+  border-bottom-right-radius: 19px;
+}
+.currentbtn {
+  background: #1486f9;
+  color: #fff;
+}
+
+.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;
+}
+
+.card-note {
+  height: 30px;
+  font-size: 12px;
+  font-family: PingFangSC-Regular, PingFang SC;
+  font-weight: 400;
+  color: #777777;
+}
+
+.media-box {
+  width: 200px;
+  height: 200px;
+  margin-top: 20px;
+}
+
+.checkbox-group {
+  width: 200px;
+  height: 50px;
+  margin-top: 20px;
+}
+</style>

+ 256 - 0
src/views/portsManage/portsList.vue

@@ -0,0 +1,256 @@
+<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="width: 330px"
+        ></el-input>
+        <div class="seach-btn" @click="getCargoOwnerCompanyList">查询</div>
+      </div>
+      <div class="cargo-owner-add" @click="dialogFormVisible = true">
+        添加货主公司
+      </div>
+      <el-dialog title="添加货主公司" v-model="dialogFormVisible">
+        <template v-slot:default>
+          <el-form
+            :model="ruleForm"
+            :rules="rules"
+            ref="form"
+            label-width="110px"
+            label-position="left"
+          >
+            <el-form-item prop="companyName" label="货主公司名称">
+              <el-input
+                style="width: 280px"
+                v-model="ruleForm.companyName"
+              ></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>
+        </template>
+        <template v-slot:footer>
+          <div class="dialog-footer">
+            <el-button @click="resetForm">取 消</el-button>
+            <el-button type="primary" @click="addCargoOwnerCompany(ruleForm)">
+              确 定
+            </el-button>
+          </div>
+        </template>
+      </el-dialog>
+    </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="companyName"
+          label="货主公司名称"
+          min-width="120"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="contactName"
+          label="联系人"
+          min-width="120"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="contactPhone"
+          label="联系人手机号"
+          min-width="160"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="createTime"
+          label="入驻时间"
+          min-width="200"
+          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">
+            <el-button
+              @click="cargoOwnerCompanyDetail(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>
+  </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 { subTimeStr } from "../../utils/utils";
+
+let currentPage = ref(1);
+let term = ref("");
+let tableData = ref([]);
+let total = ref(0);
+let dialogFormVisible = ref(false);
+let form = ref(null);
+const ruleForm = ref({
+  companyName: "",
+  contactName: "",
+  contactPhone: "",
+});
+const rules = ref({
+  companyName: [
+    { required: true, message: "请填写货主公司名称", trigger: "blur" },
+  ],
+  contactName: [{ required: true, message: "请填写联系人", trigger: "blur" }],
+  contactPhone: [
+    { required: true, message: "请填写手机号", trigger: "blur" },
+    { min: 11, max: 11, message: "请正确填写手机号", trigger: "blur" },
+  ],
+});
+async function getCargoOwnerCompanyList() {
+  tableData.value = [];
+  let res = await api.getCargoOwnerCompanyList({
+    currentPage: currentPage.value,
+    size: 10,
+    term: term.value,
+  });
+  term.value = "";
+  if (res.data.status == 0) {
+    tableData.value = res.data.result;
+    total.value = res.data.total;
+  }
+}
+function resetForm() {
+  dialogFormVisible.value = false;
+
+  form.value.resetFields();
+}
+async function addCargoOwnerCompany() {
+  form.value.validate(async (valid) => {
+    if (valid) {
+      let { companyName, contactName, contactPhone } = ruleForm.value;
+      let res = await api.addCargoOwnerCompany({
+        companyName,
+        contactName,
+        contactPhone,
+      });
+      console.log(res);
+      if (res.data.status == 0) {
+        ElNotification.success({
+          title: "添加成功",
+          duration: 0,
+          message: `${companyName}:${res.data.msg}`,
+          type: "success",
+        });
+        resetForm();
+        getCargoOwnerCompanyList();
+      } else {
+        ElNotification.error({
+          title: "失败",
+          duration: 3000,
+          message: res.data.msg,
+        });
+      }
+    } else {
+      return false;
+    }
+  });
+}
+
+async function cargoOwnerCompanyDetail(id) {
+  router.push({
+    path: "/cargoOwnerManage/cargoOwnerCompanyDetail",
+    query: {
+      id,
+    },
+  });
+}
+function pageChange(e) {
+  currentPage.value = e;
+  getCargoOwnerCompanyList();
+}
+onMounted(() => {
+  getCargoOwnerCompanyList();
+});
+</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: 15px;
+  cursor: pointer;
+  box-sizing: border-box;
+}
+
+.cargo-owner-add {
+  width: 120px;
+  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;
+  margin-right: 20px;
+}
+
+: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;
+}
+</style>

+ 256 - 0
src/views/portsManage/sailingSchedule.vue

@@ -0,0 +1,256 @@
+<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="width: 330px"
+        ></el-input>
+        <div class="seach-btn" @click="getCargoOwnerCompanyList">查询</div>
+      </div>
+      <div class="cargo-owner-add" @click="dialogFormVisible = true">
+        添加货主公司
+      </div>
+      <el-dialog title="添加货主公司" v-model="dialogFormVisible">
+        <template v-slot:default>
+          <el-form
+            :model="ruleForm"
+            :rules="rules"
+            ref="form"
+            label-width="110px"
+            label-position="left"
+          >
+            <el-form-item prop="companyName" label="货主公司名称">
+              <el-input
+                style="width: 280px"
+                v-model="ruleForm.companyName"
+              ></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>
+        </template>
+        <template v-slot:footer>
+          <div class="dialog-footer">
+            <el-button @click="resetForm">取 消</el-button>
+            <el-button type="primary" @click="addCargoOwnerCompany(ruleForm)">
+              确 定
+            </el-button>
+          </div>
+        </template>
+      </el-dialog>
+    </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="companyName"
+          label="货主公司名称"
+          min-width="120"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="contactName"
+          label="联系人"
+          min-width="120"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="contactPhone"
+          label="联系人手机号"
+          min-width="160"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="createTime"
+          label="入驻时间"
+          min-width="200"
+          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">
+            <el-button
+              @click="cargoOwnerCompanyDetail(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>
+  </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 { subTimeStr } from "../../utils/utils";
+
+let currentPage = ref(1);
+let term = ref("");
+let tableData = ref([]);
+let total = ref(0);
+let dialogFormVisible = ref(false);
+let form = ref(null);
+const ruleForm = ref({
+  companyName: "",
+  contactName: "",
+  contactPhone: "",
+});
+const rules = ref({
+  companyName: [
+    { required: true, message: "请填写货主公司名称", trigger: "blur" },
+  ],
+  contactName: [{ required: true, message: "请填写联系人", trigger: "blur" }],
+  contactPhone: [
+    { required: true, message: "请填写手机号", trigger: "blur" },
+    { min: 11, max: 11, message: "请正确填写手机号", trigger: "blur" },
+  ],
+});
+async function getCargoOwnerCompanyList() {
+  tableData.value = [];
+  let res = await api.getCargoOwnerCompanyList({
+    currentPage: currentPage.value,
+    size: 10,
+    term: term.value,
+  });
+  term.value = "";
+  if (res.data.status == 0) {
+    tableData.value = res.data.result;
+    total.value = res.data.total;
+  }
+}
+function resetForm() {
+  dialogFormVisible.value = false;
+
+  form.value.resetFields();
+}
+async function addCargoOwnerCompany() {
+  form.value.validate(async (valid) => {
+    if (valid) {
+      let { companyName, contactName, contactPhone } = ruleForm.value;
+      let res = await api.addCargoOwnerCompany({
+        companyName,
+        contactName,
+        contactPhone,
+      });
+      console.log(res);
+      if (res.data.status == 0) {
+        ElNotification.success({
+          title: "添加成功",
+          duration: 0,
+          message: `${companyName}:${res.data.msg}`,
+          type: "success",
+        });
+        resetForm();
+        getCargoOwnerCompanyList();
+      } else {
+        ElNotification.error({
+          title: "失败",
+          duration: 3000,
+          message: res.data.msg,
+        });
+      }
+    } else {
+      return false;
+    }
+  });
+}
+
+async function cargoOwnerCompanyDetail(id) {
+  router.push({
+    path: "/cargoOwnerManage/cargoOwnerCompanyDetail",
+    query: {
+      id,
+    },
+  });
+}
+function pageChange(e) {
+  currentPage.value = e;
+  getCargoOwnerCompanyList();
+}
+onMounted(() => {
+  getCargoOwnerCompanyList();
+});
+</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: 15px;
+  cursor: pointer;
+  box-sizing: border-box;
+}
+
+.cargo-owner-add {
+  width: 120px;
+  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;
+  margin-right: 20px;
+}
+
+: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;
+}
+</style>

+ 821 - 0
src/views/shipInfo/shipDetail.vue

@@ -0,0 +1,821 @@
+<template>
+  <div class="line-container-p24">
+    <i class="el-icon-arrow-left"></i>
+    <div
+      class="dib go-back ml8 pointer"
+      @click="router.replace('/shipInfo/shipList')"
+    >
+      返回船舶列表
+    </div>
+  </div>
+  <div class="container-title">船舶信息</div>
+  <div class="line-container-p24">
+    <div class="line">
+      <div class="info-line">
+        <div class="info-line-title">船名</div>
+        <el-input
+          class="info-line-text"
+          v-model="shipDetail.shipname"
+          :disabled="unchangeable"
+        ></el-input>
+      </div>
+      <div class="info-line">
+        <div class="info-line-title">MMSI</div>
+        <el-input
+          class="info-line-text"
+          v-model="shipDetail.mmsi"
+          :disabled="unchangeable"
+        ></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="shipDetail.length"
+          :disabled="unchangeable"
+        ></el-input>
+      </div>
+      <div class="info-line">
+        <div class="info-line-title">船宽</div>
+        <el-input
+          class="info-line-text"
+          v-model="shipDetail.breadth"
+          :disabled="unchangeable"
+        ></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="shipDetail.tonnage"
+          :disabled="unchangeable"
+        ></el-input>
+      </div>
+      <div class="info-line">
+        <div class="info-line-title">载货吨位</div>
+        <el-input
+          class="info-line-text"
+          v-model="shipDetail.loadTons"
+          :disabled="unchangeable"
+        ></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="shipDetail.draught"
+          :disabled="unchangeable"
+        ></el-input>
+      </div>
+    </div>
+    <Certs ref="certs"></Certs>
+    <div class="df aic jcfe">
+      <el-button v-if="unchangeable" type="primary" @click="change">
+        修改
+      </el-button>
+      <el-button v-if="!unchangeable" @click="cancelChange">取消</el-button>
+      <el-button v-if="!unchangeable" type="primary" @click="submitChange">
+        提交
+      </el-button>
+    </div>
+    <div
+      style="margin-top: 60px; min-width: 800px; width: 90%; margin-left: 60px"
+    >
+      <el-table border :data="shipOwnerTableData" stripe style="width: 100%">
+        <el-table-column
+          type="index"
+          label="序号"
+          min-width="80"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="userName"
+          label="船东名称"
+          min-width="120"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="userPhone"
+          label="手机号"
+          min-width="160"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="createTime"
+          label="入驻时间"
+          min-width="200"
+          align="center"
+        ></el-table-column>
+        <el-table-column label="操作" min-width="80" align="center">
+          <template v-slot="scope">
+            <el-button
+              @click="shipOwnerDetail(scope.row.userId, 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="shipOwnerTotal"
+          @current-change="shipOwnerPageChange"
+        ></el-pagination>
+      </div>
+    </div>
+  </div>
+  <div class="container-title">航次信息</div>
+  <div class="full-container-p24">
+    <div style="display: flex; justify-content: space-between">
+      <div class="df aic">
+        <div
+          @click="changeVoyageType(1)"
+          :class="
+            currentbtn
+              ? 'currentbtn radio-btns left-radius'
+              : 'radio-btns left-radius'
+          "
+        >
+          执行中航次
+        </div>
+        <div
+          @click="changeVoyageType(2)"
+          :class="
+            currentbtn
+              ? ' radio-btns right-radius'
+              : 'radio-btns right-radius currentbtn'
+          "
+          style="margin-right: 40px"
+        >
+          历史航次
+        </div>
+        <el-input
+          placeholder="请输入货主名称/联系人/联系人手机号"
+          prefix-icon="el-icon-search"
+          v-model="term"
+          clearable
+          style="width: 330px"
+        ></el-input>
+        <div class="search-btn" @click="getVoyageList()">查询</div>
+      </div>
+      <div class="cargo-owner-add" @click="voyageAddDialogVisible = true">
+        添加航次
+      </div>
+    </div>
+    <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="选择船舶"
+              @select="selectShip"
+              disabled
+            />
+          </el-form-item>
+          <el-form-item prop="cargoOwnerId" label="货主">
+            <el-autocomplete
+              v-model="voyageForm.cargoOwnerName"
+              :fetch-suggestions="searchCargoOwner"
+              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"
+              placeholder="选择装货港"
+              @select="selectLoadPort"
+            />
+          </el-form-item>
+          <el-form-item prop="dischargeProt" label="卸货港">
+            <el-autocomplete
+              v-model="voyageForm.dischargeProt"
+              :fetch-suggestions="getCol"
+              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> </el-form-item>
+          <el-form-item prop="tons" label="吨位">
+            <el-input v-model="voyageForm.tons"></el-input>
+          </el-form-item>
+          <el-form-item prop="pieces" label="件数">
+            <el-input v-model="voyageForm.pieces"></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>
+    <el-table :data="tableData" stripe style="width: 100%; margin-top: 24px">
+      <el-table-column
+        type="index"
+        label="序号"
+        min-width="80"
+        align="center"
+      ></el-table-column>
+      <el-table-column
+        prop="voyageName"
+        label="航次名称"
+        min-width="120"
+        align="center"
+      ></el-table-column>
+      <el-table-column
+        prop="loadDiscPort"
+        label="装货港-卸货港"
+        min-width="200"
+        align="center"
+      ></el-table-column>
+      <el-table-column
+        prop="setSailTime"
+        label="开航时间"
+        min-width="180"
+        align="center"
+      ></el-table-column>
+      <el-table-column
+        prop="todayPhotoCount"
+        label="今日照片"
+        min-width="80"
+        align="center"
+      ></el-table-column>
+      <el-table-column
+        prop="cargo"
+        label="货种"
+        min-width="80"
+        align="center"
+      ></el-table-column>
+
+      <el-table-column
+        prop="tons"
+        label="吨位(吨)"
+        min-width="80"
+        align="center"
+      ></el-table-column>
+
+      <el-table-column
+        prop="transStatus"
+        label="船舶状态"
+        min-width="100"
+        align="center"
+      ></el-table-column>
+
+      <el-table-column
+        prop="remark"
+        label="备注"
+        min-width="100"
+        align="center"
+      ></el-table-column>
+      <el-table-column label="操作" min-width="80" align="center">
+        <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>
+// import { uploadUrl } from "../../apis/config";
+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 { useRoute } from "vue-router";
+import _ from "lodash";
+export default {
+  setup() {
+    const route = useRoute();
+    let shipDetail = ref({});
+    async function getShipDetail() {
+      let res = await api.getShipDetail({
+        shipId: route.query.shipId,
+      });
+      if (res.data.status == 0) {
+        shipDetail.value = res.data.result;
+        voyageForm.voyageForm.shipName = res.data.result.shipname;
+        voyageForm.voyageForm.shipId = res.data.result.id;
+        certs.value.initCerts(shipDetail.value.shipCertificates);
+      } else {
+        console.log(res);
+      }
+    }
+    let shipDetailCache = ref({});
+    function change() {
+      shipDetailCache.value = _.cloneDeep(shipDetail.value);
+      certs.value.editCerts();
+      unchangeable.value = false;
+    }
+
+    function cancelChange() {
+      if (!_.isEqual(shipDetail.value, shipDetailCache.value)) {
+        shipDetail.value = _.cloneDeep(shipDetailCache.value);
+      }
+      certs.value.cancelEditCerts();
+      unchangeable.value = true;
+    }
+
+    let unchangeable = ref(true);
+
+    async function submitChange() {
+      shipDetail.value.shipId = shipDetail.value.id;
+      shipDetail.value.shipCerts = certs.value.sendCerts();
+      delete shipDetail.value.shipCertificates;
+      let postData = {
+        ...shipDetail.value,
+        userId: 0,
+      };
+      let res = await api.updateShip(postData);
+      if (res.data.status == 0) {
+        unchangeable.value = true;
+        ElNotification({
+          type: "success",
+          title: res.data.msg,
+        });
+      } else {
+        ElNotification({
+          type: "error",
+          title: res.data.msg,
+        });
+        console.log(res);
+      }
+      certs.value.disabled = true;
+
+      let t = setTimeout(() => {
+        getShipDetail();
+        clearTimeout(t);
+      }, 500);
+    }
+    let currentbtn = ref(true);
+    let currentPage = ref(1);
+    let term = ref("");
+    let tableData = ref();
+    let total = ref(0);
+    let status = ref(1);
+    async function getVoyageList() {
+      tableData.value = [];
+
+      let res = await api.getVoyageList({
+        cargoOwnerId: 0,
+        shipId: route.query.shipId,
+        status: status.value,
+        term: term.value,
+        currentPage: currentPage.value,
+        size: 10,
+      });
+      if (res.data.status == 0) {
+        tableData.value = res.data.result;
+      }
+    }
+    function changeVoyageType(s) {
+      currentPage.value = 1;
+      currentbtn.value = s == 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" },
+        ],
+        shipOwnerId: [
+          { required: true, message: "请选择船东", trigger: "blur" },
+        ],
+        cargoOwnerId: [
+          { required: true, message: "请选择货主", trigger: "blur" },
+        ],
+        startTime: [
+          { required: true, message: "请填写开始时间", trigger: "blur" },
+        ],
+        endTime: [
+          { required: false, message: "请填写结束时间", trigger: "blur" },
+        ],
+        loadPort: [
+          { required: true, message: "请填写装货港", trigger: "blur" },
+        ],
+        dischargeProt: [
+          { required: true, message: "请填写卸货港", trigger: "blur" },
+        ],
+        cargo: [{ required: true, message: "请填写货种", trigger: "blur" }],
+        tons: [{ required: false, message: "请填写吨位", trigger: "blur" }],
+        pieces: [{ required: false, message: "请填写件数", trigger: "blur" }],
+      },
+    });
+    let voyageForm = reactive({
+      voyageForm: {
+        voyageName: "",
+        cargoOwnerId: "",
+        startTime: "",
+        endTime: "",
+        loadPort: "",
+        dischargeProt: "",
+        cargo: "",
+        tons: "",
+      },
+    });
+    let addVoyageForm = ref(null);
+
+    async function addVoyage() {
+      console.log("提交", voyageForm.voyageForm);
+      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();
+          } else {
+            console.log(res);
+            ElNotification({
+              title: res.data.msg,
+              type: "error",
+            });
+          }
+        }
+      });
+    }
+
+    async function searchShip(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}`;
+        }
+        cb(ships);
+      }
+    }
+    const selectShip = (item) => {
+      voyageForm.voyageForm.shipId = item.shipId;
+    };
+
+    async function searchCargoOwner(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}`;
+        }
+        cb(cargoOwners);
+      }
+    }
+
+    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) {
+          cb(res.data.result);
+        }
+      },
+      1500,
+      { 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 shipCurrentPage = ref(1);
+    let shipOwnerTableData = ref([]);
+    let shipOwnerCurrentPage = ref(1);
+    let shipOwnerTotal = ref(0);
+
+    async function getShipOwnerListByShipId() {
+      let res = await api.getShipOwnerListByShipId({
+        shipId: route.query.shipId,
+        currentPage: shipOwnerCurrentPage.value,
+        size: 10,
+      });
+      if (res.data.status == 0) {
+        shipOwnerTableData.value = res.data.result;
+        shipOwnerTotal.value = res.data.total;
+      } else {
+        console.log(res);
+      }
+    }
+
+    function shipOwnerDetail(userId) {
+      router.push({
+        path: "/shipOwnerManage/shipOwnerDetail",
+        query: {
+          userId,
+        },
+      });
+    }
+
+    function shipOwnerPageChange(e) {
+      shipOwnerCurrentPage.value = e;
+      getShipOwnerListByShipId();
+    }
+
+    let certs = ref(null);
+
+    onMounted(() => {
+      getShipDetail();
+      getVoyageList();
+      getShipOwnerListByShipId();
+    });
+    return {
+      unchangeable,
+      change,
+      cancelChange,
+      submitChange,
+      shipDetail,
+      router,
+      currentPage,
+      term,
+      tableData,
+      total,
+      currentbtn,
+      changeVoyageType,
+      getVoyageList,
+      voyageDetail,
+      pageChange,
+      goToVoyageAdd,
+      addVoyage,
+      voyageAddDialogVisible,
+      addVoyageForm,
+      ...toRefs(rules),
+      ...toRefs(voyageForm),
+      searchCargoOwner,
+      selectCargoOwner,
+      resetAddVoyageForm,
+      shipCurrentPage,
+      shipOwnerTableData,
+      shipOwnerCurrentPage,
+      getShipOwnerListByShipId,
+      shipOwnerDetail,
+      shipOwnerPageChange,
+      shipOwnerTotal,
+      getCol,
+      selectLoadPort,
+      selectDischargeProt,
+      searchShip,
+      selectShip,
+      certs,
+    };
+  },
+};
+</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: 103px;
+  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;
+}
+
+.right-radius {
+  border-top-right-radius: 19px;
+  border-bottom-right-radius: 19px;
+}
+.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;
+}
+
+.upload-text {
+  height: 25%;
+  color: rgb(139, 147, 156);
+}
+
+.upload-plus-icon {
+  height: 15%;
+  color: rgb(139, 147, 156);
+  line-height: 100px;
+  font-size: 40px;
+  font-weight: 200;
+}
+</style>

+ 184 - 0
src/views/shipInfo/shipList.vue

@@ -0,0 +1,184 @@
+<template>
+  <div class="full-container-p24">
+    <div style="display: flex; justify-content: space-between">
+      <div style="display: flex">
+        <el-input
+          placeholder="请输入船名/MMSI/手机号"
+          prefix-icon="el-icon-search"
+          v-model="term"
+          clearable
+          style="height: 32px; width: 330px; line-height: 32px"
+        ></el-input>
+        <div class="seach-btn" @click="getShipList">查询</div>
+      </div>
+    </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="shipname"
+          label="船舶名称"
+          min-width="120"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="mmsi"
+          label="MMSI"
+          min-width="160"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="length"
+          label="船长(米)"
+          min-width="120"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="breadth"
+          label="船宽(米)"
+          min-width="160"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="draught"
+          label="吃水(米)"
+          min-width="120"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="loadTons"
+          label="载货吨位(吨)"
+          min-width="160"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="createTime"
+          label="入驻时间"
+          min-width="200"
+          align="center"
+        ></el-table-column>
+        <el-table-column label="操作" min-width="80" align="center">
+          <template v-slot="scope">
+            <el-button
+              @click="shipDetail(scope.row.shipId, 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>
+  </div>
+</template>
+<script>
+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";
+
+export default {
+  setup() {
+    let currentPage = ref(1);
+    let term = ref("");
+    let tableData = ref();
+    let total = ref();
+    async function getShipList() {
+      tableData.value = [];
+      let res = await api.getShipList({
+        currentPage: currentPage.value,
+        size: 10,
+        term: term.value,
+      });
+      if (res.data.status == 0) {
+        tableData.value = res.data.result;
+        total.value = res.data.total;
+      }
+    }
+
+    async function shipDetail(shipId) {
+      router.push({
+        path: "/shipInfo/shipDetail",
+        query: {
+          shipId,
+        },
+      });
+    }
+    function pageChange(e) {
+      currentPage.value = e;
+      getShipList();
+    }
+    getShipList();
+    onMounted(() => {});
+    return {
+      currentPage,
+      term,
+      tableData,
+      total,
+      getShipList,
+      shipDetail,
+      pageChange,
+    };
+  },
+};
+</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;
+}
+
+.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;
+}
+</style>

+ 452 - 0
src/views/shipOwnerManage/shipOwnerDetail.vue

@@ -0,0 +1,452 @@
+<template>
+  <div class="line-container-p24">
+    <i class="el-icon-arrow-left"></i>
+    <div
+      class="dib go-back ml8 pointer"
+      @click="router.replace('/shipOwnerManage/shipOwnerList')"
+    >
+      返回船东列表
+    </div>
+  </div>
+
+  <div class="container-title">船东信息</div>
+  <div class="line-container-p24">
+    <div class="line">
+      <div class="info-line">
+        <div class="info-line-title">船东姓名</div>
+        <el-input
+          class="info-line-text"
+          v-model="userDetail.userName"
+          :disabled="unchangeableShipOwner"
+        ></el-input>
+      </div>
+      <div class="info-line">
+        <div class="info-line-title">船东手机号</div>
+        <el-input
+          class="info-line-text"
+          v-model="userDetail.phone"
+          :disabled="unchangeableShipOwner"
+        ></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="userDetail.idcardNo"
+          :disabled="unchangeableShipOwner"
+        ></el-input>
+      </div>
+      <div class="info-line">
+        <div class="info-line-title">偏好货种</div>
+        <el-input
+          class="info-line-text"
+          v-model="userDetail.preferenceCargo"
+          :disabled="unchangeableShipOwner"
+        ></el-input>
+      </div>
+    </div>
+    <div class="line">
+      <div class="info-line aic">
+        <div class="info-line-title">上传身份证 :</div>
+        <Uploader
+          :uploaderId="'idFrontFile'"
+          :params="idParams"
+          :disabled="unchangeableShipOwner"
+          @onSendFileList="idFrontUploadSuccess"
+          :fileList="idFrontFile"
+          :limit="1"
+          :uploadText="'身份证人像面'"
+        ></Uploader>
+        <div class="mr20"></div>
+        <Uploader
+          :uploaderId="'idBackFile'"
+          :params="idParams"
+          :disabled="unchangeableShipOwner"
+          @onSendFileList="idBackUploadSuccess"
+          :fileList="idBackFile"
+          :limit="1"
+          :uploadText="'身份证国徽面'"
+        ></Uploader>
+      </div>
+    </div>
+    <div class="df aic jcfe mt50">
+      <el-button v-if="unchangeableShipOwner" type="primary" @click="change(1)">
+        修改
+      </el-button>
+      <el-button v-if="!unchangeableShipOwner" @click="cancelChange(1)">
+        取消
+      </el-button>
+      <el-button
+        v-if="!unchangeableShipOwner"
+        type="primary"
+        @click="submitChange(1)"
+      >
+        提交
+      </el-button>
+    </div>
+  </div>
+  <div class="container-title">船舶信息</div>
+  <div class="line-container-p24">
+    <div class="line">
+      <div class="info-line">
+        <div class="info-line-title">船名</div>
+        <el-input
+          class="info-line-text"
+          v-model="shipDetail.shipname"
+          :disabled="unchangeableShip"
+        ></el-input>
+      </div>
+      <div class="info-line">
+        <div class="info-line-title">MMSI</div>
+        <el-input
+          class="info-line-text"
+          v-model="shipDetail.mmsi"
+          :disabled="unchangeableShip"
+        ></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="shipDetail.length"
+          :disabled="unchangeableShip"
+        ></el-input>
+      </div>
+      <div class="info-line">
+        <div class="info-line-title">船宽</div>
+        <el-input
+          class="info-line-text"
+          v-model="shipDetail.breadth"
+          :disabled="unchangeableShip"
+        ></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="shipDetail.tonnage"
+          :disabled="unchangeableShip"
+        ></el-input>
+      </div>
+      <div class="info-line">
+        <div class="info-line-title">载货吨位</div>
+        <el-input
+          class="info-line-text"
+          v-model="shipDetail.loadTons"
+          :disabled="unchangeableShip"
+        ></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="shipDetail.draught"
+          :disabled="unchangeableShip"
+        ></el-input>
+      </div>
+    </div>
+    <Certs ref="certs"></Certs>
+    <div class="df aic jcfe">
+      <el-button v-if="unchangeableShip" type="primary" @click="change(0)">
+        修改
+      </el-button>
+      <el-button v-if="!unchangeableShip" @click="cancelChange(0)">
+        取消
+      </el-button>
+      <el-button
+        v-if="!unchangeableShip"
+        type="primary"
+        @click="submitChange(0)"
+      >
+        提交
+      </el-button>
+    </div>
+  </div>
+</template>
+<script>
+import { ref, h, reactive, toRefs, onMounted } from "vue";
+// import { uploadUrl } from "../../apis/config";
+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 { useRoute } from "vue-router";
+import _ from "lodash";
+export default {
+  setup() {
+    const route = useRoute();
+    let userDetail = ref({});
+    let shipDetail = ref({});
+    let idFrontFile = ref([]);
+    let idBackFile = ref([]);
+    let unchangeableShipOwner = ref(true);
+    let unchangeableShip = ref(true);
+    async function getUserDetail() {
+      let res = await api.getUserDetail({
+        userId: route.query.userId,
+      });
+      if (res.data.status == 0) {
+        let r = res.data.result;
+
+        let {
+          //船东信息
+          idcardBackDownloadUrl,
+          idcardBackFileKey,
+          idcardBackViewUrl,
+          idcardFrontDownloadUrl,
+          idcardFrontFileKey,
+          idcardFrontViewUrl,
+          idcardNo,
+          phone,
+          preferenceCargo,
+          userId,
+          userName,
+
+          // 船舶信息
+          breadth,
+          draught,
+          length,
+          loadTons,
+          mmsi,
+          shipId,
+          shipname,
+          tonnage,
+          shipCerts,
+        } = r;
+
+        userDetail.value = {
+          idcardNo,
+          phone,
+          preferenceCargo,
+          userId,
+          userName,
+        };
+        idFrontFile.value = idcardFrontFileKey
+          ? [
+              {
+                url: idcardFrontViewUrl,
+                idcardFrontDownloadUrl,
+                idcardFrontFileKey,
+                idcardFrontViewUrl,
+              },
+            ]
+          : [];
+        idBackFile.value = idcardBackFileKey
+          ? [
+              {
+                url: idcardBackViewUrl,
+                idcardBackDownloadUrl,
+                idcardBackFileKey,
+                idcardBackViewUrl,
+              },
+            ]
+          : [];
+
+        shipDetail.value = {
+          breadth,
+          draught,
+          length,
+          loadTons,
+          mmsi,
+          shipId,
+          shipname,
+          tonnage,
+        };
+        certs.value.initCerts(shipCerts);
+      } else {
+        ElNotification({
+          type: "error",
+          title: res.data.msg,
+        });
+        console.log(res);
+      }
+    }
+
+    let idParams = ref({
+      type: 1,
+      location: "",
+      userId: 0,
+    });
+
+    let shipParams = ref({
+      type: 2,
+      userId: 0,
+      location: "",
+    });
+
+    function idFrontUploadSuccess(list) {
+      idFrontFile.value = list;
+    }
+    function idBackUploadSuccess(list) {
+      idBackFile.value = list;
+    }
+
+    let userDetailCache = ref({});
+    let idFrontFileCache = ref([]);
+    let idBackFileCache = ref([]);
+    let shipDetailCache = ref({});
+    function change(type) {
+      if (type) {
+        userDetailCache.value = _.cloneDeep(userDetail.value);
+        idFrontFileCache.value = _.cloneDeep(idFrontFile.value);
+        idBackFileCache.value = _.cloneDeep(idBackFile.value);
+        unchangeableShipOwner.value = false;
+      } else {
+        shipDetailCache.value = _.cloneDeep(shipDetail.value);
+        certs.value.editCerts();
+        unchangeableShip.value = false;
+      }
+    }
+
+    function cancelChange(type) {
+      if (type) {
+        if (!_.isEqual(userDetail.value, userDetailCache.value)) {
+          userDetail.value = _.cloneDeep(userDetailCache.value);
+        }
+        if (!_.isEqual(idFrontFile.value, idFrontFileCache.value)) {
+          idFrontFile.value = [];
+          idFrontFile.value = _.cloneDeep(idFrontFileCache.value);
+        }
+        if (!_.isEqual(idBackFile.value, idBackFileCache.value)) {
+          idBackFile.value = [];
+          idBackFile.value = _.cloneDeep(idBackFileCache.value);
+        }
+        unchangeableShipOwner.value = true;
+      } else {
+        if (!_.isEqual(shipDetail.value, shipDetailCache.value)) {
+          shipDetail.value = _.cloneDeep(shipDetailCache.value);
+        }
+        certs.value.cancelEditCerts();
+        unchangeableShip.value = true;
+      }
+    }
+
+    async function submitChange(type) {
+      if (type) {
+        let postData = {
+          ...userDetail.value,
+          idcardFrontDownloadUrl:
+            idFrontFile.value[0]?.response?.result?.downloadUrl ||
+            idFrontFile.value[0]?.idcardFrontDownloadUrl ||
+            "",
+          idcardFrontFileKey:
+            idFrontFile.value[0]?.response?.result?.key ||
+            idFrontFile.value[0]?.idcardFrontFileKey ||
+            "",
+          idcardFrontViewUrl:
+            idFrontFile.value[0]?.response?.result?.viewUrl ||
+            idFrontFile.value[0]?.idcardFrontViewUrl ||
+            "",
+          idcardBackDownloadUrl:
+            idBackFile.value[0]?.response?.result?.downloadUrl ||
+            idBackFile.value[0]?.idcardBackDownloadUrl ||
+            "",
+          idcardBackFileKey:
+            idBackFile.value[0]?.response?.result?.key ||
+            idBackFile.value[0]?.idcardBackFileKey ||
+            "",
+          idcardBackViewUrl:
+            idBackFile.value[0]?.response?.result?.viewUrl ||
+            idBackFile.value[0]?.idcardBackViewUrl ||
+            "",
+        };
+        let res = await api.updateUserDetail(postData);
+        if (res.data.status == 0) {
+          ElNotification({
+            type: "success",
+            title: res.data.msg,
+          });
+          unchangeableShipOwner.value = true;
+        } else {
+          ElNotification({
+            type: "error",
+            title: res.data.msg,
+          });
+          console.log(res);
+        }
+        getUserDetail();
+      } else {
+        shipDetail.value.shipCerts = certs.value.sendCerts();
+        let postData = {
+          ...shipDetail.value,
+          userId: route.query.userId,
+        };
+        postData.shipId = postData.shipId || 0;
+        let res = await api.updateShip(postData);
+        console.log(res);
+        if (res.data.status == 0) {
+          ElNotification({
+            type: "success",
+            title: res.data.msg,
+          });
+          unchangeableShip.value = true;
+        } else {
+          ElNotification({
+            type: "error",
+            title: res.data.msg,
+          });
+          console.log(res);
+        }
+        certs.value.disabled = true;
+
+        getUserDetail();
+      }
+    }
+    let certs = ref(null);
+
+    onMounted(() => {
+      getUserDetail();
+    });
+    return {
+      idFrontUploadSuccess,
+      idBackUploadSuccess,
+      getUserDetail,
+      userDetail,
+      shipDetail,
+      unchangeableShipOwner,
+      unchangeableShip,
+      idParams,
+      shipParams,
+      submitChange,
+      change,
+      cancelChange,
+      router,
+      idFrontFile,
+      idBackFile,
+      certs,
+    };
+  },
+};
+</script>
+<style scoped>
+:deep().el-upload-list__item-thumbnail {
+  object-fit: contain;
+}
+
+.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);
+}
+
+:deep().el-upload--picture-card {
+  border: none;
+  width: auto;
+}
+</style>

+ 271 - 0
src/views/shipOwnerManage/shipOwnerList.vue

@@ -0,0 +1,271 @@
+<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"
+        ></el-input>
+        <div class="seach-btn" @click="getShipOwnerList">查询</div>
+      </div>
+      <div class="cargo-owner-add" @click="dialogFormVisible = true">
+        添加船东
+      </div>
+      <el-dialog title="添加船东" v-model="dialogFormVisible">
+        <template v-slot:default>
+          <el-form
+            :model="ruleForm"
+            :rules="rules"
+            ref="form"
+            label-width="110px"
+            label-position="left"
+          >
+            <el-form-item prop="userName" label="船东姓名">
+              <el-input
+                style="width: 280px"
+                v-model="ruleForm.userName"
+              ></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="shipname" label="船名">
+              <el-input
+                style="width: 280px"
+                v-model="ruleForm.shipname"
+              ></el-input>
+            </el-form-item>
+            <el-form-item prop="mmsi" label="MMSI">
+              <el-input style="width: 280px" v-model="ruleForm.mmsi"></el-input>
+            </el-form-item>
+          </el-form>
+        </template>
+        <template v-slot:footer>
+          <div class="dialog-footer">
+            <el-button @click="resetForm">取 消</el-button>
+            <el-button type="primary" @click="addShipOwner(ruleForm)">
+              确 定
+            </el-button>
+          </div>
+        </template>
+      </el-dialog>
+    </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="userName"
+          label="船东名称"
+          min-width="120"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="userPhone"
+          label="手机号"
+          min-width="160"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="createTime"
+          label="入驻时间"
+          min-width="200"
+          align="center"
+        ></el-table-column>
+        <el-table-column label="操作" min-width="80" align="center">
+          <template v-slot="scope">
+            <el-button
+              @click="shipOwnerDetail(scope.row.userId, 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>
+  </div>
+</template>
+<script>
+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";
+
+export default {
+  setup() {
+    let dialogFormVisible = ref(false);
+    let form = ref(null);
+    let ruleForm = reactive({
+      ruleForm: {
+        userName: "",
+        phone: "",
+        shipname: "",
+        mmsi: "",
+      },
+    });
+    async function resetForm() {
+      dialogFormVisible.value = false;
+      form.value.resetFields();
+    }
+    const rules = reactive({
+      rules: {
+        userName: [
+          { required: true, message: "请填写船东名称", trigger: "blur" },
+        ],
+        shipname: [{ required: true, message: "请填写船名", trigger: "blur" }],
+        mmsi: [{ required: true, message: "请填写MMSI", trigger: "blur" }],
+        phone: [
+          { required: true, message: "请填写手机号", trigger: "blur" },
+          { min: 11, max: 11, message: "请正确填写手机号", trigger: "blur" },
+        ],
+      },
+    });
+    async function addShipOwner() {
+      form.value.validate(async (valid) => {
+        if (valid) {
+          let { userName, shipname, mmsi, phone } = ruleForm.ruleForm;
+          let res = await api.addShipOwner({
+            userName,
+            shipname,
+            mmsi,
+            phone,
+          });
+          console.log(res);
+          if (res.data.status == 0) {
+            ElNotification.success({
+              title: "添加成功",
+              duration: 0,
+              message: `${userName}:${res.data.msg}`,
+              type: "success",
+            });
+            resetForm();
+            getShipOwnerList();
+          } else {
+            ElNotification.error({
+              title: "失败",
+              duration: 3000,
+              message: res.data.msg,
+            });
+          }
+        } else {
+          return false;
+        }
+      });
+    }
+    let currentPage = ref(1);
+    let term = ref("");
+    let tableData = ref();
+    let total = ref();
+    async function getShipOwnerList() {
+      tableData.value = [];
+      let res = await api.getUserList({
+        identity: 1,
+        currentPage: currentPage.value,
+        size: 10,
+        term: term.value,
+      });
+      if (res.data.status == 0) {
+        tableData.value = res.data.result;
+        total.value = res.data.total;
+      }
+    }
+
+    async function shipOwnerDetail(userId) {
+      router.push({
+        path: "/shipOwnerManage/shipOwnerDetail",
+        query: {
+          userId,
+        },
+      });
+    }
+    function pageChange(e) {
+      currentPage.value = e;
+      getShipOwnerList();
+    }
+    getShipOwnerList();
+    onMounted(() => {});
+    return {
+      currentPage,
+      term,
+      tableData,
+      total,
+      getShipOwnerList,
+      shipOwnerDetail,
+      pageChange,
+      dialogFormVisible,
+      ...toRefs(ruleForm),
+      resetForm,
+      addShipOwner,
+      dialogFormVisible,
+      form,
+      ...toRefs(rules),
+    };
+  },
+};
+</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;
+}
+
+.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;
+}
+</style>

+ 193 - 0
src/views/toolManage/blockChain.vue

@@ -0,0 +1,193 @@
+<template>
+  <div class="full-container-p24">
+    <div style="display: flex; justify-content: space-between">
+      <div style="display: flex">
+        <el-input
+          placeholder="货主名称/船名/MMSI"
+          prefix-icon="el-icon-search"
+          v-model="term"
+          clearable
+          style="height: 32px; width: 330px; line-height: 32px"
+        ></el-input>
+        <div class="seach-btn" @click="getBlockChainList">查询</div>
+      </div>
+    </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="voyageName"
+          label="航次名称"
+          min-width="160"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="shipName"
+          label="船名"
+          min-width="160"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="shipMmsi"
+          label="MMSI"
+          min-width="120"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="loadPort"
+          label="装货港"
+          min-width="160"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="dischargeProt"
+          label="卸货港"
+          min-width="120"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="loadTons"
+          label="载货吨位(吨)"
+          min-width="160"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="onChainTime"
+          label="上链时间"
+          min-width="200"
+          align="center"
+        ></el-table-column>
+        <el-table-column label="操作" min-width="80" align="center">
+          <template v-slot="scope">
+            <span v-if="scope.row.hash">已上链</span>
+            <el-button
+              v-else
+              @click="upBlockChain(scope.row.voyageId, scope.$index)"
+              type="primary"
+              size="small"
+              :loading="upLoading && currentIndex == scope.$index"
+            >
+              上链
+            </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 { 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";
+
+let currentPage = ref(1);
+let term = ref("");
+let tableData = ref([]);
+let total = ref(0);
+async function getBlockChainList() {
+  tableData.value = [];
+  let res = await api.getBlockChainList({
+    currentPage: currentPage.value,
+    size: 10,
+    term: term.value,
+  });
+  if (res.data.status == 0) {
+    tableData.value = res.data.result;
+    total.value = res.data.total;
+  }
+}
+
+let upLoading = ref(false);
+let currentIndex = ref(-1);
+
+async function upBlockChain(voyageId, index) {
+  upLoading.value = true;
+  currentIndex.value = index;
+  let res = await api.upBlockChain({
+    voyageId,
+  });
+  if (res.data.status == 0) {
+    ElNotification.success({
+      title: "成功",
+      duration: 2000,
+      message: res.data.msg,
+    });
+    getBlockChainList();
+  } else {
+    ElNotification.error({
+      title: "失败",
+      duration: 2000,
+      message: res.data.msg,
+    });
+  }
+  upLoading.value = false;
+  currentIndex.value = -1;
+}
+function pageChange(e) {
+  currentPage.value = e;
+  getBlockChainList();
+}
+onMounted(() => {
+  getBlockChainList();
+});
+</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;
+}
+
+.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;
+}
+</style>

+ 343 - 0
src/views/toolManage/versions.vue

@@ -0,0 +1,343 @@
+<template>
+  <div class="mt10 df aic jcsb">
+    <div>
+      <el-button type="primary" class="mb20 mr30" @click="isModalVisable = true"
+        >添加版本记录</el-button
+      >
+      <el-popover placement="bottom" trigger="hover" :width="240">
+        <div style="width: 100%; height: 80vh; overflow-y: scroll">
+          <el-timeline>
+            <el-timeline-item
+              v-for="item in store.state.versions"
+              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-button> 新功能日志</el-button>
+          </el-badge>
+        </template>
+      </el-popover>
+    </div>
+    <div>
+      <!-- <el-button @click="">返回版本记录列表</el-button>
+      <el-button class="mr20 ml20" type="warning" @click="trash"
+        >回收站</el-button
+      > -->
+    </div>
+
+    <el-dialog
+      title="版本管理"
+      width="500px"
+      v-model="isModalVisable"
+      @closed="cancel"
+    >
+      <el-form
+        ref="ruleFormRef"
+        :model="ruleForm"
+        :rules="rules"
+        class="demo-ruleForm"
+        label-width="100px"
+        label-position="left"
+      >
+        <el-form-item label="版本号" prop="version">
+          <el-input style="width: 240px" v-model="ruleForm.version"></el-input>
+        </el-form-item>
+        <el-form-item label="发布时间" prop="timer">
+          <el-date-picker
+            style="width: 240px"
+            v-model="ruleForm.timer"
+            type="date"
+            placeholder="请选择日期"
+            format="YYYY/MM/DD"
+            value-format="YYYY/MM/DD"
+          >
+          </el-date-picker>
+        </el-form-item>
+        <hr class="mb20" />
+        <el-form-item
+          v-for="(item, index) in ruleForm.remarks"
+          :label="'发布内容 ' + parseInt(index + 1) + ' :'"
+          prop="remarks"
+        >
+          <div class="df aic jcsb">
+            <el-input
+              type="textarea"
+              style="width: 240px"
+              v-model="item.text"
+              autosize
+            ></el-input>
+            <el-button size="small" @click="deleteRemark(index)" type="danger"
+              >删除</el-button
+            >
+          </div>
+        </el-form-item>
+        <div class="df aic jcfe">
+          <el-button
+            size="small"
+            type="primary"
+            @click="ruleForm.remarks.push({})"
+            >添加一条记录</el-button
+          >
+        </div>
+        <hr class="mt20 mb20" />
+        <div class="df aic jcfe">
+          <el-button @click="cancel(ruleFormRef)">取消</el-button>
+          <el-button type="primary" @click="add(ruleFormRef)">提交</el-button>
+        </div>
+      </el-form>
+    </el-dialog>
+  </div>
+  <el-table :data="versions">
+    <el-table-column
+      type="index"
+      label="序号"
+      min-width="120"
+      align="center"
+    ></el-table-column>
+    <el-table-column
+      prop="version"
+      label="版本号"
+      min-width="100"
+      align="center"
+    ></el-table-column>
+    <el-table-column
+      prop="timer"
+      label="发布时间"
+      min-width="100"
+      align="center"
+    ></el-table-column>
+    <el-table-column label="版本内容" min-width="150" align="center">
+      <template v-slot="scope">
+        <div
+          style="width: 100%; text-align: left"
+          v-for="(item, index) in scope.row.remarks"
+        >
+          {{ index + 1 + ". " + item.text }}
+        </div>
+      </template>
+    </el-table-column>
+    <el-table-column label="状态" min-width="150" align="center">
+      <template v-slot="scope">
+        <el-switch
+          v-model="scope.row.disabled"
+          active-color="#13ce66"
+          :active-value="false"
+          :inactive-value="true"
+          inactive-color="#ff4949"
+          @change="changeSwitch($event, scope.row._id)"
+        />
+      </template>
+    </el-table-column>
+    <el-table-column label="操作" min-width="150" align="center">
+      <template v-slot="scope">
+        <el-button
+          @click="detail(scope.row, scope.$index)"
+          type="primary"
+          size="small"
+        >
+          修改
+        </el-button>
+        <el-button
+          @click="deleteItem(scope.row._id, scope.$index)"
+          type="danger"
+          size="small"
+        >
+          删除
+        </el-button>
+      </template>
+    </el-table-column>
+  </el-table>
+  <div style="width: 100%; text-align: right; padding: 43px 0">
+    <el-pagination
+      background
+      layout="prev, pager, next"
+      :total="total"
+      :page-size="pageSize"
+      @current-change="pageChange"
+    ></el-pagination>
+  </div>
+</template>
+
+<script setup>
+import { reactive, ref, onMounted } from "vue";
+import { AnonymousLogin, tcb } from "apis/cloudLogin";
+import _ from "lodash";
+import { ElMessage, ElMessageBox } from "element-plus";
+import store from "../../store";
+const db = tcb.database();
+const v = db.collection("huihenduo_versions");
+const __ = db.command;
+
+let versions = ref([]);
+let isModalVisable = ref(false);
+let ruleFormRef = ref(null);
+let currentPage = ref(1);
+let pageSize = ref(10);
+let currentVersionId = ref(null);
+let total = ref(0);
+
+function detail(item1) {
+  let item = _.cloneDeep(item1);
+  let { _id, version, timer, remarks } = item;
+  currentVersionId.value = _id;
+  ruleForm.value = { version, timer, remarks };
+
+  isModalVisable.value = true;
+}
+
+async function deleteItem(_id) {
+  ElMessageBox.confirm("确认删除版本日志?", "Warning", {
+    confirmButtonText: "删除",
+    cancelButtonText: "取消",
+    type: "warning",
+  })
+    .then(async () => {
+      let res = await v.doc(_id).update({
+        deleted: true,
+      });
+      getAllVersions();
+    })
+    .catch(() => {
+      ElMessage({
+        type: "info",
+        message: "取消删除",
+      });
+    });
+}
+
+async function changeSwitch(disabled, id) {
+  let res = await v.doc(id).update({
+    disabled,
+  });
+  getAllVersions();
+}
+
+function deleteRemark(index) {
+  ruleForm.value.remarks.splice(index, 1);
+}
+
+async function add() {
+  ruleFormRef.value.validate(async (valid) => {
+    if (valid) {
+      let res;
+      for (let i in ruleForm.value.remarks) {
+        if (!ruleForm.value.remarks[i].text) {
+          ruleForm.value.remarks.splice(i, 1);
+        }
+      }
+      if (currentVersionId.value) {
+        res = await v.doc(currentVersionId.value).update({
+          ...ruleForm.value,
+          updateTime: db.serverDate(),
+        });
+      } else {
+        res = await v.add({
+          ...ruleForm.value,
+          disabled: false,
+          createTime: db.serverDate(),
+        });
+      }
+
+      getAllVersions();
+      cancel();
+    } else {
+      console.log("未提交", ruleForm.value);
+    }
+  });
+}
+
+async function getAllVersions(listType) {
+  let res1 = await v.where({ deleted: __.neq(true) }).count();
+  total.value = res1.total;
+  let res = await v
+    .aggregate()
+    .match({ deleted: __.neq(true) })
+    .sort({
+      createTime: -1,
+    })
+    .skip((currentPage.value - 1) * pageSize.value)
+    .limit(pageSize.value)
+    .end();
+  versions.value = res.data;
+  getAbledVersions();
+}
+
+let abledVersions = ref([]);
+async function getAbledVersions() {
+  let res = await v
+    .aggregate()
+    .match({ deleted: __.neq(true) })
+    .sort({
+      createTime: -1,
+    })
+    .limit(10)
+    .end();
+  abledVersions.value = res.data;
+  store.commit("setVersions", abledVersions.value);
+}
+
+function cancel() {
+  isModalVisable.value = false;
+  ruleForm.value = {
+    version: "",
+    timer: "",
+    remarks: [{}, {}, {}],
+  };
+  currentVersionId.value = null;
+}
+
+let ruleForm = ref({
+  version: "",
+  timer: "",
+  remarks: [{}, {}, {}],
+});
+
+const rules = reactive({
+  version: [
+    {
+      required: true,
+      message: "请填写版本号",
+      trigger: "blur",
+    },
+  ],
+  timer: [
+    {
+      required: true,
+      message: "请填写发布时间",
+      trigger: "blur",
+    },
+  ],
+});
+async function init() {
+  await AnonymousLogin();
+  getAllVersions();
+}
+
+function pageChange(e) {
+  currentPage.value = e;
+  getAllVersions();
+}
+
+onMounted(() => {
+  init();
+});
+</script>
+
+<style scoped>
+</style>

+ 114 - 0
src/views/voyage/voyageAdd.vue

@@ -0,0 +1,114 @@
+<template>
+  <div class="line-container-p24">
+    <i class="el-icon-arrow-left"></i>
+    <div class="dib go-back ml8">返回航次列表</div>
+  </div>
+  <div id="map-container" class="map-container"></div>
+
+  <div class="line-container-p24">
+    <el-form
+      :rules="rules"
+      label-position="right"
+      label-width="80px"
+      :model="voyageForm"
+    >
+      <div class="line1 df" style="flex-wrap: wrap">
+        <el-form-item prop="voyageName" label="航次名称">
+          <el-input v-model="voyageForm.voyageName"></el-input>
+        </el-form-item>
+        <el-form-item label="货主">
+          <el-input v-model="voyageForm.cargoOwnerId"></el-input>
+        </el-form-item>
+        <el-form-item label="开始时间">
+          <el-input v-model="voyageForm.startTime"></el-input>
+        </el-form-item>
+        <el-form-item label="结束时间">
+          <el-input v-model="voyageForm.endTime"></el-input>
+        </el-form-item>
+        <el-form-item label="装货港">
+          <el-input v-model="voyageForm.loadPort"></el-input>
+        </el-form-item>
+        <el-form-item label="卸货港">
+          <el-input v-model="voyageForm.dischargPort"></el-input>
+        </el-form-item>
+        <el-form-item label="货种">
+          <el-input v-model="voyageForm.cargo"></el-input>
+        </el-form-item>
+        <el-form-item label="吨位">
+          <el-input v-model="voyageForm.tons"></el-input>
+        </el-form-item>
+      </div>
+    </el-form>
+  </div>
+</template>
+<script>
+import { onMounted, ref, toRefs, reactive } from "vue";
+export default {
+  setup() {
+    let map = ref();
+    const rules = reactive({
+      rules: {
+        voyageName: [{ required: true, message: "", trigger: "blur" }],
+        cargoOwnerId: [{ required: true, message: "", trigger: "blur" }],
+        startTime: [{ required: true, message: "", trigger: "blur" }],
+        endTime: [{ required: true, message: "", trigger: "blur" }],
+        loadPort: [{ required: true, message: "", trigger: "blur" }],
+        dischargPort: [{ required: true, message: "", trigger: "blur" }],
+        cargo: [{ required: true, message: "", trigger: "blur" }],
+        tons: [{ required: true, message: "", trigger: "blur" }],
+      },
+    });
+    let voyageForm = reactive({
+      voyageForm: {
+        voyageName: "",
+        cargoOwnerId: "",
+        startTime: "",
+        endTime: "",
+        loadPort: "",
+        dischargPort: "",
+        cargo: "",
+        tons: "",
+      },
+    });
+
+    function initMap() {
+      var center = new TMap.LatLng(31.228721, 121.524761);
+      //初始化地图
+      map.value = new TMap.Map("map-container", {
+        zoom: 12, //设置地图缩放级别
+        center: center, //设置地图中心点坐标
+      });
+    }
+
+    onMounted(() => {
+      initMap();
+    });
+
+    return {
+      ...toRefs(voyageForm),
+      ...toRefs(rules),
+    };
+  },
+};
+</script>
+<style scoped>
+.go-back {
+  font-size: 16px;
+  font-family: PingFangSC-Medium, PingFang SC;
+  font-weight: 500;
+  color: #333d43;
+  line-height: 100%;
+  cursor: pointer;
+}
+
+.map-container {
+  width: 100%;
+  height: 658px;
+  margin: 20px 0;
+}
+
+:deep() .el-form-item {
+  margin-right: 22px;
+  width: 280px;
+}
+</style>

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

@@ -0,0 +1,2849 @@
+<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>
+  </div>
+  <div class="line-container-p24">
+    <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="mapId" 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 v-if="voyage.voyageStatus != 2">
+        <div
+          class="info-line"
+          v-if="!insertDiscPortVisable"
+          style="margin-left: 140px"
+        >
+          <el-button type="primary" @click="insertDiscPortVisable = true"
+            >添加提前卸货港</el-button
+          >
+        </div>
+        <div class="info-line" v-else style="margin-left: 40px">
+          <div class="info-line-title" style="width: 80px">提前卸货港</div>
+          <el-autocomplete
+            class="info-line-text mr20"
+            v-model="toInsertDiscPortValue"
+            :fetch-suggestions="getCol"
+            @blur="clear('toInsertDiscPortId')"
+            placeholder="选择卸货港"
+            @select="selectToInsertDiscProt($event)"
+            style="width: 120px !important"
+          />
+          <el-button type="primary" @click="addNewPort">确认添加</el-button>
+          <el-button type="default" @click="cancelInsertDiscPort"
+            >取消添加</el-button
+          >
+        </div>
+      </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 class="container-second-title df aic jcsb mt40">
+      <div>船舶运输记录详情</div>
+      <div class="df aic">
+        <div v-if="voyage.voyageStatus == 1">
+          <el-button
+            class="mr20"
+            v-if="disabledStatus"
+            type="primary"
+            @click="changeVoyageInfo"
+          >
+            修改航次
+          </el-button>
+          <div v-else>
+            <div>
+              <el-button class="ml20" @click="cancelVoyageChange">
+                取消修改
+              </el-button>
+              <el-button
+                class="ml20 mr20"
+                type="primary"
+                @click="submitVoyageChange"
+              >
+                提交修改
+              </el-button>
+            </div>
+          </div>
+        </div>
+        <el-button
+          style="width: 160px"
+          type="primary"
+          @click="downloadExcel"
+          :loading="isLoadingExcel"
+        >
+          下载船舶跟踪表
+        </el-button>
+      </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.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:ss"
+          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 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 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 mt20"></div>
+
+    <div class="container-second-title df aic jcsb mt40">
+      <div>提单信息</div>
+      <div>
+        <el-button type="primary" @click="showAddLab()">
+          新增提单记录
+        </el-button>
+      </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="billingNum"
+        label="开单数量"
+        min-width="100"
+        align="center"
+      ></el-table-column>
+      <el-table-column
+        prop="billingDate"
+        label="开单日期"
+        min-width="120"
+        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"
+          >
+            {{ scope.row.file ? "查看" : "上传" }}
+          </el-button>
+        </template>
+      </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>
+          <el-button
+            @click="deleteLab(scope.row.id, scope.$index)"
+            type="danger"
+            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"
+      destroy-on-close
+    >
+      <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="开单日期"
+          ></el-date-picker>
+        </el-form-item>
+        <el-form-item label="开单数量">
+          <el-input
+            style="width: 240px"
+            v-model="labForm.billingNum"
+            placeholder="开单数量"
+          ></el-input>
+        </el-form-item>
+        <el-form-item label="提单">
+          <Uploader
+            :actionUrl="store.state.wayBillUrl"
+            :uploaderId="'labLoad'"
+            :params="labParams"
+            @onSendFileList="getLabBillList"
+            :fileList="labBillList"
+            uploadText="上传提单"
+            :limit="1"
+          ></Uploader>
+        </el-form-item>
+      </el-form>
+
+      <template #footer>
+        <el-button @click="cancelUploadLab">取消</el-button>
+        <el-button style="margin-left: 30px" type="primary" @click="addLab">
+          提交
+        </el-button>
+      </template>
+    </el-dialog>
+    <div class="hr mt20"></div>
+
+    <div class="container-second-title df aic jcsb">
+      <div>卸货记录</div>
+      <div>
+        <el-button
+          class="mr20"
+          type="primary"
+          @click="isAddPoundVisable = true"
+        >
+          新增卸货记录
+        </el-button>
+        <el-button
+          @click="exportDischargeExcel"
+          style="width: 160px"
+          type="primary"
+          :loading="isDischargeLoadingExcel"
+          >下载卸货信息</el-button
+        >
+      </div>
+    </div>
+    <el-table
+      style="width: 1200px"
+      :data="dischargeList"
+      stripe
+      :disabled="disabledStatus"
+    >
+      <el-table-column
+        type="index"
+        label="序号"
+        min-width="120"
+        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"
+          >
+            {{ scope.row.files ? "查看" : "上传" }}
+          </el-button>
+        </template>
+      </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"
+          >
+            修改
+          </el-button>
+          <el-button
+            @click="deleteDischarge(scope.row.id, scope.$index)"
+            type="danger"
+            size="small"
+          >
+            删除
+          </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>
+    <el-dialog v-model="isAddPoundVisable" destroy-on-close>
+      <el-form :model="formInline">
+        <el-form-item label="卸货时间">
+          <el-date-picker
+            class="info-line-text"
+            v-model="formInline.dischargeTime"
+            type="datetime"
+            format="YYYY/MM/DD HH:mm:ss"
+            value-format="YYYY/MM/DD HH:mm:ss"
+            placeholder="卸货时间"
+          ></el-date-picker>
+        </el-form-item>
+        <el-form-item label="卸货吨位">
+          <el-input
+            class="info-line-text"
+            v-model="formInline.dischargeTons"
+            placeholder="卸货吨位"
+          ></el-input>
+        </el-form-item>
+        <el-form-item label="卸货件数">
+          <el-input
+            class="info-line-text"
+            v-model="formInline.dischargePieces"
+            placeholder="卸货件数"
+          ></el-input>
+        </el-form-item>
+        <el-form-item label="卸货记录">
+          <Uploader
+            :actionUrl="store.state.wayBillUrl"
+            :uploaderId="'pound'"
+            :params="poundParams"
+            @onSendFileList="getPoundBillList"
+            :fileList="poundBillList"
+            uploadText="拖拽或点击上传磅单"
+          ></Uploader>
+        </el-form-item>
+      </el-form>
+      <div class="df aic jcfe mb30">
+        <el-button type="primary" @click="addDischarge"> 新增 </el-button>
+      </div>
+    </el-dialog>
+    <el-dialog
+      v-model="updateDischargeDialog"
+      title="修改记录"
+      width="700px"
+      center
+      destroy-on-close
+    >
+      <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:ss"
+            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="updateForm.dischargeTons"
+            placeholder="卸货吨位"
+          ></el-input>
+        </el-form-item>
+        <el-form-item label="上传磅单">
+          <Uploader
+            :actionUrl="store.state.wayBillUrl"
+            :uploaderId="'updatePound'"
+            :params="updatePoundParams"
+            @onSendFileList="getupdatePoundBillList"
+            :fileList="updatePoundBillList"
+            uploadText="拖拽或点击上传磅单"
+          ></Uploader>
+        </el-form-item>
+      </el-form>
+
+      <template #footer>
+        <el-button @click="cancelUpdateDischarge">取消</el-button>
+        <el-button
+          style="margin-left: 30px"
+          type="primary"
+          @click="updateDischarge"
+        >
+          提交
+        </el-button>
+      </template>
+    </el-dialog>
+    <el-dialog
+      v-model="dialogVisible"
+      title="图片预览"
+      width="30%"
+      destroy-on-close
+    >
+      <el-image
+        :src="dialogImageUrl"
+        style="height: 100%; width: 100%"
+      ></el-image>
+    </el-dialog>
+
+    <div class="hr m30-0"></div>
+    <div class="container-second-title df aic jcsb mt40">
+      <div>汽车装货记录详情</div>
+      <div>
+        <el-button type="primary" @click="showAddTruckRecord()">
+          新增汽车装货记录
+        </el-button>
+      </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"
+          >
+            {{ scope.row.file ? "查看" : "上传" }}
+          </el-button>
+        </template>
+      </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>
+          <el-button
+            @click="deleteTruckRecord(scope.row.id, scope.$index)"
+            type="danger"
+            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 class="hr mt20"></div>
+    <div class="df aic jcfe mt20" v-if="voyage.voyageStatus == 1">
+      <el-button type="primary" class="mr20" @click="cancelVoyage">
+        取消航次
+      </el-button>
+      <el-button
+        v-if="voyage.canComplete && disabledStatus"
+        type="primary"
+        @click="finishVoyage"
+      >
+        完成航次
+      </el-button>
+    </div>
+    <el-dialog
+      v-model="isAddTruckRecordVisable"
+      :title="truckRecordModalType"
+      width="780px"
+      center
+      @close="cancelUploadTruckRecord"
+      destroy-on-close
+    >
+      <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:ss"
+            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="store.state.wayBillUrl"
+            :uploaderId="'truckLoad'"
+            :params="truckLoadParams"
+            @onSendFileList="getTruckLoadBillList"
+            :fileList="truckRecordBillList"
+            uploadText="上传装货单"
+            :limit="1"
+          ></Uploader>
+        </el-form-item>
+      </el-form>
+
+      <template #footer>
+        <el-button @click="cancelUploadTruckRecord">取消</el-button>
+        <el-button
+          style="margin-left: 30px"
+          type="primary"
+          @click="addTruckLoadRecord"
+        >
+          提交
+        </el-button>
+      </template>
+    </el-dialog>
+  </div>
+  <div class="container-title">单据信息</div>
+  <div class="line-container-p24">
+    <div class="line">
+      <div class="info-line">
+        <div class="info-line-title">保险单</div>
+        <el-upload
+          drag
+          multiple
+          :action="store.state.wayBillUrl"
+          list-type="picture-card"
+          :on-preview="handlePictureCardPreview"
+          :on-remove="handleRemovePolicy"
+          :data="policyParams"
+          :on-success="policyUploadSuccess"
+          :file-list="policyFileList"
+        >
+          <div class="upload-plus-icon">+</div>
+          <div class="upload-text">拖拽或点击上传</div>
+        </el-upload>
+      </div>
+    </div>
+    <div class="line">
+      <div class="info-line">
+        <div class="info-line-title">运单</div>
+        <el-upload
+          drag
+          multiple
+          :action="store.state.wayBillUrl"
+          list-type="picture-card"
+          :on-preview="handlePictureCardPreview"
+          :on-remove="handleRemoveBill"
+          :data="billParams"
+          :on-success="billUploadSuccess"
+          :file-list="voyageBill"
+        >
+          <div class="upload-plus-icon">+</div>
+          <div class="upload-text">拖拽或点击上传</div>
+        </el-upload>
+      </div>
+    </div>
+  </div>
+  <!-- <div class="container-title">运单列表</div>
+  <div class="line-container-p24">
+    <div class="line">
+      <div class="info-line">
+        <div class="info-line-title">上传航次运单</div>
+        <el-upload
+          drag
+          multiple
+          :action="store.state.wayBillUrl"
+          list-type="picture-card"
+          :on-preview="handlePictureCardPreview"
+          :on-remove="handleRemoveBill"
+          :data="billParams"
+          :on-success="billUploadSuccess"
+          :file-list="voyageBill"
+        >
+          <div class="upload-plus-icon">+</div>
+          <div class="upload-text">拖拽或点击上传</div>
+        </el-upload>
+      </div>
+    </div>
+  </div> -->
+  <!-- <div class="container-title">磅单列表</div>
+  <div class="line-container-p24">
+    <div class="line" style="margin-bottom: 60px">
+      <div class="info-line">
+        <div class="info-line-title">上传磅单</div>
+        <el-upload
+          drag
+          multiple
+          :action="store.state.wayBillUrl"
+          list-type="picture-card"
+          :on-preview="handlePictureCardPreview"
+          :on-remove="handleRemovePoundBill"
+          :data="poundParams"
+          :on-success="poundBillUploadSuccess"
+          :file-list="poundBill"
+        >
+          <div class="upload-plus-icon">+</div>
+          <div class="upload-text">拖拽或点击上传</div>
+        </el-upload>
+      </div>
+    </div>
+  </div> -->
+  <!-- <div class="container-title">船舶证书</div>
+  <div class="line-container-p24">
+    <Certs ref="certs"></Certs>
+  </div> -->
+  <div class="container-title df aic jcsb">
+    <div @click="getVoyageDetail()">单据图片分配</div>
+    <div>
+      <el-checkbox
+        v-model="isCheckAll"
+        @change="checkAll"
+        :indeterminate="isIndeterminate"
+        size="large"
+        style="margin-right: 20px"
+        >全选</el-checkbox
+      >
+      <el-button
+        style="width: 220px; margin-right: 20px"
+        type="primary"
+        @click="showDistributeModal"
+      >
+        分配单据
+      </el-button>
+    </div>
+  </div>
+  <div class="line-container-p24">
+    <div v-show="shipownerUploadFiles.length" class="df aic fww">
+      <div
+        style="
+          width: 100px;
+          text-align: center;
+          margin-right: 20px;
+          margin-bottom: 20px;
+        "
+        v-for="item in shipownerUploadFiles"
+      >
+        <el-image
+          style="height: 100px; width: 100px"
+          fit="contain"
+          :src="item.viewUrl"
+          :preview-src-list="previewList"
+        ></el-image>
+        <div style="width: 100px; text-align: center; font-size: 12px">
+          {{ item.createTime }}
+        </div>
+        <div class="tac">
+          <el-checkbox
+            @change="checkItem"
+            v-model="item.checked"
+            size="large"
+          ></el-checkbox>
+        </div>
+      </div>
+    </div>
+    <div
+      v-show="!shipownerUploadFiles.length"
+      style="text-align: center; font-size: 24px; color: #ccc"
+    >
+      暂无单据图片
+    </div>
+  </div>
+  <el-dialog
+    title="单据分配"
+    v-model="isDistributeModalVisable"
+    @closed="distributeModalClose"
+    destroy-on-close
+    width="80%"
+  >
+    <h4 class="mb10">请选择卸货港:</h4>
+    <div class="df aic distribute-group">
+      <el-button
+        v-for="(item, index) in voyage.voyageDetails"
+        class="mr20"
+        :type="distributePortId == item.portId ? 'primary' : ''"
+        @click="distributePortId = item.portId"
+        >{{ item.portName }}</el-button
+      >
+    </div>
+    <div class="hr m10-0"></div>
+    <h4 class="mb10">请选择单据类型:</h4>
+    <div class="df aic distribute-group">
+      <el-button
+        class="mr20"
+        :type="distributeType == 1 ? 'primary' : ''"
+        @click="distributeType = 1"
+        >运单</el-button
+      >
+      <el-button
+        class="mr20"
+        :type="distributeType == 2 ? 'primary' : ''"
+        @click="distributeType = 2"
+        >磅单</el-button
+      >
+      <el-button
+        :type="distributeType == 4 ? 'primary' : ''"
+        @click="distributeType = 4"
+        >汽运装货单</el-button
+      >
+      <!-- <el-radio v-model="distributeType" label="1" size="large">运单</el-radio>
+      <el-radio v-model="distributeType" label="2" size="large">磅单</el-radio>
+      <el-radio v-model="distributeType" label="4" size="large"
+        >汽运装货单</el-radio
+      > -->
+    </div>
+    <div class="hr m10-0"></div>
+    <div class="df aic fww" style="max-height: 300px; overflow: scroll">
+      <div
+        style="
+          width: 100px;
+          text-align: center;
+          margin-right: 20px;
+          margin-bottom: 10px;
+          border: 2px solid #7598b1;
+        "
+        v-for="item in checkedImages"
+      >
+        <el-image
+          style="height: 100px; width: 100px"
+          fit="contain"
+          :src="item.viewUrl"
+          :preview-src-list="checkedPreviewList"
+        ></el-image>
+      </div>
+    </div>
+    <div class="hr m10-0"></div>
+    <div v-show="distributeType == 2">
+      <el-form inline :model="distributePoundForm">
+        <el-form-item label="卸货时间">
+          <el-date-picker
+            class="info-line-text"
+            v-model="distributePoundForm.dischargeTime"
+            type="datetime"
+            format="YYYY/MM/DD HH:mm:ss"
+            value-format="YYYY/MM/DD HH:mm:ss"
+            placeholder="卸货时间"
+          ></el-date-picker>
+        </el-form-item>
+        <el-form-item label="卸货吨位">
+          <el-input
+            class="info-line-text"
+            v-model="distributePoundForm.dischargeTons"
+            placeholder="卸货吨位"
+          ></el-input>
+        </el-form-item>
+        <el-form-item label="卸货件数">
+          <el-input
+            class="info-line-text"
+            v-model="distributePoundForm.dischargePieces"
+            placeholder="卸货件数"
+          ></el-input>
+        </el-form-item>
+      </el-form>
+    </div>
+    <div v-show="distributeType == 4">
+      <div class="tar">
+        <el-button @click="ocr" :loading="ocrLoading" type="primary">{{
+          ocrLoading ? "正在识别" : "开始识别"
+        }}</el-button>
+      </div>
+      <el-table :data="ocrTruckRecordTableData" stripe max-height="500">
+        <el-table-column label="港口名称" min-width="200" align="center">
+          <template v-slot="scope">
+            <el-input size="small" v-model="scope.row.portName"></el-input>
+          </template>
+        </el-table-column>
+        <!-- <el-table-column label="称重时间" min-width="200" align="center">
+          <template v-slot="scope">
+            <el-date-picker
+              size="small"
+              class="info-line-text"
+              v-model="scope.row.weighTime"
+              type="datetime"
+              format="YYYY/MM/DD HH:mm:ss"
+              value-format="YYYY/MM/DD HH:mm:ss"
+              placeholder="称重时间"
+            ></el-date-picker>
+          </template>
+        </el-table-column> -->
+        <el-table-column label="称重时间" min-width="120" align="center">
+          <template v-slot="scope">
+            <el-input
+              :input-style="
+                scope.row.weighTime ? {} : { border: '1px solid red' }
+              "
+              size="small"
+              v-model="scope.row.weighTime"
+            ></el-input>
+          </template>
+        </el-table-column>
+        <el-table-column label="车号" min-width="120" align="center">
+          <template v-slot="scope">
+            <el-input
+              :input-style="
+                scope.row.carNum?.length == 7 ? {} : { border: '1px solid red' }
+              "
+              size="small"
+              v-model="scope.row.carNum"
+            ></el-input>
+          </template>
+        </el-table-column>
+        <el-table-column label="货物名称" min-width="120" align="center">
+          <template v-slot="scope">
+            <el-input size="small" v-model="scope.row.cargoName"></el-input>
+          </template>
+        </el-table-column>
+        <el-table-column label="发货单位" min-width="200" align="center">
+          <template v-slot="scope">
+            <el-input size="small" v-model="scope.row.shippingUnit"></el-input>
+          </template>
+        </el-table-column>
+        <el-table-column label="收货单位" min-width="200" align="center">
+          <template v-slot="scope">
+            <el-input
+              :input-style="
+                scope.row.receivingUnit ? {} : { border: '1px solid red' }
+              "
+              size="small"
+              v-model="scope.row.receivingUnit"
+            ></el-input>
+          </template>
+        </el-table-column>
+        <el-table-column label="毛重" min-width="120" align="center">
+          <template v-slot="scope">
+            <el-input
+              :input-style="
+                scope.row.grossWeight ? {} : { border: '1px solid red' }
+              "
+              size="small"
+              v-model="scope.row.grossWeight"
+            ></el-input>
+          </template>
+        </el-table-column>
+        <el-table-column label="皮重" min-width="120" align="center">
+          <template v-slot="scope">
+            <el-input
+              :input-style="scope.row.tare ? {} : { border: '1px solid red' }"
+              size="small"
+              v-model="scope.row.tare"
+            ></el-input>
+          </template>
+        </el-table-column>
+        <el-table-column label="净重" min-width="120" align="center">
+          <template v-slot="scope">
+            <el-input
+              :input-style="
+                scope.row.netWeight ? {} : { border: '1px solid red' }
+              "
+              size="small"
+              v-model="scope.row.netWeight"
+            ></el-input>
+          </template>
+        </el-table-column>
+        <el-table-column label="货船名称" min-width="120" align="center">
+          <template v-slot="scope">
+            <el-input size="small" v-model="scope.row.shipName"></el-input>
+          </template>
+        </el-table-column>
+        <el-table-column label="司磅员" min-width="120" align="center">
+          <template v-slot="scope">
+            <el-input size="small" v-model="scope.row.weigher"></el-input>
+          </template>
+        </el-table-column>
+        <el-table-column label="查看" min-width="120" align="center">
+          <template v-slot="scope">
+            <el-image
+              style="height: 40px; width: 60px"
+              fit="contain"
+              :src="scope.row.viewUrl"
+              :preview-src-list="ocrImageList"
+            ></el-image
+          ></template>
+        </el-table-column>
+        <!-- <el-table-column label="操作" min-width="120" align="center">
+          <template v-slot="scope">
+            <el-button
+              size="small"
+              @click="deleteOcrRecord(scope.$index)"
+              type="danger"
+              >删除</el-button
+            >
+          </template>
+        </el-table-column> -->
+      </el-table>
+    </div>
+    <div class="hr m10-0"></div>
+    <div class="tar">
+      <el-button @click="submitDistribute" type="primary">确定</el-button>
+    </div>
+  </el-dialog>
+  <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 class="checkbox-group df aic jcsa mb10">
+              <el-checkbox
+                @change="auditMedia(item.id, 1, index, item.mediaType)"
+                :model-value="item.audit == 1"
+                label="通过"
+              ></el-checkbox>
+              <el-checkbox
+                @change="auditMedia(item.id, 2, index, item.mediaType)"
+                :model-value="item.audit == 2"
+                label="未通过"
+              ></el-checkbox>
+            </div>
+            <div class="checkbox-group df aic jcsa">
+              <el-checkbox
+                @change="markMedia(item.id, 1, index, item.mediaType)"
+                :model-value="item.type == 1"
+                label="验仓"
+              ></el-checkbox>
+              <el-checkbox
+                @change="markMedia(item.id, 2, index, item.mediaType)"
+                :model-value="item.type == 2"
+                label="清仓"
+              ></el-checkbox>
+            </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>
+        <el-dialog v-model="mediaModal" :title="modalTitle" destroy-on-close>
+          <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>
+      </div>
+    </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 shipAudits = ref([]);
+let shipownerUploadFiles = ref([]);
+async function getVoyageDetail(isInit) {
+  previewList.value = [];
+  policyFileList.value = [];
+  voyageBill.value = [];
+  previewSrcList.value = [];
+  shipownerUploadFiles.value = [];
+  let res = await api.getVoyageDetail({
+    type: localStorage.userType,
+    voyageId: route.query.id,
+  });
+  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;
+    shipownerUploadFiles.value = res.data.result.shipownerUploadFiles;
+    for (let i of shipownerUploadFiles.value || []) {
+      previewList.value.push(i.viewUrl);
+    }
+    for (let i of res.data.result.policys || []) {
+      policyFileList.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 arr = [];
+
+  if (poundBillList.value.length) {
+    for (let i of poundBillList.value) {
+      arr.push(i.response.result.id);
+    }
+  }
+  let res = await api.addDischarge({
+    ...formInline.value,
+    voyageFileIds: arr.join(","),
+    voyageId: route.query.id,
+    portId: currentDiscPortId.value,
+  });
+  if (res.data.status == 0) {
+    getDischargeList(1);
+    poundBillList.value = [];
+    formInline.value = {
+      dischargeTons: 0,
+      dischargePieces: 0,
+    };
+  } else {
+    console.log(res);
+  }
+  isAddPoundVisable.value = false;
+}
+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({
+  dischargeTons: 0,
+  dischargePieces: 0,
+});
+async function getDischargeList() {
+  let res = await api.getDischargeList({
+    voyageId: route.query.id,
+    portId: currentDiscPortId.value,
+    currentPage: dischargeCurrentPage.value,
+    size: 10,
+  });
+  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);
+async function updateDischarge() {
+  let postData = {
+    ...updateForm.value,
+  };
+  let arr = [];
+  if (updatePoundBillList.value.length) {
+    for (let i of updatePoundBillList.value) {
+      arr.push(i.id || i.response.result.id);
+    }
+    postData.voyageFileIds = arr.join(",");
+  } else {
+    postData.voyageFileIds = "";
+  }
+  let res = await api.updateDischarge({
+    ...postData,
+  });
+  if (res.data.status == 0) {
+    getDischargeList();
+    cancelUpdateDischarge();
+    ElNotification({
+      type: "success",
+      title: res.data.msg,
+    });
+  } else {
+    console.log(res);
+    ElNotification({
+      type: "error",
+      title: res.data.msg,
+    });
+  }
+}
+function showUpdateDischarge(item, index) {
+  updateDischargeDialog.value = true;
+  updatePoundBillList.value = [];
+
+  let { id, dischargeTons, dischargeTime, voyageId, files } = item;
+  if (files && files.length) {
+    for (let i of files)
+      updatePoundBillList.value.push({ id: i.id, url: i.viewUrl });
+  }
+
+  updateForm.value = {
+    id,
+    dischargeTons,
+    dischargeTime,
+  };
+}
+function cancelUpdateDischarge() {
+  updateDischargeDialog.value = false;
+  updateForm.value = {};
+  updatePoundBillList.value = [];
+}
+let mapId = ref(`map${route.query.id}`);
+function initMap() {
+  let c;
+  let longitude = 121.524761;
+  let latitude = 31.228721;
+  if (medias.value.length) {
+    c = Math.floor(medias.value.length / 2);
+    longitude = medias.value[c].longitude;
+    latitude = medias.value[c].latitude;
+  }
+  map.value = new AMap.Map(mapId.value, {
+    zoom: 16, //级别
+    center: [longitude, latitude], //中心点坐标
+    mapStyle: "amap://styles/f48d96805f5fa7f5aada657c5ee37017",
+  });
+  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);
+  });
+}
+
+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 res = await api.updateVoyage({
+    ...voyage.value,
+    hasInsurance: Number(voyage.value.hasInsurance),
+  });
+  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);
+  }
+  getVoyageDetail();
+}
+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.canComplete) return;
+  let res = await api.finishVoyage({
+    voyageId: route.query.id,
+  });
+
+  if (res.data.status == 0) {
+    ElNotification({
+      type: "success",
+      title: "航次已完成",
+    });
+  } else {
+    ElNotification({
+      type: "error",
+      title: res.data.msg,
+    });
+    console.log(res);
+  }
+  getVoyageDetail();
+}
+let currentUrl = ref("");
+let mediaModal = ref(false);
+let modalType = ref(1);
+let modalTitle = ref("");
+let modalPreview = ref([]);
+function openMediaModal(url, type, title, item = {}) {
+  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);
+  }
+}
+
+async function markMedia(mediaId, type, index, mediaType) {
+  console.log(mediaId, type, index, mediaType);
+  let res = await api.markMedia({
+    mediaId,
+    type,
+  });
+  if (res.data.status == 0) {
+    medias.value[index].type = type;
+    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 handleRemovePolicy(file, list) {
+  let cache = _.cloneDeep(policyFileList.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",
+        });
+        policyFileList.value = list;
+      }
+    })
+    .catch(() => {
+      policyFileList.value = cache;
+      ElMessage({
+        type: "info",
+        message: "取消删除",
+      });
+    });
+}
+let policyFileList = ref([]);
+
+function policyUploadSuccess(response, file, list) {
+  list[list.length - 1] = {
+    ...response.result,
+    url: response.result.viewUrl,
+  };
+  console.log(list);
+  policyFileList.value = list;
+}
+
+let policyParams = ref({
+  voyageId: route.query.id,
+  type: 3,
+});
+
+async function handleRemovePoundBill(file, list) {
+  let cache = _.cloneDeep(poundBill.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",
+        });
+        poundBill.value = list;
+      }
+    })
+    .catch(() => {
+      poundBill.value = cache;
+      ElMessage({
+        type: "info",
+        message: "取消删除",
+      });
+    });
+}
+
+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,
+  type: 1,
+});
+
+function getPoundBillList(list) {
+  console.log(list);
+  poundBillList.value = list;
+}
+let poundBillList = ref([]);
+
+let poundBill = ref([]);
+
+function poundBillUploadSuccess(response, file, list) {
+  list[list.length - 1] = {
+    ...response.result,
+    url: response.result.viewUrl,
+  };
+  console.log(list);
+  poundBill.value = list;
+}
+function previewPoundBill(url) {
+  console.log(url);
+}
+
+let updatePoundParams = ref({
+  voyageId: route.query.id,
+  type: 2,
+});
+let updatePoundBillList = ref([]);
+function getupdatePoundBillList(list) {
+  console.log(list);
+  updatePoundBillList.value = list;
+}
+
+let previewPoundList = ref([]);
+
+let poundParams = ref({
+  voyageId: route.query.id,
+  type: 2,
+});
+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"
+  );
+  if (res) {
+    isLoadingExcel.value = false;
+  }
+}
+
+async function cancelVoyage() {
+  console.log("取消航次");
+  ElMessageBox.confirm("确认取消航次?", "确认操作", {
+    confirmButtonText: "确认",
+    cancelButtonText: "取消操作",
+    type: "warning",
+  })
+    .then(async (e) => {
+      console.log(e);
+      let res = await api.cancelVoyage({
+        id: route.query.id,
+      });
+      if (res.data.status == 0) {
+        ElMessage({
+          type: "success",
+          message: "航次取消成功!",
+        });
+        router.push("/voyageList");
+      } else {
+        console.log(res);
+      }
+    })
+    .catch((e) => {
+      console.log(e);
+      ElMessage({
+        type: "info",
+        message: "取消操作",
+      });
+    });
+}
+let certs = ref(null);
+let isCertsVisable = ref(false);
+
+function showCerts() {
+  setTimeout(() => {
+    certs.value.initCerts(shipAudits.value);
+  });
+}
+let isDischargeLoadingExcel = ref(false);
+async function exportDischargeExcel() {
+  isDischargeLoadingExcel.value = true;
+  let res = await downloadBlobFile(
+    `${url.baseurl}/voyage/exportDischargeExcel`,
+    { voyageId: route.query.id },
+    "卸货记录表",
+    "post"
+  );
+  if (res) {
+    isDischargeLoadingExcel.value = false;
+  }
+}
+
+let isAddPoundVisable = ref(false);
+
+let isAddTruckRecordVisable = ref(false);
+let truckTableData = ref([]);
+let truckRecordForm = ref({});
+let truckRecordBillList = ref([]);
+let truckCurrentPage = ref(1);
+let truckTotal = ref(0);
+let truckLoadParams = ref({
+  voyageId: route.query.id,
+  type: 4,
+});
+function getTruckLoadBillList(list) {
+  truckRecordBillList.value = list;
+}
+function cancelUploadTruckRecord() {
+  isAddTruckRecordVisable.value = false;
+  truckRecordForm.value = {};
+  truckRecordBillList.value = [];
+  currentRecordId.value = -1;
+}
+let currentRecordId = ref(-1);
+async function addTruckLoadRecord() {
+  let postData = {};
+  if (truckRecordBillList.value.length) {
+    if (truckRecordBillList.value[0].viewUrl) {
+      let { fileKey, viewUrl, downloadUrl } = truckRecordBillList.value[0];
+      postData = {
+        fileKey,
+        viewUrl,
+        downloadUrl,
+      };
+    } else {
+      let { fileKey, viewUrl, downloadUrl } =
+        truckRecordBillList.value[0].response.result;
+      postData = {
+        fileKey,
+        viewUrl,
+        downloadUrl,
+      };
+    }
+  } else {
+    postData = {
+      fileKey: "",
+      viewUrl: "",
+      downloadUrl: "",
+    };
+  }
+  if (currentRecordId.value != -1) {
+    postData.recordId = currentRecordId.value;
+    delete truckRecordForm.value.file;
+    delete truckRecordForm.value.fileId;
+  }
+  let res = await api[
+    `${
+      truckRecordModalType.value == "新增记录"
+        ? "addTruckLoadRecord"
+        : "updateTruckLoadRecord"
+    }`
+  ]({
+    voyageId: route.query.id,
+    portId: currentDiscPortId.value,
+    ...postData,
+    ...truckRecordForm.value,
+  });
+  cancelUploadTruckRecord();
+  getTruckLoadRecord();
+}
+function showAddTruckRecord() {
+  isAddTruckRecordVisable.value = true;
+  truckRecordModalType.value = "新增记录";
+}
+
+async function getTruckLoadRecord() {
+  let res = await api.getTruckLoadRecord({
+    voyageId: route.query.id,
+    portId: currentDiscPortId.value,
+    currentPage: truckCurrentPage.value,
+    size: 10,
+  });
+  truckTableData.value = res.data.result;
+  truckTotal.value = res.data.total;
+}
+function truckPageChange(e) {
+  truckCurrentPage.value = e;
+  getTruckLoadRecord();
+}
+
+let truckRecordModalType = ref("");
+function showTruckRecord(item, index, text) {
+  isAddTruckRecordVisable.value = true;
+  currentRecordId.value = item.id;
+  truckRecordModalType.value = text;
+  if (item.file) {
+    truckRecordBillList.value[0] = {
+      ...item.file,
+      url: item.file.viewUrl,
+    };
+  }
+
+  truckRecordForm.value = { ...item };
+}
+async function deleteTruckRecord(id, index) {
+  console.log(id);
+  ElMessageBox.confirm("确认删除装货记录?", "Warning", {
+    confirmButtonText: "删除",
+    cancelButtonText: "取消",
+    type: "warning",
+  })
+    .then(async () => {
+      let res = await api.deleteTruckLoadRecord({
+        id,
+      });
+      if (res.data.status == 0) {
+        ElMessage({
+          message: "删除成功!",
+          type: "success",
+        });
+      }
+      getTruckLoadRecord();
+    })
+    .catch(() => {
+      ElMessage({
+        type: "info",
+        message: "取消删除",
+      });
+    });
+}
+
+let isDistributeModalVisable = ref(false);
+let previewList = ref([]);
+let checkedImages = ref([]);
+let distributeType = ref(-1);
+let distributePortId = ref(-1);
+let checkedPreviewList = ref([]);
+let recordIds = ref("");
+let ocrTruckRecordTableData = ref([]);
+function showDistributeModal() {
+  checkedImages.value = shipownerUploadFiles.value.filter((e) => e.checked);
+
+  if (!checkedImages.value.length) {
+    ElMessage({
+      type: "error",
+      message: "请选择要分配的图片!",
+    });
+    return;
+  }
+  let arr0 = [];
+  for (let i of checkedImages.value) {
+    checkedPreviewList.value.push(i.viewUrl);
+    arr0.push(i.id);
+  }
+  recordIds.value = arr0.join(",");
+  isDistributeModalVisable.value = true;
+}
+
+function distributeModalClose() {
+  isDistributeModalVisable.value = false;
+  distributeType.value = -1;
+  distributePortId.value = -1;
+  distributePoundForm.value = {
+    dischargeTime: "",
+    dischargeTons: "",
+    dischargePieces: "",
+  };
+  ocrTruckRecordTableData.value = [];
+}
+
+async function submitDistribute() {
+  if (distributePortId.value == -1) {
+    ElMessage({
+      type: "error",
+      message: "请选择要分配的港口!",
+    });
+    return;
+  }
+  if (distributeType.value == -1) {
+    ElMessage({
+      type: "error",
+      message: "请选择要分配类型!",
+    });
+    return;
+  }
+  let postData = {};
+  let type = distributeType.value;
+  if (2 == type) {
+    postData = {
+      poundBillData: {
+        ...distributePoundForm.value,
+        portId: distributePortId.value,
+      },
+    };
+  } else if (4 == type) {
+    for (let i of ocrTruckRecordTableData.value) {
+      i.portId = distributePortId.value;
+    }
+    postData = { carLoadDatas: ocrTruckRecordTableData.value };
+  }
+  let res = await api.distribute({
+    ...postData,
+    voyageId: route.query.id,
+    recordIds: recordIds.value,
+    type,
+  });
+  console.log(res);
+  distributeModalClose();
+  getVoyageDetail();
+  if (2 == type) getDischargeList();
+  if (4 == type) getTruckLoadRecord();
+}
+
+async function deleteOcrRecord(index) {
+  console.log(index);
+  ocrTruckRecordTableData.value.splice(index, 1);
+}
+
+let distributePoundForm = ref({});
+async function ocr() {
+  ocrLoading.value = true;
+  ocrImageList.value = [];
+  let res = await api.ocr({
+    recordIds: recordIds.value,
+  });
+  ocrLoading.value = false;
+  ocrTruckRecordTableData.value = res.data.result;
+  for (let i of ocrTruckRecordTableData.value) {
+    if (i.weighTime) i.weighTime = i.weighTime.substring(0, 10);
+    ocrImageList.value.push(i.viewUrl);
+  }
+}
+let isCheckAll = ref(false);
+let isIndeterminate = ref(false);
+function checkAll(b) {
+  isIndeterminate.value = false;
+  shipownerUploadFiles.value.forEach((e) => {
+    e.checked = b;
+  });
+}
+function checkItem() {
+  let arr = shipownerUploadFiles.value.filter((e) => e.checked);
+  if (arr.length == shipownerUploadFiles.value.length) {
+    isCheckAll.value = true;
+    isIndeterminate.value = false;
+  } else if (arr.length == 0) {
+    isCheckAll.value = false;
+    isIndeterminate.value = false;
+  } else {
+    isIndeterminate.value = true;
+  }
+}
+
+let ocrLoading = ref(false);
+let ocrImageList = ref([]);
+let blockchainInfo = ref("");
+
+let isAddLabVisable = ref(false);
+let labTableData = ref([]);
+let labForm = ref({});
+let labBillList = ref([]);
+let labCurrentPage = ref(1);
+let labTotal = ref(0);
+let labParams = ref({
+  voyageId: route.query.id,
+  type: 5,
+});
+function getLabBillList(list) {
+  labBillList.value = list;
+}
+function cancelUploadLab() {
+  isAddLabVisable.value = false;
+  labForm.value = {};
+  labBillList.value = [];
+  currentLabId.value = -1;
+}
+let currentLabId = ref(-1);
+async function addLab() {
+  let postData = {
+    portId: currentDiscPortId.value,
+  };
+  if (labBillList.value.length) {
+    if (labBillList.value[0].viewUrl) {
+      let { fileKey, viewUrl, downloadUrl } = labBillList.value[0];
+      postData = {
+        ...postData,
+        fileKey,
+        viewUrl,
+        downloadUrl,
+      };
+    } else {
+      let { fileKey, viewUrl, downloadUrl } =
+        labBillList.value[0].response.result;
+      postData = {
+        ...postData,
+        fileKey,
+        viewUrl,
+        downloadUrl,
+      };
+    }
+  } else {
+    postData = {
+      ...postData,
+      fileKey: "",
+      viewUrl: "",
+      downloadUrl: "",
+    };
+  }
+  if (currentLabId.value != -1) {
+    postData.labId = currentLabId.value;
+    delete postData.portId;
+    delete labForm.value.file;
+    delete labForm.value.fileId;
+  }
+  let res = await api[
+    `${labModalType.value == "新增记录" ? "addLab" : "updateLab"}`
+  ]({
+    voyageId: route.query.id,
+    ...postData,
+    ...labForm.value,
+  });
+  cancelUploadLab();
+  getLabList();
+}
+function showAddLab() {
+  isAddLabVisable.value = true;
+  labModalType.value = "新增记录";
+}
+
+async function getLabList() {
+  let res = await api.getLabList({
+    voyageId: route.query.id,
+    portId: currentDiscPortId.value,
+    currentPage: labCurrentPage.value,
+    size: 10,
+  });
+  labTableData.value = res.data.result;
+  labTotal.value = res.data.total;
+}
+function labPageChange(e) {
+  labCurrentPage.value = e;
+  getLabList();
+}
+
+let labModalType = ref("");
+function showLab(item, index, text) {
+  isAddLabVisable.value = true;
+  currentLabId.value = item.id;
+  labModalType.value = text;
+  if (item.file) {
+    labBillList.value[0] = {
+      ...item.file,
+      url: item.file.viewUrl,
+    };
+  }
+
+  labForm.value = { ...item };
+}
+async function deleteLab(id, index) {
+  console.log(id);
+  ElMessageBox.confirm("确认删除装货记录?", "Warning", {
+    confirmButtonText: "删除",
+    cancelButtonText: "取消",
+    type: "warning",
+  })
+    .then(async () => {
+      let res = await api.deleteLab({
+        id,
+      });
+      if (res.data.status == 0) {
+        ElMessage({
+          message: "删除成功!",
+          type: "success",
+        });
+      }
+      getLabList();
+    })
+    .catch(() => {
+      ElMessage({
+        type: "info",
+        message: "取消删除",
+      });
+    });
+}
+
+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,
+    currentPage: weatherCurrentPage.value,
+    size: 10,
+  });
+  weatherTableData.value = res.data.result;
+  weatherTotal.value = res.data.total;
+}
+
+function weatherPageChange(e) {
+  weatherCurrentPage.value = e;
+  getPortWeatherList();
+}
+
+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();
+}
+
+let toInsertDiscPortId = ref("");
+let toInsertDiscPortValue = ref("");
+let getCol = _.debounce(
+  async (queryString, cb) => {
+    if (!queryString) return;
+    let res = await api.getCol({
+      term: queryString,
+    });
+    if (res.data.status == 0) {
+      cb(res.data.result);
+    }
+  },
+  1000,
+  { leading: true }
+);
+function clear(type) {
+  toInsertDiscPortId.value = "";
+  toInsertDiscPortValue.value = "";
+}
+let selectToInsertDiscProt = (item, index) => {
+  console.log(item);
+  toInsertDiscPortId.value = item.key;
+  toInsertDiscPortValue.value = item.value;
+};
+let insertDiscPortVisable = ref(false);
+async function addNewPort() {
+  if (!toInsertDiscPortId.value || !toInsertDiscPortValue.value) {
+    ElMessage({
+      message: "请选择提前卸货港!",
+      type: "warning",
+    });
+    return;
+  }
+  let res = await api.addNewPort({
+    voyageId: route.query.id,
+    portId: toInsertDiscPortId.value,
+  });
+  if (res.data.status == 0) {
+    ElMessage({
+      message: "添加提前卸货港成功!",
+      type: "success",
+    });
+    getVoyageDetail(true);
+    cancelInsertDiscPort();
+  } else {
+    ElMessage({
+      message: res.data.msg,
+      type: "error",
+    });
+  }
+}
+
+function cancelInsertDiscPort() {
+  insertDiscPortVisable.value = false;
+  toInsertDiscPortId.value = "";
+  toInsertDiscPortValue.value = "";
+}
+
+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;
+}
+
+.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: 255px;
+  width: 180px;
+  border: 5px solid #dddddd;
+  transition: all 0.5s;
+  background: #fff;
+  z-index: 10;
+}
+
+.point {
+  position: relative;
+  left: 93px;
+  top: 277px;
+  width: 16px;
+  height: 16px;
+  background-image: url(../../assets/blue-circle.png);
+}
+
+.s-line {
+  position: absolute;
+  left: 100px;
+  top: 261px;
+  height: 20px;
+  border-left: 2px dashed;
+  box-sizing: border-box;
+  border-color: #ddd;
+}
+
+.l-line {
+  position: relative;
+  bottom: 30px;
+  left: 111px;
+  top: 268px;
+  height: 3px;
+  width: 100px;
+  background-color: #dddddd;
+}
+
+.bottom-box {
+  top: 309px;
+}
+.top210px {
+  top: 289px;
+}
+
+.box:hover {
+  transform: scale(1.15);
+}
+
+.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;
+}
+
+.el-checkbox {
+  height: 20px;
+  margin-top: 15px;
+  margin-right: 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);
+}
+.el-checkbox {
+  margin-top: 0;
+}
+
+:deep().el-upload-dragger {
+  width: 100%;
+  height: 148px !important;
+}
+
+.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>

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

@@ -0,0 +1,937 @@
+<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: 330px"
+        ></el-input>
+        <div class="search-btn" @click="getVoyageList()">查询</div>
+      </div>
+      <div class="df aic">
+        <el-button
+          @click="FYDIModalVisable = true"
+          class="mr20"
+          size="medium"
+          type="primary"
+          >上传FYDI指数</el-button
+        >
+
+        <el-dialog
+          title="上传FYDI指数"
+          v-model="FYDIModalVisable"
+          @close="FYDIModalClose"
+        >
+          <template v-slot:default>
+            <div class="df aic jcsa">
+              <RemoteSearch
+                api="getUserSelect"
+                v-model="cargoOwnerCompanyStr"
+                placeholder="公司名称/联系人/手机号"
+                :params="{
+                  identity: 2,
+                }"
+                @selectItem="selectCargoOwnerUpload($event)"
+                class="mb10"
+              ></RemoteSearch>
+              <el-upload
+                v-if="FYDIParams.cargoOwnerId"
+                class="upload-demo"
+                :action="this.$store.state.fydi"
+                :show-file-list="false"
+                :data="FYDIParams"
+                :on-success="upFYDISuccess"
+                :before-upload="beforeFYDI"
+              >
+                <el-button
+                  class="mr20"
+                  size="medium"
+                  type="primary"
+                  :loading="isUpLoading"
+                  >上传FYDI指数</el-button
+                >
+              </el-upload>
+            </div>
+          </template>
+        </el-dialog>
+        <el-button
+          class="mr20"
+          size="medium"
+          type="primary"
+          @click="voyageAddDialogVisible = true"
+          >添加航次</el-button
+        >
+      </div>
+    </div>
+    <el-dialog
+      v-model="voyageAddDialogVisible"
+      @closed="resetAddVoyageForm"
+      title="添加航次"
+    >
+      <el-form
+        :rules="rules"
+        label-position="right"
+        label-width="100px"
+        ref="voyageFormRef"
+        :model="voyageForm"
+      >
+        <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></el-form-item>
+          <el-form-item
+            v-for="(item, index) in discPorts"
+            prop="dischargeProt"
+            :label="'第 ' + (index + 1) + ' 卸货港'"
+          >
+            <el-autocomplete
+              v-model="item.loadPort"
+              :fetch-suggestions="getCol"
+              @blur="clear('dischargeProt')"
+              placeholder="选择卸货港"
+              @select="selectDischargeProt($event, index)"
+            />
+          </el-form-item>
+          <el-form-item>
+            <el-button type="primary" size="small" @click="addDiscPort"
+              >添加卸货港</el-button
+            ></el-form-item
+          >
+          <el-form-item v-if="discPorts.length % 2 == 0"></el-form-item>
+
+          <el-form-item prop="cargo" label="货种">
+            <el-input
+              style="width: 220px"
+              v-model="voyageForm.cargo"
+            ></el-input>
+          </el-form-item>
+          <el-form-item prop="tons" label="吨位">
+            <el-input style="width: 220px" v-model="voyageForm.tons"></el-input>
+          </el-form-item>
+          <el-form-item prop="pieces" label="件数">
+            <el-input
+              style="width: 220px"
+              v-model="voyageForm.pieces"
+            ></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>
+    <el-table
+      :data="tableData"
+      stripe
+      style="width: 100%; margin-top: 24px"
+      :row-style="rowStyle"
+    >
+      <!-- <el-table-column
+        type="index"
+        label="序号"
+        min-width="80"
+        align="center"
+      ></el-table-column> -->
+      <el-table-column
+        prop="voyageName"
+        label="航次名称"
+        min-width="140"
+        align="center"
+      ></el-table-column>
+      <el-table-column
+        prop="loadPort"
+        label="装货港"
+        min-width="90"
+        align="center"
+      ></el-table-column>
+      <el-table-column
+        prop="dischargePort"
+        label="卸货港"
+        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="预计到港时间"
+        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="航次状态"
+        min-width="80"
+        align="center"
+      >
+        <template v-slot="scope">
+          {{ scope.row.abnormalStatus == 0 ? "正常" : "异常" }}
+        </template>
+      </el-table-column>
+      <el-table-column
+        prop="daysInPortStr"
+        label="在港天数"
+        sortable
+        min-width="100"
+        align="center"
+      ></el-table-column>
+      <el-table-column
+        prop="todayPhotoCount"
+        label="今日照片"
+        min-width="70"
+        align="center"
+      ></el-table-column>
+      <el-table-column
+        prop="cargo"
+        label="货种"
+        min-width="70"
+        align="center"
+      ></el-table-column>
+      <el-table-column
+        prop="actualLoadTons"
+        label="装载吨位"
+        min-width="80"
+        align="center"
+      ></el-table-column>
+      <el-table-column
+        prop="unloadedtons"
+        label="已卸货吨位"
+        min-width="80"
+        align="center"
+      ></el-table-column>
+      <el-table-column
+        prop="remainTons"
+        label="剩余吨位"
+        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="保险状态"
+        min-width="70"
+        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"
+      ></el-table-column>
+      <el-table-column
+        prop="remark"
+        label="备注"
+        min-width="100"
+        align="center"
+      ></el-table-column> -->
+      <el-table-column label="操作" min-width="80" align="center">
+        <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"
+        :page-size="pageSize"
+        @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";
+
+let currentPage = ref(1);
+let pageSize = ref(20);
+let term = ref("");
+let tableData = ref([]);
+let total = ref(0);
+let status = ref(0);
+async function getVoyageList() {
+  tableData.value = [];
+
+  let res = await api.getVoyageList({
+    cargoOwnerId: 0,
+    shipId: 0,
+    status: status.value,
+    term: term.value,
+    currentPage: currentPage.value,
+    size: pageSize.value,
+  });
+  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 = ref({
+  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: false, message: "请填写吨位", trigger: "blur" }],
+  pieces: [{ required: false, message: "请填写件数", trigger: "blur" }],
+});
+let voyageForm = ref({
+  voyageName: "",
+  cargoOwnerId: "",
+  cargoOwnerName: "",
+  startTime: "",
+  endTime: "",
+  loadPort: "",
+  dischargeProt: "",
+  cargo: "",
+  tons: 0,
+  loadPortId: "",
+  dischargeProtId: "",
+  shipId: "",
+  shipName: "",
+  pieces: 0,
+});
+
+function clear(type) {
+  setTimeout(() => {
+    switch (type) {
+      case "shipId": {
+        let index = ref(-1);
+        for (let i in shipsCache.value) {
+          if (voyageForm.value.shipName == shipsCache.value[i].shipName) {
+            index.value = i;
+            break;
+          }
+        }
+        if (index.value != -1) {
+          voyageForm.value.shipId = shipsCache.value[index.value].shipId;
+        } else {
+          let b = shipsCache.value.some((item) => {
+            return (
+              item.shipId == voyageForm.value.shipId &&
+              item.shipName == voyageForm.value.shipName
+            );
+          });
+          voyageForm.value["shipId"] = "";
+          voyageForm.value["shipName"] = "";
+        }
+        break;
+      }
+      case "cargoOwnerId": {
+        let index = ref(-1);
+        for (let i in cargoOwnersCache.value) {
+          if (
+            voyageForm.value.cargoOwnerName ==
+            cargoOwnersCache.value[i].userName
+          ) {
+            index.value = i;
+            break;
+          }
+        }
+        if (index.value != -1) {
+          voyageForm.value.cargoOwnerId =
+            cargoOwnersCache.value[index.value].userId;
+        } else {
+          let b = cargoOwnersCache.value.some((item) => {
+            return (
+              item.userId == voyageForm.value.cargoOwnerId &&
+              item.userName == voyageForm.value.cargoOwnerName
+            );
+          });
+          if (!b) {
+            voyageForm.value["cargoOwnerId"] = "";
+            voyageForm.value["cargoOwnerName"] = "";
+          }
+        }
+
+        break;
+      }
+      case "loadPort": {
+        let index = ref(-1);
+        for (let i in colCache.value) {
+          if (voyageForm.value.loadPort == colCache.value[i].value) {
+            index.value = i;
+            break;
+          }
+        }
+        if (index.value != -1) {
+          voyageForm.value.loadPortId = colCache.value[index.value].key;
+        } else {
+          let b = colCache.value.some((item) => {
+            return (
+              item.value == voyageForm.value.loadPort &&
+              item.key == voyageForm.value.loadPortId
+            );
+          });
+          if (!b) {
+            voyageForm.value["loadPort"] = "";
+            voyageForm.value["loadPortId"] = "";
+          }
+        }
+
+        break;
+      }
+
+      case "dischargeProt": {
+        let index = ref(-1);
+        for (let i in colCache.value) {
+          if (voyageForm.value.dischargeProt == colCache.value[i].value) {
+            index.value = i;
+            break;
+          }
+        }
+        if (index.value != -1) {
+          voyageForm.value.dischargeProtId = colCache.value[index.value].key;
+        } else {
+          let b = colCache.value.some((item) => {
+            return (
+              item.value == voyageForm.value.dischargeProt &&
+              item.key == voyageForm.value.dischargeProtId
+            );
+          });
+          if (!b) {
+            voyageForm.value["dischargeProt"] = "";
+            voyageForm.value["dischargeProtId"] = "";
+          }
+        }
+        break;
+      }
+    }
+  }, 200);
+}
+let voyageFormRef = ref(null);
+
+async function addVoyage() {
+  voyageFormRef.value.validate(async (valid) => {
+    if (valid) {
+      console.log("提交", voyageForm.value);
+      discPorts.value = discPorts.value.filter((item) => {
+        return item.loadPortId && item.loadPort;
+      });
+
+      if (!discPorts.value.length) {
+        ElNotification({
+          title: "请选择至少一个装货港",
+          type: "error",
+        });
+        return;
+      }
+      let discId = [];
+      let discStr = [];
+      for (let i of discPorts.value) {
+        discId.push(i.loadPortId);
+        discStr.push(i.loadPort);
+      }
+      let res = await api.addVoyage({
+        ...voyageForm.value,
+        dischargePortIds: discId.join(","),
+        dischargePorts: discStr.join(","),
+      });
+      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.value);
+    }
+  });
+}
+
+let shipsCache = ref([]);
+let colCache = ref([]);
+let cargoOwnersCache = ref([]);
+
+let 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 }
+);
+
+let selectShip = (item) => {
+  voyageForm.value.shipId = item.shipId;
+};
+
+let 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 }
+);
+
+let selectCargoOwner = (item) => {
+  voyageForm.value.cargoOwnerId = item.userId;
+};
+
+let 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 }
+);
+
+let selectLoadPort = (item) => {
+  voyageForm.value.loadPortId = item.key;
+  voyageForm.value.loadPort = item.value;
+};
+
+let selectDischargeProt = (item, index) => {
+  discPorts.value[index] = { loadPortId: item.key, loadPort: item.value };
+};
+
+function resetAddVoyageForm() {
+  voyageAddDialogVisible.value = false;
+  voyageFormRef.value.resetFields();
+  voyageForm.value = {
+    voyageName: "",
+    cargoOwnerId: "",
+    cargoOwnerName: "",
+    startTime: "",
+    endTime: "",
+    loadPort: "",
+    dischargeProt: "",
+    cargo: "",
+    tons: 0,
+    loadPortId: "",
+    dischargeProtId: "",
+    shipId: "",
+    shipName: "",
+    pieces: 0,
+  };
+  discPorts.value = [{}];
+}
+
+let sortradio = ref(0);
+
+let isUpLoading = ref(false);
+function upFYDISuccess(e) {
+  if (e.status == 0) {
+    ElNotification.success({
+      title: "成功",
+      duration: 2000,
+      message: e.msg,
+    });
+  } else {
+    ElNotification.error({
+      title: "失败",
+      duration: 2000,
+      message: e.msg,
+    });
+  }
+  isUpLoading.value = false;
+  FYDIModalVisable.value = false;
+  FYDIParams.value.cargoOwnerId = "";
+  cargoOwnerCompanyStr.value = "";
+}
+
+function beforeFYDI() {
+  isUpLoading.value = true;
+}
+
+let FYDIModalVisable = ref(false);
+let FYDIParams = ref({
+  cargoOwnerId: "",
+});
+let cargoOwnerCompanyStr = ref("");
+function selectCargoOwnerUpload(item) {
+  FYDIParams.value.cargoOwnerId = item.key;
+}
+
+function FYDIModalClose() {
+  isUpLoading.value = false;
+  FYDIModalVisable.value = false;
+  FYDIParams.value.cargoOwnerId = "";
+  cargoOwnerCompanyStr.value = "";
+}
+
+function rowStyle({ row }) {
+  let rowStyle = {};
+  if (row.daysInPort >= 30) {
+    rowStyle.color = "red";
+    return rowStyle;
+  }
+}
+let discPorts = ref([{}]);
+
+function addDiscPort() {
+  discPorts.value.push({});
+}
+onMounted(() => {
+  getVoyageList();
+});
+</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>

+ 42 - 0
vite.config.js

@@ -0,0 +1,42 @@
+import { defineConfig } from "vite";
+import vue from "@vitejs/plugin-vue";
+import viteCompression from "vite-plugin-compression";
+import AutoImport from "unplugin-auto-import/vite";
+import Components from "unplugin-vue-components/vite";
+import { ElementPlusResolver } from "unplugin-vue-components/resolvers";
+import path from "path";
+
+// https://vitejs.dev/config/
+export default defineConfig({
+  plugins: [
+    vue(),
+    viteCompression(),
+    AutoImport({
+      resolvers: [ElementPlusResolver()],
+    }),
+    Components({
+      resolvers: [ElementPlusResolver()],
+    }),
+  ],
+  css: {
+    preprocessorOptions: {
+      scss: {
+        charset: false,
+      },
+    },
+  },
+  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: 5568,
+  },
+});