فهرست منبع

init 汇很多船务公司

王智慧 3 سال پیش
والد
کامیت
0aaf33920b
46فایلهای تغییر یافته به همراه4429 افزوده شده و 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. 31 0
      package.json
  7. BIN
      public/汇很多-数据中心.ico
  8. 240 0
      src/App.vue
  9. 19 0
      src/apis/cloudLogin.js
  10. 24 0
      src/apis/config.js
  11. 423 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. 96 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. 86 0
      src/components/RemoteSelect.vue
  28. 1 0
      src/components/Table.vue
  29. 135 0
      src/components/Uploader.vue
  30. BIN
      src/images/顺发999.png
  31. 61 0
      src/main.js
  32. 76 0
      src/router/index.js
  33. 40 0
      src/store/index.js
  34. 199 0
      src/styles/index.css
  35. 16 0
      src/utils/chooseFile.js
  36. 33 0
      src/utils/downloadBlobFile.js
  37. 13 0
      src/utils/utils.js
  38. 17 0
      src/views/index/Index.vue
  39. 232 0
      src/views/index/Login.vue
  40. 855 0
      src/views/shipManage/shipDetail.vue
  41. 189 0
      src/views/shipManage/shipList.vue
  42. 452 0
      src/views/shipOwnerManage/shipOwnerDetail.vue
  43. 274 0
      src/views/shipOwnerManage/shipOwnerList.vue
  44. 5 0
      src/views/workStation/certsManage.vue
  45. 5 0
      src/views/workStation/insuranceManage.vue
  46. 42 0
      vite.config.js

+ 2 - 0
.env.dev

@@ -0,0 +1,2 @@
+VITE_PROJECT_ENV = 'dev'
+VITE_BASEURL = 'https://interface.huihenduo.com.cn/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
 # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
 node_modules
 node_modules
 
 
+dist
+

+ 6 - 2
README.md

@@ -1,3 +1,7 @@
-# ShippingService_WebApp
+# 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>

+ 31 - 0
package.json

@@ -0,0 +1,31 @@
+{
+  "name": "shippingservice",
+  "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",
+    "@element-plus/icons-vue": "^2.0.6",
+    "axios": "^0.21.1",
+    "copy-to-clipboard": "^3.3.1",
+    "element-plus": "^2.2.20",
+    "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/汇很多-数据中心.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 };

+ 423 - 0
src/apis/fetch.js

@@ -0,0 +1,423 @@
+import { $http } from "./config";
+export default {
+  // 员工登录
+  staffLogin(data) {
+    return $http("/staff/login", data);
+  },
+
+  // 获取用户列表 货主/船东
+  getUserList(data) {
+    return $http("user/list", data);
+  },
+
+  getShipOwnerList(data) {
+    return $http("/shipOwner/list", data);
+  },
+
+  getShipOwnerDetail(data) {
+    return $http("/shipOwner/details", data);
+  },
+
+  updateShipOwner(data) {
+    return $http("/shipOwner/update", data);
+  },
+
+  // 添加用户
+  addUser(data) {
+    return $http("user/add", data);
+  },
+
+  // 添加船东
+  addShipOwner(data) {
+    return $http("/shipOwner/add", data);
+  },
+
+  // 获取用户详情
+  getUserDetail(data) {
+    return $http("user/details", data);
+  },
+
+  // 更新用户详情
+  updateUserDetail(data) {
+    return $http("/user/update", data);
+  },
+
+  // 获取航次列表
+  getVoyageList(data) {
+    return $http("voyage/list", data);
+  },
+
+  // 获取航次详情
+  getVoyageDetail(data) {
+    return $http("/voyage/detail", data);
+  },
+
+  // 更新航次
+  updateVoyage(data) {
+    return $http("/voyage/update", data);
+  },
+
+  // 完成航次
+  finishVoyage(data) {
+    return $http("/voyage/finish", data);
+  },
+
+  // 根据船名/MMSI/船东手机号获取船舶用户信息(员工端添加航次选择船)
+  getUserInfoAndShipInfo(data) {
+    return $http("ship/userShipInfo", data);
+  },
+
+  // 添加航次
+  addVoyage(data) {
+    return $http("voyage/add", data);
+  },
+
+  // 获取媒体列表
+  getMediaList(data) {
+    return $http("/media/list", data);
+  },
+
+  // 审核媒体文件
+  auditMedia(data) {
+    return $http("/media/audit", data);
+  },
+
+  // 标记媒体文件
+  markMedia(data) {
+    return $http("/media/markMedia", data);
+  },
+
+  // 模糊搜索用户
+  searchUser(data) {
+    return $http("/user/search", data);
+  },
+
+  // 获取船舶列表
+  getShipList(data) {
+    return $http("/ship/list", data);
+  },
+
+  // 更新船舶信息
+  updateShip(data) {
+    return $http("/ship/update", data);
+  },
+
+  // 更新船东信息
+  updateShipOwner(data) {
+    return $http("/shipOwner/update", data);
+  },
+
+  // 获取船舶详情
+  getShipDetail(data) {
+    return $http("/ship/detail", data);
+  },
+
+  // 船舶查询
+  searchShip(data) {
+    return $http("ship/search", data);
+  },
+
+  // 根据shipId获取船东列表
+  getShipOwnerListByShipId(data) {
+    return $http("/ship/detail/shopOwner/list", 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/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/getCol", data);
+  },
+
+  // 取消航次
+  cancelVoyage(data) {
+    return $http("/voyage/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/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/addNewPort", data);
+  },
+
+  // 代理列表
+  getAgencyList(data) {
+    return $http("/proxy/list", data);
+  },
+
+  // 代理子账户列表
+  getAgencySubAccountList(data) {
+    return $http("/proxy/detail/account/list", data);
+  },
+
+  // 添加代理子账户
+  addAgencySubAccount(data) {
+    return $http("/proxy/detail/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);
+  },
+
+  // 获取代理公司审核列表
+  getAgencyExamineList(data) {
+    return $http("/proxy/audit/list", data);
+  },
+
+  // 代理公司审核
+  examineAgency(data) {
+    return $http("/proxy/audit", 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);
+  },
+
+  // 取消货主公司详情-关联代理;代理公司详情-关联货主公司
+  unrelateCargoAgency(data) {
+    return $http("/proxy/cargoOwner/delete", data);
+  },
+
+  // 代理公司详情-货主公司列表
+  getCargoOwnerCompanyByAgencyCompany(data) {
+    return $http("/proxy/detail/cargoOwner/list", data);
+  },
+
+  // 关联货主公司-select
+  getCargoOwnerCompanySelect(data) {
+    return $http("/proxy/detail/cargoOwner/select", data);
+  },
+
+  // 添加港口
+  addPort(data) {
+    return $http("/port/add", data);
+  },
+
+  // 港口列表
+  getPortsList(data) {
+    return $http("/port/list", data);
+  },
+
+  // 获取港口列表select
+  getPortsSelect(data) {
+    return $http("/port/select", data);
+  },
+
+  // 启用禁用港口
+  updatePortStatus(data) {
+    return $http("/port/updateStatus", data);
+  },
+
+  // 添加港口航期
+  addTransPort(data) {
+    return $http("/port/trans/add", data);
+  },
+
+  // 港口航期列表
+  getTransPortsList(data) {
+    return $http("/port/trans/list", data);
+  },
+
+  // 获取货主下拉
+  getCargoOwnerSelect(data) {
+    return $http("/proxy/detail/cargoOwner/select", data);
+  },
+
+  // 货主公司更新有效期
+  updateCargoOwnerExpiredTime(data) {
+    return $http("/cargoOwner/updateExpiredTime", data);
+  },
+
+  // 代理公司更新有效期
+  updateAgencyExpiredTime(data) {
+    return $http("/proxy/updateExpiredTime", data);
+  },
+
+  // 删除通用文件
+  deleteUFile(data) {
+    return $http("/ufile/delete", data);
+  },
+
+  // 获取通用文件列表
+  getUFileList(data) {
+    return $http("/ufile/getFileList", data);
+  },
+
+  // 获取通用文件类型选项
+  getUFileTypeSelect(data) {
+    return $http("/ufile/getTypeSelect", data);
+  },
+
+  // 上传通用文件
+  uploadUFile(data) {
+    return $http("/ufile/upload", 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


+ 96 - 0
src/components/Aside.vue

@@ -0,0 +1,96 @@
+<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>
+        <el-icon :size="18">
+          <component :is="item.icon" />
+        </el-icon>
+        <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: "Fold",
+        title: "首页",
+        items: [
+          {
+            path: "/index",
+            name: "首页",
+          },
+        ],
+      },
+      {
+        icon: "Avatar",
+        title: "船东管理",
+        items: [
+          {
+            path: "/shipOwnerManage/shipOwnerList",
+            name: "船东列表",
+          },
+        ],
+      },
+      {
+        icon: "Ship",
+        title: "船舶管理",
+        items: [
+          {
+            path: "/shipManage/shipList",
+            name: "船舶列表",
+          },
+        ],
+      },
+      {
+        icon: "Document",
+        title: "工作站",
+        items: [
+          {
+            path: "/workStation/certsManage",
+            name: "证书管理",
+          },
+          {
+            path: "/workStation/insuranceManage",
+            name: "保险管理",
+          },
+        ],
+      },
+    ];
+
+    return {
+      changeIndex,
+      defaultActive,
+      menu,
+    };
+  },
+};
+</script>
+<style scoped>
+.el-sub-menu__title i {
+  height: 20px;
+  margin-right: 10px;
+}
+</style>

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

+ 86 - 0
src/components/RemoteSelect.vue

@@ -0,0 +1,86 @@
+<template>
+  <el-select
+    filterable
+    :multiple="multiple"
+    :multiple-limit="multiple ? 0 : 1"
+    remote
+    clearable
+    reserve-keyword
+    :placeholder="placeholder"
+    :loading="loading"
+    @change="selectItem"
+    @focus="getSelectList"
+  >
+    <el-option
+      v-for="item in options"
+      :key="item.key"
+      :label="item.value"
+      :value="{ value: item.key, key: item.value }"
+    />
+  </el-select>
+</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,
+    multiple: {
+      type: Boolean,
+      default: false,
+    },
+  },
+
+  emits: ["selectItem"],
+  setup(props, { emit }) {
+    let loading = ref(true);
+    const getSelectList = _.debounce(
+      async (term) => {
+        if (options.value.length) return;
+        loading.value = true;
+        let res = await api[props.api]({
+          ...props.params,
+          term: "",
+        });
+        if (res.data.status == 0) {
+          options.value = res.data.result;
+        } else {
+          options.value = [];
+        }
+        loading.value = false;
+      },
+      2000,
+      { leading: true }
+    );
+    let options = ref([]);
+    const selectItem = (item) => {
+      emit("selectItem", item);
+    };
+
+    onMounted(() => {});
+    return {
+      getSelectList,
+      selectItem,
+      options,
+      loading,
+    };
+  },
+};
+</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";
+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) {
+      try {
+        let d = document.getElementById(props.uploaderId);
+        d.childNodes[1].style.display = boo ? "inline-block" : "none";
+      } catch (error) {}
+
+      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


+ 61 - 0
src/main.js

@@ -0,0 +1,61 @@
+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";
+import RemoteSelect from "./components/RemoteSelect.vue";
+import zhCn from "element-plus/dist/locale/zh-cn.mjs";
+
+const app = createApp(App);
+app.use(ElementPlus, {
+  locale: zhCn,
+});
+app.component("Certs", Certs);
+app.component("Uploader", Uploader);
+app.component("RemoteSearch", RemoteSearch);
+app.component("RemoteSelect", RemoteSelect);
+import * as ElementPlusIconsVue from "@element-plus/icons-vue";
+for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
+  app.component(key, component);
+}
+router.beforeEach(async (to, from, next) => {
+  let id = localStorage.id;
+  if (id) {
+    store.commit("changeLogin", true);
+    if (0 === to.matched.length) {
+      next("/index");
+    } else if (to.path == "/login" || to.path == "/") {
+      next("/index");
+    } 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");

+ 76 - 0
src/router/index.js

@@ -0,0 +1,76 @@
+import {
+  createWebHistory,
+  createWebHashHistory,
+  createMemoryHistory,
+  createRouter,
+} from "vue-router";
+import Login from "../views/index/Login.vue";
+
+const router = createRouter({
+  history: createWebHashHistory(),
+  routes: [
+    {
+      path: "/index",
+      name: "index",
+      component: () => import("../views/index/Index.vue"),
+    },
+    {
+      path: "/login",
+      name: "login",
+      meta: {
+        title: "登录",
+      },
+      component: Login,
+    },
+    {
+      path: "/shipManage/shipList",
+      name: "shipList",
+      meta: {
+        title: "船舶列表",
+      },
+      component: () => import("../views/shipManage/shipList.vue"),
+    },
+    {
+      path: "/shipManage/shipDetail",
+      name: "shipDetail",
+      meta: {
+        title: "船舶详情",
+      },
+      component: () => import("../views/shipManage/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: "//workStation/certsManage",
+      name: "certsManage",
+      meta: {
+        title: "证书管理",
+      },
+      component: () => import("../views/workStation/certsManage.vue"),
+    },
+    {
+      path: "/workStation/insuranceManage",
+      name: "insuranceManage",
+      meta: {
+        title: "保险管理",
+      },
+      component: () => import("../views/workStation/insuranceManage.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: "/voyage/voyageList",
+    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;

+ 199 - 0
src/styles/index.css

@@ -0,0 +1,199 @@
+
+.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;
+}
+
+.tac{
+  text-align: center;
+}
+
+

+ 16 - 0
src/utils/chooseFile.js

@@ -0,0 +1,16 @@
+function chooseFile(type) {
+  return new Promise((resolve, reject) => {
+    let fileId = type + new Date().getTime();
+    let inputObj = document.createElement("input");
+    inputObj.setAttribute("id", fileId);
+    inputObj.setAttribute("type", "file");
+    inputObj.setAttribute("style", "visibility:hidden");
+    document.body.appendChild(inputObj);
+    inputObj.addEventListener("change", (e) => {
+      resolve(document.getElementById(fileId).files[0]);
+    });
+    inputObj.click();
+  });
+}
+
+export { chooseFile };

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

+ 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 © 2022 汇很多(江苏)科技有限公司 苏ICP备2022023253号-1 号
+    </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: "/index" });
+          } 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: 100vw;
+  bottom: 70px;
+  text-align: center;
+  font-family: PingFang SC;
+  font-weight: 400;
+  color: #aaa;
+  opacity: 0.8;
+  cursor: pointer;
+}
+</style>

+ 855 - 0
src/views/shipManage/shipDetail.vue

@@ -0,0 +1,855 @@
+<template>
+  <div class="line-container-p24">
+    <i class="el-icon-arrow-left"></i>
+    <div
+      class="dib go-back ml8 pointer"
+      @click="router.replace('/shipManage/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="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="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"
+          :current-page="currentPage"
+          :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(1)">查询</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
+        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="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="hasInsurance"
+        label="保险状态"
+        min-width="70"
+        align="center"
+      >
+        <template v-slot="scope">
+          {{ scope.row.hasInsurance == 0 ? "未购买" : "已购买" }}
+        </template>
+      </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"
+        :current-page="currentPage"
+        :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(page) {
+      currentPage.value = page || currentPage.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;
+        total.value = res.data.total;
+      } else {
+        tableData.value = [];
+        total.value = 0;
+      }
+    }
+    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>

+ 189 - 0
src/views/shipManage/shipList.vue

@@ -0,0 +1,189 @@
+<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(1)">查询</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="100"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="mmsi"
+          label="MMSI"
+          min-width="100"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="length"
+          label="船长(米)"
+          min-width="100"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="breadth"
+          label="船宽(米)"
+          min-width="100"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="draught"
+          label="吃水(米)"
+          min-width="100"
+          align="center"
+        ></el-table-column>
+        <el-table-column
+          prop="loadTons"
+          label="载货吨位(吨)"
+          min-width="100"
+          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"
+          :current-page="currentPage"
+          :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 getShipList(page) {
+      currentPage.value = page || currentPage.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;
+      } else {
+        tableData.value = [];
+        total.value = 0;
+      }
+    }
+
+    async function shipDetail(shipId) {
+      router.push({
+        path: "/shipManage/shipDetail",
+        query: {
+          shipId,
+        },
+      });
+    }
+    function pageChange(e) {
+      currentPage.value = e;
+      getShipList();
+    }
+    onMounted(() => {
+      getShipList();
+    });
+    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.getShipOwnerDetail({
+        shipOwnerId: route.query.shipOwnerId,
+      });
+      if (res.data.status == 0) {
+        let r = res.data.result;
+
+        let {
+          //船东信息
+          idcardBackDownloadUrl,
+          idcardBackFileKey,
+          idcardBackViewUrl,
+          idcardFrontDownloadUrl,
+          idcardFrontFileKey,
+          idcardFrontViewUrl,
+          idcardNo,
+          phone,
+          preferenceCargo,
+          userId: shipOwnerId,
+          userName,
+
+          // 船舶信息
+          breadth,
+          draught,
+          length,
+          loadTons,
+          mmsi,
+          shipId,
+          shipname,
+          tonnage,
+          shipCerts,
+        } = r;
+
+        userDetail.value = {
+          idcardNo,
+          phone,
+          preferenceCargo,
+          shipOwnerId,
+          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: "",
+      shipOwnerId: 0,
+    });
+
+    let shipParams = ref({
+      type: 2,
+      shipOwnerId: 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.updateShipOwner(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,
+          shipOwnerId: route.query.shipOwnerId,
+        };
+        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>

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

@@ -0,0 +1,274 @@
+<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(1)">查询</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="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="shipOwnerDetail(scope.row.shipOwnerId, 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"
+          :current-page="currentPage"
+          :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(0);
+    async function getShipOwnerList(page) {
+      currentPage.value = page || currentPage.value;
+      let res = await api.getShipOwnerList({
+        currentPage: currentPage.value,
+        size: 10,
+        term: term.value,
+      });
+      if (res.data.status == 0) {
+        tableData.value = res.data.result;
+        total.value = res.data.total;
+      } else {
+        tableData.value = [];
+        total.value = 0;
+      }
+    }
+
+    async function shipOwnerDetail(shipOwnerId) {
+      router.push({
+        path: "/shipOwnerManage/shipOwnerDetail",
+        query: {
+          shipOwnerId,
+        },
+      });
+    }
+    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>

+ 5 - 0
src/views/workStation/certsManage.vue

@@ -0,0 +1,5 @@
+<template>证书管理</template>
+
+<script setup></script>
+
+<style scoped></style>

+ 5 - 0
src/views/workStation/insuranceManage.vue

@@ -0,0 +1,5 @@
+<template>保险管理</template>
+
+<script setup></script>
+
+<style scoped></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,
+  },
+});