浏览代码

值班管理系统

CzRger 1 年之前
当前提交
3ce5110419
共有 100 个文件被更改,包括 4732 次插入0 次删除
  1. 1 0
      .env
  2. 25 0
      .gitignore
  3. 0 0
      README.md
  4. 26 0
      index.html
  5. 38 0
      package.json
  6. 29 0
      preBuild.js
  7. 38 0
      src/App.vue
  8. 19 0
      src/api/index.ts
  9. 100 0
      src/api/interceptors.ts
  10. 166 0
      src/api/modules/bayonet-deep-check/car.ts
  11. 93 0
      src/api/modules/bayonet-deep-check/com.ts
  12. 106 0
      src/api/modules/bayonet-deep-check/people.ts
  13. 62 0
      src/api/modules/bayonet-deep-check/work.ts
  14. 45 0
      src/api/modules/global/global.ts
  15. 10 0
      src/api/modules/global/info-center.ts
  16. 9 0
      src/api/modules/home-management/home.ts
  17. 17 0
      src/api/modules/inside-management/equipment.ts
  18. 16 0
      src/api/modules/jurisdiction-management/element.ts
  19. 9 0
      src/api/modules/law-management/case.ts
  20. 33 0
      src/api/modules/law-management/patrol.ts
  21. 30 0
      src/api/modules/law-management/security-inspection.ts
  22. 12 0
      src/api/modules/law-management/task.ts
  23. 133 0
      src/api/modules/law-management/visit.ts
  24. 38 0
      src/api/modules/platform-management/dept.ts
  25. 9 0
      src/api/modules/platform-management/dict.ts
  26. 24 0
      src/api/modules/ship-management/cbgl.ts
  27. 41 0
      src/api/modules/ship-management/port.ts
  28. 218 0
      src/api/request.ts
  29. 二进制
      src/assets/images/cus/cus-tab_type2-active.png
  30. 二进制
      src/assets/images/cus/cus-title_type2-icon.png
  31. 二进制
      src/assets/images/cus/file-del.png
  32. 二进制
      src/assets/images/cus/to-bottom.png
  33. 二进制
      src/assets/images/cus/to-right.png
  34. 二进制
      src/assets/images/file-type/file-type_default.png
  35. 二进制
      src/assets/images/file-type/file-type_excel.png
  36. 二进制
      src/assets/images/file-type/file-type_pdf.png
  37. 二进制
      src/assets/images/file-type/file-type_ppt.png
  38. 二进制
      src/assets/images/file-type/file-type_rar.png
  39. 二进制
      src/assets/images/file-type/file-type_txt.png
  40. 二进制
      src/assets/images/file-type/file-type_word.png
  41. 二进制
      src/assets/images/global/login_bg.jpeg
  42. 二进制
      src/assets/images/global/password.png
  43. 二进制
      src/assets/images/global/temp.png
  44. 二进制
      src/assets/images/global/user.png
  45. 二进制
      src/assets/images/layout/background.png
  46. 二进制
      src/assets/images/layout/background_home.png
  47. 二进制
      src/assets/images/layout/head-bg.png
  48. 二进制
      src/assets/images/layout/head-com-avatar.png
  49. 二进制
      src/assets/images/layout/head-com-bg.png
  50. 二进制
      src/assets/images/layout/head-com-dropdown.png
  51. 二进制
      src/assets/images/layout/head-com-info-center.png
  52. 二进制
      src/assets/images/layout/head-com-search.png
  53. 二进制
      src/assets/images/layout/logo_icon.png
  54. 二进制
      src/assets/images/layout/logo_name.png
  55. 二进制
      src/assets/images/layout/menu-active-special.png
  56. 二进制
      src/assets/images/layout/menu-bg-special.png
  57. 二进制
      src/assets/images/layout/menu-item-bg-active.png
  58. 二进制
      src/assets/images/layout/menu-item-bg.png
  59. 二进制
      src/assets/images/layout/menu-top-title.png
  60. 二进制
      src/assets/mock.png
  61. 6 0
      src/assets/svg/add.svg
  62. 5 0
      src/assets/svg/arrow_1.svg
  63. 8 0
      src/assets/svg/arrow_2.svg
  64. 7 0
      src/assets/svg/close_1.svg
  65. 8 0
      src/assets/svg/close_2.svg
  66. 7 0
      src/assets/svg/cus-content_field-column-drag.svg
  67. 7 0
      src/assets/svg/cus-content_field-column-select.svg
  68. 8 0
      src/assets/svg/cus-content_field-column.svg
  69. 11 0
      src/assets/svg/cus-content_field-fixed.svg
  70. 11 0
      src/assets/svg/cus-content_field-in.svg
  71. 11 0
      src/assets/svg/cus-content_tools.svg
  72. 9 0
      src/assets/svg/default.svg
  73. 1 0
      src/assets/svg/menu-mock.svg
  74. 8 0
      src/assets/svg/search.svg
  75. 1 0
      src/assets/svg/to_bottom_camera.svg
  76. 1 0
      src/assets/svg/to_right_camera.svg
  77. 1 0
      src/assets/svg/u212.svg
  78. 53 0
      src/components/SvgIcon/index.vue
  79. 580 0
      src/components/cus/CusContent.vue
  80. 115 0
      src/components/cus/CusContentCard.vue
  81. 156 0
      src/components/cus/CusDialog.vue
  82. 67 0
      src/components/cus/CusEllipsis.vue
  83. 86 0
      src/components/cus/CusForm.vue
  84. 369 0
      src/components/cus/CusFormColumn.vue
  85. 92 0
      src/components/cus/CusPopover.vue
  86. 86 0
      src/components/cus/CusSearchButtons.vue
  87. 122 0
      src/components/cus/CusTab.vue
  88. 395 0
      src/components/cus/CusTable.vue
  89. 156 0
      src/components/cus/CusTableCard.vue
  90. 72 0
      src/components/cus/CusTableColumn.vue
  91. 117 0
      src/components/cus/CusTree.vue
  92. 109 0
      src/components/cus/CusTreeItem.vue
  93. 139 0
      src/components/cus/README.md
  94. 125 0
      src/components/cus/cus-form-link/cascader.vue
  95. 90 0
      src/components/cus/cus-form-link/checkbox.vue
  96. 113 0
      src/components/cus/cus-form-link/date.vue
  97. 73 0
      src/components/cus/cus-form-link/datetime.vue
  98. 83 0
      src/components/cus/cus-form-link/dept.vue
  99. 77 0
      src/components/cus/cus-form-link/input.vue
  100. 0 0
      src/components/cus/cus-form-link/portOfRegistry.vue

+ 1 - 0
.env

@@ -0,0 +1 @@
+VITE_PROJECT_TITLE = 值班管理系统

+ 25 - 0
.gitignore

@@ -0,0 +1,25 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+seat*
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+stats.html
+yarn.lock

+ 0 - 0
README.md


+ 26 - 0
index.html

@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+  <meta charset="UTF-8" />
+  <meta name="referrer" content="no-referrer" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <meta name="keywords" content="值班管理系统" />
+  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+  <meta http-equiv="X-UA-Compatible" content="IE=8"/>
+  <meta http-equiv="Pragma" content="no-cache"/>
+  <meta http-equiv="cache-control" content="no-cache, no-store, must-revalidate"/>
+  <meta http-equiv="Cache" content="no-cache"/>
+  <meta http-equiv="expires" content="0"/>
+  <title><{VITE_PROJECT_TITLE}></title>
+<!--  <link rel="stylesheet" href="/Cesium/Widgets/widgets.css">-->
+<!--  <script src="/Cesium/Cesium.js"></script>-->
+  <script src="/config.js"></script>
+</head>
+
+<body style="overflow: hidden;">
+<div id="app"></div>
+<script type="module" src="/src/main.ts"></script>
+</body>
+
+</html>

+ 38 - 0
package.json

@@ -0,0 +1,38 @@
+{
+  "name": "ax-web-seats",
+  "private": true,
+  "version": "0.0.0",
+  "type": "module",
+  "scripts": {
+    "dev": "vite --mode development --open",
+    "build": "node preBuild.js && vite build --mode development",
+    "serve": "vite preview"
+  },
+  "dependencies": {
+    "@types/node": "^20.1.0",
+    "animate.css": "^4.1.1",
+    "axios": "^1.3.4",
+    "element-plus": "^2.4.1",
+    "sass": "^1.60.0",
+    "uuid": "^9.0.0",
+    "vue": "^3.3.4",
+    "vue-router": "^4.2.4",
+    "vuex": "^4.1.0",
+    "vite-plugin-svg-icons": "^2.0.1",
+    "fast-glob": "^3.3.0",
+    "vite-plugin-top-level-await": "^1.3.1",
+    "vite-plugin-html-env": "^1.2.8",
+    "dayjs": "^1.11.9",
+    "echarts": "^5.4.3",
+    "default-passive-events": "^2.0.0"
+  },
+  "devDependencies": {
+    "@types/uuid": "^9.0.2",
+    "@vitejs/plugin-vue": "^4.1.0",
+    "typescript": "^4.9.3",
+    "vite": "^4.2.0",
+    "vue-tsc": "^1.2.0",
+    "vite-plugin-compression": "^0.5.1",
+    "rollup-plugin-visualizer": "^5.9.2"
+  }
+}

+ 29 - 0
preBuild.js

@@ -0,0 +1,29 @@
+import * as child_process from 'child_process'
+import * as fs from 'fs'
+import dayjs from 'dayjs'
+
+let commitId = child_process.execSync(`git log -n1 --format=format:"%H"`).toString();
+let commitUser = child_process.execSync(`git log -n1 --format=format:"%an"`).toString();
+let commitBranch = child_process.execSync(`git log -n1 --format=format:"%d"`).toString().trim();
+let commitTime = child_process.execSync(`git log -n1 --pretty=format:"%ad" --date=iso`).toString().substring(0, 19);
+let buildUser = child_process.execSync(`git config user.name`).toString().trim();
+
+// const txt = `提交ID:${commitId}
+// 提交用户:${commitUser}
+// 提交分支:${commitBranch}
+// 提交时间:${commitTime}
+// 打包用户:${buildUser}
+// 打包时间:${dayjs(new Date()).format('YYYY-MM-DD HH:mm:ss')}
+// `
+const j = {
+  '提交ID:': commitId,
+  '提交用户:': commitUser,
+  '提交分支:': commitBranch,
+  '提交时间:': commitTime,
+  '打包用户:': buildUser,
+  '打包时间:': dayjs(new Date()).format('YYYY-MM-DD HH:mm:ss'),
+}
+
+fs.writeFileSync('./src/out/git_info.json', JSON.stringify(j));
+
+console.log('========== Save git info done. ==========');

+ 38 - 0
src/App.vue

@@ -0,0 +1,38 @@
+<template>
+  <div style="overflow: hidden">
+    <ElConfigProvider :locale="locale">
+      <router-view/>
+    </ElConfigProvider>
+  </div>
+</template>
+<script lang="ts">
+import {
+  defineComponent,
+  ref,
+  nextTick,
+  onMounted,
+  watch,
+  computed,
+  ComponentInternalInstance,
+  getCurrentInstance
+} from 'vue'
+import {useStore} from 'vuex'
+import {ElConfigProvider} from 'element-plus'
+import zhLocale from 'element-plus/es/locale/lang/zh-cn'
+export default defineComponent({
+  name: 'App',
+  components: {
+    [ElConfigProvider.name]: ElConfigProvider, //添加组件
+  },
+  setup() {
+    const store = useStore()
+    const locale = ref(zhLocale)
+    const that = (getCurrentInstance() as ComponentInternalInstance).appContext.config.globalProperties
+    return {
+      locale
+    }
+  }
+})
+</script>
+<style scope lang="scss">
+</style>

+ 19 - 0
src/api/index.ts

@@ -0,0 +1,19 @@
+import HttpService from "./request";
+
+export const handle = ({url, params, method,config,filName}: any) => {
+  const httpService = new HttpService()
+  // @ts-ignore
+  return httpService[method.toLowerCase()](url, params,config,filName)
+}
+
+// @ts-ignore
+const files = import.meta.glob("/src/api/modules/**/*.ts")
+const apiModule: any = {}
+
+for (const [key, value] of Object.entries(files)) {
+  // @ts-ignore
+  value().then(res => {
+    Object.assign(apiModule, res)
+  })
+}
+export default apiModule

+ 100 - 0
src/api/interceptors.ts

@@ -0,0 +1,100 @@
+import axios from 'axios';
+import {notify} from '@/utils/notify'
+import {toLogin} from '@/router/index'
+export class Interceptors {
+  public instance: any
+
+  constructor() {
+    this.instance = axios.create({timeout: 1000 * 300})
+    this.initInterceptors()
+  }
+
+  public getInterceptors() {
+    return this.instance
+  }
+
+  public initInterceptors() {
+    // this.instance.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=utf-8'
+    /**
+     * 请求拦截器
+     * 每次请求前,如果存在token则在请求头中携带token
+     */
+    this.instance.interceptors.request.use(
+      (config: any) => {
+        if (!config.headers.Authorization) {
+          const token = sessionStorage.getItem('ax_token');
+          if (token) {
+            config.headers.Authorization = token;
+          } else {
+            // @ts-ignore
+            delete config.headers.Authorization
+          }
+        }
+        return config;
+      },
+      (error: any) => {
+      }
+    );
+
+    // 响应拦截器
+    this.instance.interceptors.response.use(
+      // 请求成功
+      (res: any) => {
+        if (res.headers.authorization) {
+          sessionStorage.setItem('ax_token', res.headers.authorization);
+        } else {
+          if (res.data && res.data.code === 200 && res.data.token) {
+            sessionStorage.setItem('ax_token', res.data.token);
+          }
+        }
+        if (res.status === 200) {
+          if(res.data.code === 200){
+            return Promise.resolve(res.data);
+          } else if (res.data.code === 401) {
+            if(res.data.msg){
+              //@ts-ignore
+              notify.warning(res.data.msg);
+            }
+            toLogin()
+          } else {
+            return Promise.resolve(res.data);
+          }
+        } else {
+          this.errorHandle(res);
+          return Promise.reject(res.data);
+        }
+      },
+      // 请求失败
+      (error: { response: any; }) => {
+        const { response } = error;
+        if (response) {
+          // 请求已发出,但是不在2xx的范围
+          this.errorHandle(response);
+          return Promise.reject(response.data);
+        } else {
+          //@ts-ignore
+          notify.warning('网络连接异常,请稍后再试!');
+          // 抛出报错信息,在页面里需要接收
+          return Promise.reject(error);
+        }
+      });
+  }
+
+  private errorHandle(res: any) {
+    console.error('错误接口:' + res.data.path)
+    // 状态码判断
+    switch (res.status) {
+      case 401:
+        break;
+      case 403:
+        break;
+      case 404:
+        //@ts-ignore
+        notify.warning('请求的资源不存在');
+        break;
+      default:
+        //@ts-ignore
+        notify.warning('连接错误');
+    }
+  }
+}

+ 166 - 0
src/api/modules/bayonet-deep-check/car.ts

@@ -0,0 +1,166 @@
+import { handle } from '../../index'
+
+const suffix = 'api'
+
+// 获取车辆品牌
+export const getBrand = (params: any) => handle({
+	url: `/${suffix}/bayonet/brand/${params}`,
+	method: 'get',
+})
+// 车辆查询-不分组
+export const carWarningDataNoGroup = (params: any) => handle({
+	url: `/${suffix}/bayonet/car/search/carWarningDataNoGroup`,
+	method: 'post',
+	params
+})
+// 获取凭证
+export const carWarningDataGroupCache = (params: any) => handle({
+	url: `/${suffix}/bayonet/car/search/carWarningDataGroupCache`,
+	method: 'post',
+	params
+})
+// 车辆查询-分组
+export const carWarningDataGroup = (params: any) => handle({
+	url: `/${suffix}/bayonet/car/search/carWarningDataGroup`,
+	method: 'post',
+	params
+})
+//车辆抽查列表
+export const getCarList = (params: any) => handle({
+	url: `/${suffix}/bayonet/vehicleSpotChecks/getList`,
+	method: 'post',
+	params
+})
+export const submitCarImgModelTask = (params: any) => handle({
+	url: `/${suffix}/bayonet/car/search/submitCarImgModelTask`,
+	method: 'post',
+	params
+})
+export const selectCarDataPage = (params: any) => handle({
+	url: `/${suffix}/bayonet/car/search/selectCarDataPage`,
+	method: 'post',
+	params
+})
+//获取车辆详情
+export const getCarDetailInfoById = (id: any) => handle({
+	url: `/${suffix}/bayonet/common/${id}`,
+	method: 'get',
+})
+//以车搜车-获取车辆详情
+export const selectCarDataById = (params: any) => handle({
+	url: `/${suffix}/bayonet/car/search/selectCarDataById`,
+	method: 'post',
+	params
+})
+//新增核查结果
+export const addCheckResult = (type: number) => handle({
+	url: `/${suffix}/bayonet/vehicleSpotChecks/addCheckResult/${type}`,
+	method: 'post'
+})
+//获取区域管控列表
+export const getAreaControlList = (params:any) => handle({
+	url: `/${suffix}/bayonet/vehicleSpotChecks/getAreaControlList`,
+	method: 'post',
+	params
+})
+//将未核查图片改为已核查
+export const addCheckTaskRecord = (params:any) => handle({
+	url: `/${suffix}/bayonet/vehicleSpotChecks/addCheckTaskRecord`,
+	method: 'getjson',
+	params
+})
+//区域id获取区域信息
+export const getTaskArealnfoById = (id:any) => handle({
+	url: `/${suffix}/bayonet/vehicleSpotChecks/getTaskAreaInfoById/${id}`,
+	method: 'get',
+})
+//新增区域管控
+export const addAreaControl = (params:any) => handle({
+	url: `/${suffix}/bayonet/vehicleSpotChecks/addAreaControl`,
+	method: 'post',
+	params
+})
+//根据id查询区域管控信息
+export const getAreaControlById = (id:any) => handle({
+	url: `/${suffix}/bayonet/vehicleSpotChecks/getAreaControlById/${id}`,
+	method: 'get',
+})
+//根据任务id滚动查询区域车辆核查清单
+export const getVehicleListByTaskId = (params:any) => handle({
+	url: `/${suffix}/bayonet/vehicleSpotChecks/getVehicleListByTaskId`,
+	method: 'getjson',
+	params
+})
+//战果统计查询详情
+export const getStatisticsList = (params:any) => handle({
+	url: `/${suffix}/bayonet/vehicleSpotChecks/getStatisticsList`,
+	method: 'post',
+	params
+})
+//提前结束-提交战果
+export const earlyEndTask = (params:any) => handle({
+	url: `/${suffix}/bayonet/vehicleSpotChecks/earlyEndTask`,
+	method: 'getjson',
+	params
+})
+//查询新数据数据量
+export const getNewDataCount = (params:any) => handle({
+	url: `/${suffix}/bayonet/vehicleSpotChecks/getNewDataCount`,
+	method: 'getjson',
+	params
+})
+// 获取区域列表数据条数
+export const getAreaControlData = (params:any) => handle({
+	url: `/${suffix}/bayonet/vehicleSpotChecks/getAreaControlData`,
+	method: 'post',
+	params
+})
+// 结束任务
+export const onverTask = (params:any) => handle({
+	url: `/${suffix}/bayonet/vehicleSpotChecks/earlyEndTask`,
+	method: 'getjson',
+	params
+})
+// 获取战果详情list条数
+export const getStatisticsData = (params:any) => handle({
+	url: `/${suffix}/bayonet/vehicleSpotChecks/getStatisticsData`,
+	method: 'post',
+	params
+})
+
+// 以车搜车-数据导出
+export const CarDataPageExport = (params: any) => handle({
+	url: `/${suffix}/bayonet/car/search/selectCarDataPageExport`,
+	method: 'exportfile',
+	params: {
+		method: "post",
+		params,
+	}
+})
+//车辆查询-数据导出
+export const carWarningDataExport = (params: any) => handle({
+	url: `/${suffix}/bayonet/car/search/carWarningDataExport`,
+	method: 'exportfile',
+	params: {
+		method: "post",
+		params,
+	}
+})
+//车辆查询-根据车牌号查询车辆是否存在
+export const xqglCarExist = (params: any) => handle({
+	url: `/${suffix}/bayonet/car/search/xqglCarExist`,
+	method: 'post',
+	params
+})
+//车辆查询-根据车牌号查询车辆信息和车辆所有人信息
+export const carDetailInfoByPlateNo = (params: any) => handle({
+	url: `/${suffix}/bayonet/car/search/carDetailInfoByPlateNo`,
+	method: 'post',
+	params
+})
+// 自定义车辆标签查询
+export const customLabelsGetCarList = (params: any) => handle({
+	url: `/${suffix}/bayonet/customLabels/getCarList`,
+	method: 'post',
+	params
+})

+ 93 - 0
src/api/modules/bayonet-deep-check/com.ts

@@ -0,0 +1,93 @@
+import { handle } from '../../index'
+
+const suffix = 'api'
+
+// 卡口深度核查 > 文件上传 > 人脸建模
+export const getFaceModel = (params: any) => handle({
+  url: `/${suffix}/bayonet/person/search/imgFirstModeling`,
+  method: 'post',
+  params
+})
+// 卡口深度核查 > 文件上传 > 车辆建模
+export const getCarModel = (params: any) => handle({
+  url: `/${suffix}/bayonet/car/search/carImgModeling`,
+  method: 'post',
+  params
+})
+// 卡口深度核查 > 点位选择 > 点位树
+export const getVideoHKTree = (type: string) => handle({
+  url: `/${suffix}/bayonet/tree/hkTree/${type}`,
+  method: 'get'
+})
+// 卡口深度核查 > 点位选择 > 卡口地图标点
+export const getVideoMap = (params: any) => handle({
+  url: `/${suffix}/bayonet/tree/space`,
+  method: 'get',
+  params
+})
+// 卡口深度核查 > 图层 > 总队、支队、派出所、综合执法站、船管站详情
+export const elementDeptInfo = (params: any) => handle({
+  url: `/${suffix}/system/dept/onduty/detail/`,
+  method: 'getid',
+  params
+})
+// 卡口深度核查 > 图层 > 海岸线
+export const elementCoastInfo = (params: any) => handle({
+  url: `/${suffix}/system/coastline/`,
+  method: 'getid',
+  params
+})
+// 卡口深度核查 > 图层 > 岛屿
+export const elementIslandInfo = (params: any) => handle({
+  url: `/${suffix}/system/island/`,
+  method: 'getid',
+  params
+})
+// 卡口深度核查 > 图层 > 行业场所
+export const elementSiteInfo = (params: any) => handle({
+  url: `/${suffix}/system/site/`,
+  method: 'getid',
+  params
+})
+// 卡口深度核查 > 图层 > 政府机构
+export const elementGovernmentalInfo = (params: any) => handle({
+  url: `/${suffix}/system/governmental/`,
+  method: 'getid',
+  params
+})
+// 卡口深度核查 > 图层 > 警务区
+export const elementDistrictInfo = (params: any) => handle({
+  url: `/${suffix}/system/district/`,
+  method: 'getid',
+  params
+})
+// 卡口深度核查 > 图层 > 泊位
+export const elementBerthInfo = (params: any) => handle({
+  url: `/${suffix}/system/berth/`,
+  method: 'getid',
+  params
+})
+// 卡口深度核查 > 图层 > 港区设施
+export const elementFacilitiesInfo = (params: any) => handle({
+  url: `/${suffix}/system/facilities/`,
+  method: 'getid',
+  params
+})
+// 卡口深度核查 > 图层 > 进出港通道
+export const elementAccessWayInfo = (params: any) => handle({
+  url: `/${suffix}/system/accessWay/`,
+  method: 'getid',
+  params
+})
+// 卡口深度核查 > 图层 > 锚地
+export const elementAnchorageGroundInfo = (params: any) => handle({
+  url: `/${suffix}/system/anchorageGround/`,
+  method: 'getid',
+  params
+})
+// 卡口深度核查 > 图层 > 港区
+export const elementPortInfo = (params: any) => handle({
+  url: `/${suffix}/system/port/getCardInfo/`,
+  method: 'getid',
+  params
+})

+ 106 - 0
src/api/modules/bayonet-deep-check/people.ts

@@ -0,0 +1,106 @@
+import { handle } from '../../index'
+
+const suffix = 'api'
+
+// 人员查询-不分组
+export const personWarningDataNoGroup = (params: any) => handle({
+	url: `/${suffix}/bayonet/person/search/personWarningDataNoGroup`,
+	method: 'post',
+	params
+})
+
+// 人员查询-分组凭证获取
+export const personWarningDataCache = (params: any) => handle({
+	url: `/${suffix}/bayonet/person/search/personWarningDataCache`,
+	method: 'post',
+	params
+})
+
+// 人员查询-分组数据获取
+export const personWarningDataGroup = (params: any) => handle({
+	url: `/${suffix}/bayonet/person/search/personWarningDataGroup`,
+	method: 'post',
+	params
+})
+
+// 人员查询-查看详情
+export const getPersonDataById = (params: any) => handle({
+	url: `/${suffix}/bayonet/person/search/getPersonDataById`,
+	method: 'post',
+	params
+})
+
+// 人员查询-预警规则
+export const warningRules = (params: any) => handle({
+	url: `/${suffix}/bayonet/person/search/warningRules`,
+	method: 'post',
+	params
+})
+// 车辆查询-预警规则
+export const carWarning = (params: any) => handle({
+	url: `/${suffix}/bayonet/car/search/warningRules`,
+	method: 'post',
+	params
+})
+
+// 人员查询-数据导出
+export const personDataExport = (params: any) => handle({
+	url: `/${suffix}/bayonet/person/search/personDataExport`,
+	method: 'exportfile',
+	params: {
+		method: "post",
+		params,
+	}
+})
+
+// 以人搜人提交搜索数据-凭证获取
+export const searchPersonSubmit = (params: any) => handle({
+	url: `/${suffix}/bayonet/person/search/searchPersonSubmit`,
+	method: 'post',
+	params
+})
+
+// 以人搜人分页查询-不分组、卡口
+export const selectPersonDataPage = (params: any) => handle({
+	url: `/${suffix}/bayonet/person/search/selectPersonDataPage`,
+	method: 'post',
+	params
+})
+
+// 以人搜人查询-根据对象分组
+export const selectPersonDataByObjectGroup = (params: any) => handle({
+	url: `/${suffix}/bayonet/person/search/selectPersonDataByObjectGroup`,
+	method: 'post',
+	params
+})
+
+// 以人搜人查询-根据id查询详情
+export const selectPerSonDataById = (params: any) => handle({
+	url: `/${suffix}/bayonet/person/search/selectPerSonDataById`,
+	method: 'post',
+	params
+})
+
+// 以人搜人查询-按对象查询详情
+export const getQueryCondition = (params: any) => handle({
+	url: `/${suffix}/bayonet/person/search/getQueryCondition`,
+	method: 'post',
+	params
+})
+
+// 以人搜人分页查询导出
+export const selectPersonDataPageExport = (params: any) => handle({
+	url: `/${suffix}/bayonet/person/search/selectPersonDataPageExport`,
+	method: 'exportfile',
+	params: {
+		method: "post",
+		params,
+	}
+})
+
+// 自定义人员标签查询
+export const customLabelsGetPersonList = (params: any) => handle({
+	url: `/${suffix}/bayonet/customLabels/getPersonList`,
+	method: 'post',
+	params
+})

+ 62 - 0
src/api/modules/bayonet-deep-check/work.ts

@@ -0,0 +1,62 @@
+import { handle } from '../../index'
+
+const suffix = 'api'
+
+// 实时动态-人脸
+export const getFaceCaptureDataApi = (params: any) => handle({
+  url: `/${suffix}/bayonet/comprehensive/getFaceCaptureData`,
+  method: 'post',
+  params
+})
+
+// 实时动态-车辆
+export const getCarCaptureDataApi = (params: any) => handle({
+  url: `/${suffix}/bayonet/comprehensive/getCarCaptureData`,
+  method: 'post',
+  params
+})
+
+// 活跃度-按预警量
+export const getListByWarningCountApi = (params: any) => handle({
+  url: `/${suffix}/bayonet/comprehensive/getListByWarningCount`,
+  method: 'getjson',
+  params
+})
+// 活跃度-按抓拍量
+export const queryBayonetTrafficVolumeApi = (params: any) => handle({
+  url: `/${suffix}/bayonet/comprehensive/queryBayonetTrafficVolume`,
+  method: 'post',
+  params
+})
+
+// 预警信息
+export const getWarningDataApi = (params: any) => handle({
+  url: `/${suffix}/bayonet/comprehensive/getWarningData`,
+  method: 'getjson',
+  params
+})
+// 感知数据
+export const getPerceiveDataAPi = (params: any) => handle({
+  url: `/${suffix}/bayonet/comprehensive/getPerceiveData`,
+  method: 'getjson',
+  params
+})
+
+// 通行数据
+export const getTransitDataApi = (params: any) => handle({
+  url: `/${suffix}/bayonet/comprehensive/getTransitData`,
+  method: 'post',
+  params
+})
+// 模型配置查询
+export const queryRegulatoryModelApi = (params: any) => handle({
+  url: `/${suffix}/bayonet/comprehensive/queryRegulatoryModel`,
+  method: 'getjson',
+  params
+})
+// 模型配置修改
+export const updateRegulatoryModelApi = (params: any) => handle({
+  url: `/${suffix}/bayonet/comprehensive/updateRegulatoryModel`,
+  method: 'post',
+  params
+})

+ 45 - 0
src/api/modules/global/global.ts

@@ -0,0 +1,45 @@
+import { handle } from '../../index'
+
+const suffix = 'api'
+
+export const login = (params: any) => handle({
+  url: `/${suffix}/login`,
+  method: 'post',
+  params,
+})
+
+// 获取路由
+export const getRoutes = () => handle({
+  // url: `/mock-api/getRouters`,
+  url: `/${suffix}/getRouters`,
+  method: 'get',
+})
+
+// 获取用户信息
+export const getUserInfo = () => handle({
+  url: `/${suffix}/getInfo`,
+  method: 'get',
+})
+
+// 获取参数配置列表
+export const getParameterList = params => handle({
+  url: `/${suffix}/system/config/list`,
+  method: 'getjson',
+  params
+})
+
+// 上传图片
+export const uploadPicOnly = params => handle({
+  url: `/${suffix}/common/upload/image`,
+  method: 'post',
+  params,
+  contentType: 'application/x-www-form-urlencoded'
+})
+
+// 上传文件
+export const uploadFile = (params: any) => handle({
+  url: `/${suffix}/common/upload`,
+  method: 'post',
+  params,
+  contentType: 'application/x-www-form-urlencoded'
+})

+ 10 - 0
src/api/modules/global/info-center.ts

@@ -0,0 +1,10 @@
+import { handle } from '../../index'
+
+const suffix = 'api'
+
+// 获取消息中心预警数量
+export const getHistoryMessagePageList = (params: any) => handle({
+  url: `/${suffix}/prevention/message/pageList`,
+  method: 'post',
+  params
+})

+ 9 - 0
src/api/modules/home-management/home.ts

@@ -0,0 +1,9 @@
+import { handle } from '../../index'
+
+const suffix = 'api'
+
+// 首页-获取权限部门
+export const getPowerDept = (id: any) => handle({
+  url: `/${suffix}/system/port/getPowerDept?deptId=${id}`,
+  method: 'get'
+})

+ 17 - 0
src/api/modules/inside-management/equipment.ts

@@ -0,0 +1,17 @@
+import { handle } from '../../index'
+
+const suffix = 'api'
+
+// 巡逻艇 > 列表查询
+export const patrolBoatList = (params: any) => handle({
+  url: `/${suffix}/system/patrolBoat/list`,
+  method: 'get',
+  params
+})
+
+// 辖区要素 > 设备 > 视频监控 > 查询
+export const monitoringList = (params: any) => handle({
+  url: `/${suffix}/system/monitoring/list`,
+  method: 'get',
+  params
+})

+ 16 - 0
src/api/modules/jurisdiction-management/element.ts

@@ -0,0 +1,16 @@
+import { handle } from '../../index'
+
+const suffix = 'api'
+
+// 辖区要素 > 地图要素偏好查询
+export const preferenceInquire = (params: any) => handle({
+  url: `/${suffix}/display/config/queryByUserIdAndType`,
+  method: 'getjson',
+  params
+})
+// 辖区要素 > 地图要素树形列表(新新)
+export const systemFactorListInfoNew = (params: any) => handle({
+  url: `/${suffix}/system/port/listInfo2`,
+  method: 'get',
+  params
+})

+ 9 - 0
src/api/modules/law-management/case.ts

@@ -0,0 +1,9 @@
+import { handle } from '../../index'
+
+const suffix = 'api'
+
+export const getCaseTypeTree = (params: any) => handle({
+  url: `/${suffix}/case/type/tree`,
+  method: 'getjson',
+  params
+})

+ 33 - 0
src/api/modules/law-management/patrol.ts

@@ -0,0 +1,33 @@
+import { handle } from '../../index'
+
+const suffix = 'api'
+
+
+export const getPatrolPointListAll = params => handle({
+	url: `/${suffix}/system/patrol/point/listAll`,
+	method: 'post',
+	params
+})
+
+// 线
+export const getPatrolLineListAll = params => handle({
+	url: `/${suffix}/system/patrol/line/listAll`,
+	method: 'post',
+	params
+})
+export const deletePatrolLine = params => handle({
+	url: `/${suffix}/system/patrol/line/batchDelete`,
+	method: 'POST',
+	params
+})
+export const deletePatrolPoint = params => handle({
+	url: `/${suffix}/system/patrol/point/batchDelete`,
+	method: 'POST',
+	params
+})
+//巡逻记录列表
+export const getPatrolInfoList = params => handle({
+	url: `/${suffix}/system/patrol/info/listPage`,
+	method: 'getjson',
+	params
+})

+ 30 - 0
src/api/modules/law-management/security-inspection.ts

@@ -0,0 +1,30 @@
+import { handle } from '../../index'
+
+const suffix = 'api'
+
+// 治安检查记录列表
+export const recordPage = params => handle({
+    url: `/${suffix}/check/record/page`,
+    method: 'post',
+    params
+})
+// 治安检查删除
+export const delBatch = params => handle({
+    url: `/${suffix}/check/record/delBatch`,
+    method: 'post',
+    params
+})
+
+// 人员检查记录列表
+export const getPeopleCheckList = params => handle({
+    url: `/${suffix}/zfzq/security/warning/person/list`,
+    method: 'post',
+    params
+})
+
+// 车辆检查记录列表
+export const getCarCheckList = params => handle({
+    url: `/${suffix}/zfzq/security/warning/car/list`,
+    method: 'post',
+    params
+})

+ 12 - 0
src/api/modules/law-management/task.ts

@@ -0,0 +1,12 @@
+import { handle } from '../../index'
+
+const suffix = 'api'
+
+export const taskPlanDropPatrolBoatAll = () => handle({
+  url: `/${suffix}/task/plan/drop/patrolBoat/all`,
+  method: 'GET'
+})
+export const taskPlanDropPoliceCarAll = () => handle({
+  url: `/${suffix}/task/plan/drop/policeCar/all`,
+  method: 'GET'
+})

+ 133 - 0
src/api/modules/law-management/visit.ts

@@ -0,0 +1,133 @@
+import { handle } from '../../index'
+
+const suffix = 'api'
+//走访计划
+export const getPlanList = params => handle({
+	url: `/${suffix}/visit/plan/list`,
+	method: 'post',
+	params
+})
+// 新增走访计划
+export const savePlan = params => handle({
+	url: `/${suffix}/visit/plan/save`,
+	method: 'post',
+	params
+})
+// 走访计划名称根据计划周期自动生成
+export const getPlanName = params => handle({
+	url: `/${suffix}/visit/drop/list/generate/plan/name`,
+	method: 'getjson',
+	params
+})
+// 获取该单位下未分配的人员数据
+export const getPerson = params => handle({
+	url: `/${suffix}/visit/drop/list/list/person`,
+	method: 'post',
+	params
+})
+// 获取该单位下(未安排-全部)走访计划的民警信息
+export const getPolices = params => handle({
+	url: `/${suffix}/visit/drop/list/get/polices`,
+	method: 'post',
+	params
+})
+// 查询走访记录表格数据
+export const get_tabPos = params => handle({
+	url: `/${suffix}/visit/plan/police/visit/record`,
+	method: 'POST',
+	params
+})
+// 查询详情
+export const getEditPolic = params => handle({
+	url: `/${suffix}/visit/plan/get/Info`,
+	method: 'get',
+	params
+})
+// 获取该单位下未分配的场所数据
+export const getListPlace = params => handle({
+	url: `/${suffix}/visit/drop/list/list/place`,
+	method: 'post',
+	params
+})
+// 获取该单位下未分配的船舶数据
+export const getListShip = params => handle({
+	url: `/${suffix}/visit/drop/list/list/ship`,
+	method: 'post',
+	params
+})
+// 更新走访计划
+export const uodatePlan = params => handle({
+	url: `/${suffix}/visit/plan/update`,
+	method: 'post',
+	params
+})
+// 走访登记上限接口
+export const visitRegistrationLimit = params => handle({
+	url: `/${suffix}/visit/drop/list/number`,
+	method: 'get',
+	params
+})
+// 获取该单位下部门信息
+export const get_dept_list = params => handle({
+	url: `/${suffix}/visit/drop/list/dept/list`,
+	method: 'POST',
+	params
+})
+// 获取该单位下用戶信息
+export const get_user_list = params => handle({
+	url: `/${suffix}/visit/drop/list/user/list`,
+	method: 'POST',
+	params
+})
+//导出走访计划列表
+export const outXlsx = (params) => handle({
+	url: `/${suffix}/visit/plan/end/plan/visit/export`,
+	method: 'exportfile',
+	params: {
+		params,
+		method: "post",
+	}
+})
+// 删除走访删除
+export const delPlan = params => handle({
+	url: `/${suffix}/visit/plan/remove`,
+	method: 'getjson',
+	params
+})
+// 发布
+export const pushPlan = params => handle({
+	url: `/${suffix}/visit/plan/push/plan`,
+	method: 'getjson',
+	params
+})
+// 结束数据
+export const endTabPlan = params => handle({
+	url: `/${suffix}/visit/plan/manualend/plan`,
+	method: 'getjson',
+	params
+})
+// 撤回
+export const retractPlan = params => handle({
+	url: `/${suffix}/visit/plan/recall/plan`,
+	method: 'getjson',
+	params
+})
+
+
+
+
+
+//走访记录
+
+//列表
+export const getRecordList = params => handle({
+	url: `/${suffix}/visit/record/list`,
+	method: 'post',
+	params
+})
+//删除
+export const handleRemoveRecordList = params => handle({
+	url: `/${suffix}/visit/record/remove`,
+	method: 'getjson',
+	params
+})

+ 38 - 0
src/api/modules/platform-management/dept.ts

@@ -0,0 +1,38 @@
+import { handle } from '../../index'
+
+const suffix = 'api'
+
+// 获取部门树
+export const getDeptTree = () => handle({
+  url: `/${suffix}/system/dept/deptTree`,
+  method: 'get',
+})
+
+// 获取所有部门树
+export const getDeptTreeAll = () => handle({
+  url: `/${suffix}/system/dept/deptTree/all`,
+  method: 'get',
+})
+
+// 获取部门树所有用户
+export const getUserAll = () => handle({
+  url: `/${suffix}/system/dept/allUserTree`,
+  method: 'get'
+})
+
+// 字典 > 获取部门级别
+export const dicDeptDeptLevel = () => handle({
+  url: `/${suffix}/system/dept/deptLevel`,
+  method: 'get',
+})
+
+// 字典 > 获取部门类型
+export const dicDeptDeptType = () => handle({
+  url: `/${suffix}/system/dept/deptType`,
+  method: 'get',
+})
+//  字典 > 通用方法
+export const dicDeptDictList = (params: any) => handle({
+  url: `/${suffix}/system/dict/data/type/${params}`,
+  method: 'get',
+})

+ 9 - 0
src/api/modules/platform-management/dict.ts

@@ -0,0 +1,9 @@
+import { handle } from '../../index'
+
+const suffix = 'api'
+
+//  字典 > 通用方法
+export const dicDeptDictList = (params: any) => handle({
+  url: `/${suffix}/system/dict/data/type/${params}`,
+  method: 'get',
+})

+ 24 - 0
src/api/modules/ship-management/cbgl.ts

@@ -0,0 +1,24 @@
+import { handle } from '../../index'
+
+const suffix = 'api'
+
+// 船舶信息船籍港一级
+export const cbglPersonDictionary = () => handle({
+  url: `/${suffix}/cbgl/person/dictionary`,
+  method: 'getjson',
+})
+// 船舶信息船籍港二级
+export const cbglPersonDictionary2 = (params: string) => handle({
+  url: `/${suffix}/cbgl/person/dictionarys/${params}`,
+  method: 'get',
+})
+// 船舶信息常驻停泊点一级
+export const cbglPersonTemporary = () => handle({
+  url: `/${suffix}/cbgl/person/temporary`,
+  method: 'getjson',
+})
+// 船舶信息常驻停泊点二级
+export const cbglPersonTemporary2 = (params: string) => handle({
+  url: `/${suffix}/cbgl/person/temporarys/${params}`,
+  method: 'get',
+})

+ 41 - 0
src/api/modules/ship-management/port.ts

@@ -0,0 +1,41 @@
+import { handle } from '../../index'
+
+const suffix = 'api'
+
+//根据部门树动态获取港口数据
+export const portQueryPortListName = (params: any) => handle({
+  url: `/${suffix}/system/port/queryPortListName`,
+  method: 'getjson',
+  params
+})
+
+// 港岙口信息列表
+export const portList = (params: any) => handle({
+  url: `/${suffix}/system/port/list`,
+  method: 'getjson',
+  params
+})
+
+// 辖区要素 > 港区 > 港岙口 > 详情
+export const portCheck = (params: any) => handle({
+  url: `/${suffix}/system/port/`,
+  method: 'getid',
+  params
+})
+
+// 辖区要素 > 港区 > 港岙口 > 删除
+export const portDel = (params: any) => handle({
+  url: `/${suffix}/system/port/`,
+  method: 'delete',
+  params
+})
+
+// 港岙口管理导出
+export const getSystemPortExport = (params: any) => handle({
+  url: `/${suffix}/system/port/export`,
+  method: 'exportfile',
+  params: {
+    params,
+    method: "post",
+  }
+})

+ 218 - 0
src/api/request.ts

@@ -0,0 +1,218 @@
+import { Interceptors } from './interceptors';
+import { ElMessage } from "element-plus";
+export class HttpRequest {
+  public axios: any
+  constructor() {
+    // 获取axios实例
+    this.axios = new Interceptors().getInterceptors();
+  }
+  public get(url: string, params: String, config: Object = {}) {
+    return new Promise((resolve, reject) => {
+      let paramUrl = url
+      if (params) {
+        paramUrl += `?${params}`
+      }
+      this.axios.get(paramUrl, {
+        ...config
+      }).then((res: any) => {
+        this.resultHandle(res, resolve, reject, url);
+      }).catch((err: { message: any; }) => {
+        console.log(err)
+        reject(err.message);
+      })
+    })
+  }
+
+  public getjson(url: string, params: Object = {}, config: Object = {}) {
+    return new Promise((resolve, reject) => {
+      let paramUrl = url
+      let paramStr = ''
+      for (const [key, value] of Object.entries(params)) {
+        if (value !== null && value !== undefined && value !== '') {
+          paramStr += `${key}=${value}&`
+        }
+      }
+      if (paramStr.length > 0) {
+        paramUrl += `?${paramStr.substring(0, paramStr.length - 1)}`
+      }
+      this.axios.get(paramUrl, {
+        ...config
+      }).then((res: any) => {
+        this.resultHandle(res, resolve, reject, url);
+      }).catch((err: { message: any; }) => {
+        reject(err.message);
+      })
+    })
+  }
+   public getjsontab(url: string, params: Object = {}, config: Object = {}) {
+    return new Promise((resolve, reject) => {
+      let paramUrl = url
+      let paramStr = ''
+      for (const [key, value] of Object.entries(params)) {
+          paramStr += `${key}=${value}&`
+      }
+      if (paramStr.length > 0) {
+        paramUrl += `?${paramStr.substring(0, paramStr.length - 1)}`
+      }
+      this.axios.get(paramUrl, {
+        ...config
+      }).then((res: any) => {
+        this.resultHandle(res, resolve, reject, url);
+      }).catch((err: { message: any; }) => {
+        reject(err.message);
+      })
+    })
+  }
+  public getid(url: string, params: String, config: Object = {}) {
+    return new Promise((resolve, reject) => {
+      let paramUrl = url
+      if (params) {
+        paramUrl += `${params}`
+      }
+      this.axios.get(paramUrl, {
+        ...config
+      }).then((res: any) => {
+        this.resultHandle(res, resolve, reject, url);
+      }).catch((err: { message: any; }) => {
+        reject(err.message);
+      })
+    })
+  }
+
+  public post(url: string, params: Object, config: Object = {}) {
+    return new Promise((resolve, reject) => {
+      this.axios.post(url, params, {
+        ...config   //  导出添加的下载类型
+      }).then((res: any) => {
+        this.resultHandle(res, resolve, reject, url);
+      }).catch((err: { message: any; }) => {
+        reject(err.message);
+      })
+    })
+  }
+
+  public put(url: string, params: Object, config: Object = {}) {
+    return new Promise((resolve, reject) => {
+      this.axios.put(url, params, {
+        ...config
+      }).then((res: any) => {
+        this.resultHandle(res, resolve, reject, url);
+      }).catch((err: { message: any; }) => {
+        reject(err.message);
+      })
+    })
+  }
+
+  public delete(url: string, params: Array<any>) {
+    return new Promise((resolve, reject) => {
+      let paramUrl = url + params.toString()
+      this.axios.delete(paramUrl, {
+      }).then((res: any) => {
+        this.resultHandle(res, resolve, reject, url);
+      }).catch((err: { message: any; }) => {
+        reject(err.message);
+      })
+    })
+  }
+
+  /**
+   *
+   * @param url   请求路径
+   * @param params  { method  //  封装的请求类型(getId,post,get...) , params{ fileName // 文件名 } // 请求参数   }
+   * @returns
+   */
+
+  public exportfile(url: string, params: any) {
+    console.log(JSON.parse(JSON.stringify(params)))
+    return new Promise((resolve, reject) => {
+      // @ts-ignore
+      this[params.method](url,params.params,{responseType: "blob",}).then((res:any)=>{
+        console.log(JSON.parse(JSON.stringify(res)))
+        if(res.code == 500) return reject(res.message)
+          let blob = new Blob([res], { type: "application/x-xlsx" });
+          let url = URL.createObjectURL(blob);
+          console.log(params)
+          this.downloadFile(url, params.params.fileName);
+          resolve('下载成功!')
+      }).catch((err: { message: any; }) => {
+        reject(err.message);
+      })
+    })
+  }
+
+  public exprotfl(url: String, params: Object) {
+    return new Promise((resolve, reject) => {
+      let paramUrl = url
+      if (params) {
+        paramUrl += `?${params}`
+      }
+      this.axios.get(paramUrl, {
+        responseType: "blob",
+      }).then((res: any) => {
+        let blob = new Blob([res], { type: "application/x-xlsx" });
+        let url = URL.createObjectURL(blob);
+        this.downloadFile(url, '船舶信息数据.xlsx');
+        this.resultHandle(res, resolve, reject, url);
+      }).catch((err: { message: any; }) => {
+        reject(err.message);
+      })
+    })
+  }
+  public examefl(url: String, params: Object,un:string,filName:string) {
+    return new Promise((resolve, reject) => {
+      let paramUrl = url
+      if (params) {
+        paramUrl += `?${params}`
+      }
+      this.axios.get(paramUrl, {
+        responseType: "blob",
+      }).then((res: any) => {
+        let blob = new Blob([res], { type: "application/x-xlsx" });
+        let url = URL.createObjectURL(blob);
+        this.downloadFile(url, `${filName}.xlsx`);
+        this.resultHandle(res, resolve, reject, url);
+      }).catch((err: { message: any; }) => {
+        reject(err.message);
+      })
+    })
+  }
+
+  public downloadFile(url: string, filename: string) {
+    const a = document.createElement('a')
+    a.href = url
+    if (filename) a.download = filename
+    a.click()
+  }
+
+  public resultHandle(res: any, resolve: { (value: unknown): void; (value: unknown): void; (arg0: any): void; },reject: { (value: unknown): void; (value: unknown): void; (arg0: any): void; }, url: string) {
+    if (res) {
+      if (res.code === 200 || (res.size > 0 && res.type)) {   //  增加blob文件判断
+        resolve(res);
+      } else {
+        this.errorHandle(res,reject, url);
+      }
+    }
+  }
+
+  public errorHandle(res: any, reject: any, url: string) {
+    console.error('错误接口:' + url)
+    if (res.hasOwnProperty('message')) {
+      ElMessage.warning(res.message);  // 统一谈服务端提示,我们提示统一由服务端提供
+    } else if (res.hasOwnProperty('msg')) {
+      ElMessage.warning(res.msg);  // 统一谈服务端提示,我们提示统一由服务端提供
+    }
+    // 状态码判断
+    if (res) {
+      switch (res.code) {
+        case -102:
+          break;
+        case -152:
+          break;
+        default:
+          reject(res);
+      }
+    }
+  }
+}
+
+export default HttpRequest

二进制
src/assets/images/cus/cus-tab_type2-active.png


二进制
src/assets/images/cus/cus-title_type2-icon.png


二进制
src/assets/images/cus/file-del.png


二进制
src/assets/images/cus/to-bottom.png


二进制
src/assets/images/cus/to-right.png


二进制
src/assets/images/file-type/file-type_default.png


二进制
src/assets/images/file-type/file-type_excel.png


二进制
src/assets/images/file-type/file-type_pdf.png


二进制
src/assets/images/file-type/file-type_ppt.png


二进制
src/assets/images/file-type/file-type_rar.png


二进制
src/assets/images/file-type/file-type_txt.png


二进制
src/assets/images/file-type/file-type_word.png


二进制
src/assets/images/global/login_bg.jpeg


二进制
src/assets/images/global/password.png


二进制
src/assets/images/global/temp.png


二进制
src/assets/images/global/user.png


二进制
src/assets/images/layout/background.png


二进制
src/assets/images/layout/background_home.png


二进制
src/assets/images/layout/head-bg.png


二进制
src/assets/images/layout/head-com-avatar.png


二进制
src/assets/images/layout/head-com-bg.png


二进制
src/assets/images/layout/head-com-dropdown.png


二进制
src/assets/images/layout/head-com-info-center.png


二进制
src/assets/images/layout/head-com-search.png


二进制
src/assets/images/layout/logo_icon.png


二进制
src/assets/images/layout/logo_name.png


二进制
src/assets/images/layout/menu-active-special.png


二进制
src/assets/images/layout/menu-bg-special.png


二进制
src/assets/images/layout/menu-item-bg-active.png


二进制
src/assets/images/layout/menu-item-bg.png


二进制
src/assets/images/layout/menu-top-title.png


二进制
src/assets/mock.png


文件差异内容过多而无法显示
+ 6 - 0
src/assets/svg/add.svg


文件差异内容过多而无法显示
+ 5 - 0
src/assets/svg/arrow_1.svg


文件差异内容过多而无法显示
+ 8 - 0
src/assets/svg/arrow_2.svg


+ 7 - 0
src/assets/svg/close_1.svg

@@ -0,0 +1,7 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+  "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg t="1692149440475" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1471"
+     xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200">
+  <path d="M250.785684 841.155368l589.473684-589.473684-67.907368-67.907368-589.473684 589.473684z"></path>
+  <path d="M772.358737 841.155368l67.907368-67.907368-589.473684-589.473684-67.907368 67.907368z"></path>
+</svg>

文件差异内容过多而无法显示
+ 8 - 0
src/assets/svg/close_2.svg


+ 7 - 0
src/assets/svg/cus-content_field-column-drag.svg

@@ -0,0 +1,7 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+  "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg t="1692757700168" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
+     p-id="17105" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128">
+  <path d="M204.8 341.333333h614.4v68.266667H204.8v-68.266667z m0 273.066667h614.4v68.266667H204.8v-68.266667z"
+        fill="#000000" p-id="17106"></path>
+</svg>

+ 7 - 0
src/assets/svg/cus-content_field-column-select.svg

@@ -0,0 +1,7 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+  "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg t="1692757700168" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
+     p-id="17105" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128">
+  <path d="M204.8 341.333333h614.4v68.266667H204.8v-68.266667z m0 273.066667h614.4v68.266667H204.8v-68.266667z"
+        p-id="17106"></path>
+</svg>

文件差异内容过多而无法显示
+ 8 - 0
src/assets/svg/cus-content_field-column.svg


文件差异内容过多而无法显示
+ 11 - 0
src/assets/svg/cus-content_field-fixed.svg


文件差异内容过多而无法显示
+ 11 - 0
src/assets/svg/cus-content_field-in.svg


文件差异内容过多而无法显示
+ 11 - 0
src/assets/svg/cus-content_tools.svg


+ 9 - 0
src/assets/svg/default.svg

@@ -0,0 +1,9 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+  "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg t="1692585246601" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
+     p-id="10280" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200">
+  <path
+    d="M512 958.8C265.6 958.8 65.2 758.4 65.2 512S265.6 65.2 512 65.2 958.8 265.6 958.8 512 758.4 958.8 512 958.8z m0-832c-212.4 0-385.2 172.8-385.2 385.2S299.6 897.2 512 897.2 897.2 724.4 897.2 512 724.4 126.8 512 126.8z"
+    p-id="10281"></path>
+  <path d="M512 512m-169.5 0a169.5 169.5 0 1 0 339 0 169.5 169.5 0 1 0-339 0Z" p-id="10282"></path>
+</svg>

文件差异内容过多而无法显示
+ 1 - 0
src/assets/svg/menu-mock.svg


+ 8 - 0
src/assets/svg/search.svg

@@ -0,0 +1,8 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+  "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg t="1692862188228" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1695"
+     xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200">
+  <path
+    d="M469.333333 85.333333a384 384 0 0 1 300.032 623.658667l139.306667 139.349333a42.666667 42.666667 0 1 1-60.330667 60.330667l-139.349333-139.306667A384 384 0 1 1 469.333333 85.333333z m0 85.333334a298.666667 298.666667 0 1 0 0 597.333333 298.666667 298.666667 0 0 0 0-597.333333z"
+    p-id="1696"></path>
+</svg>

文件差异内容过多而无法显示
+ 1 - 0
src/assets/svg/to_bottom_camera.svg


文件差异内容过多而无法显示
+ 1 - 0
src/assets/svg/to_right_camera.svg


文件差异内容过多而无法显示
+ 1 - 0
src/assets/svg/u212.svg


+ 53 - 0
src/components/SvgIcon/index.vue

@@ -0,0 +1,53 @@
+<template>
+  <svg aria-hidden="true" class="svg-icon" :style="`width: ${size}px;height: ${size}px;transform: rotate(${rotate}deg);`">
+    <use :xlink:href="symbolId" :fill="color" />
+  </svg>
+</template>
+
+
+<script lang="ts">
+  import {
+    defineComponent,
+    computed,
+  } from "vue";
+  export default defineComponent({
+    name: "SvgIcon",
+    components: {},
+    props: {
+      prefix: {
+        type: String,
+        default: 'icon'
+      },
+      name: {
+        type: String,
+        required: true
+      },
+      color: {
+        type: String,
+        default: ''
+      },
+      size: {
+        type: [Number, String],
+        default: '18'
+      },
+      rotate: {
+        type: [Number, String],
+        default: 0
+      }
+    },
+    setup(props, { emit }) {
+      const symbolId = computed(() => `#${props.prefix}-${props.name}`)
+      return {
+        symbolId
+      }
+    }
+  })
+</script>
+
+<style scoped>
+.svg-icon {
+  vertical-align: -0.15em;
+  overflow: hidden;
+  fill: currentColor;
+}
+</style>

+ 580 - 0
src/components/cus/CusContent.vue

@@ -0,0 +1,580 @@
+<template>
+  <div class="cus-content" :class="{'cus-content-full': full !== false}">
+    <div class="cc-top-slot" v-if="$slots.top">
+      <slot name="top"/>
+    </div>
+    <div class="cc-field-out" id="field-out" v-if="$slots.fieldOut">
+      <div class="field-out-slot cus-content-form">
+        <slot name="fieldOut"/>
+      </div>
+    </div>
+    <div class="cc-center-slot" v-if="$slots.center">
+      <slot name="center"/>
+    </div>
+    <div class="cc-bottom">
+      <div id="operation" class="operation">
+        <div class="buttons">
+          <slot name="buttons"/>
+        </div>
+        <div class="filters">
+          <el-dropdown :teleported="false">
+            <div class="tools __hover">
+              <SvgIcon name="cus-content_tools" size="18" color="#666666"/>
+            </div>
+            <template #dropdown>
+              <el-dropdown-menu>
+                <el-dropdown-item @click="showFieldIn = true" v-if="$slots.fieldIn" style="width: 170px;">
+                  <div class="tools-item">
+                    <SvgIcon name="cus-content_field-in" size="18"/>高级筛选
+                  </div>
+                </el-dropdown-item>
+                <el-dropdown-item @click="showFieldColumn = true">
+                  <div class="tools-item">
+                    <SvgIcon name="cus-content_field-column" size="18"/>字段筛选
+                  </div>
+                </el-dropdown-item>
+                <el-dropdown-item>
+                  <div class="tools-item" @click="showContentFixed = true">
+                    <SvgIcon name="cus-content_field-fixed" size="18"/>冻结窗格
+                  </div>
+                </el-dropdown-item>
+              </el-dropdown-menu>
+            </template>
+          </el-dropdown>
+        </div>
+      </div>
+      <div id="table" class="table">
+        <slot name="table"/>
+      </div>
+    </div>
+    <el-dialog
+        v-model="showFieldIn"
+        width="566px"
+        align-center
+        modal-class="field-dialog"
+        :close-on-click-modal="false"
+        :show-close="false"
+    >
+      <template #header>
+        <span>高级筛选设置</span>
+        <SvgIcon class="__hover" name="close_2" size="16" @click="showFieldIn = false"/>
+      </template>
+      <div class="field-in-slot">
+        <slot name="fieldIn"/>
+      </div>
+      <template #footer>
+        <div class="buttons">
+          <div class="cancel __hover" @click="showFieldIn = false">取消</div>
+          <div class="submit __hover" @click="$emit('handleSearch'), showFieldIn = false">确定</div>
+        </div>
+      </template>
+    </el-dialog>
+    <el-dialog
+        v-model="showFieldColumn"
+        width="790px"
+        align-center
+        modal-class="field-dialog"
+        :close-on-click-modal="false"
+        :show-close="false"
+    >
+      <template #header>
+        <span>字段筛选设置</span>
+        <SvgIcon class="__hover" name="close_2" size="16" @click="showFieldColumn = false"/>
+      </template>
+      <div class="field-column">
+        <div class="fc-block fc-all">
+          <div class="fcb-title">全部字段</div>
+          <div class="fcb-content">
+            <div class="fcbc-filter">
+              <el-input v-model="columnAllInputFilter" placeholder="请输入关键词..." clearable>
+                <template #suffix>
+                  <SvgIcon name="search"/>
+                </template>
+              </el-input>
+            </div>
+            <div class="fcbc-list">
+              <template v-for="(item, index) in tableHeadTempDeep.filter(v => v.allLabel.includes(columnAllInputFilter))" :key="item.allLabel">
+                <div class="table-head-item __hover" @click="item.show = !item.show">
+                  <div class="check" :class="{active: item.show}"/>
+                  <CusEllipsis class="label" :value="item.allLabel"/>
+                </div>
+              </template>
+            </div>
+            <div class="fcbc-bottom">
+              <div>共 {{ tableHeadTempDeep.length }} 条</div>
+              <div class="__hover" @click="handleCheckAllChange">
+                <div class="check" :class="{active: columnIsAllCpt}"/>全选
+              </div>
+            </div>
+          </div>
+        </div>
+        <div class="fc-block fc-select">
+          <div class="fcb-title">已选字段</div>
+          <div class="fcb-content">
+            <div class="fcbc-filter">
+              <el-input v-model="columnSelectInputFilter" placeholder="请输入关键词..." clearable>
+                <template #suffix>
+                  <SvgIcon name="search"/>
+                </template>
+              </el-input>
+            </div>
+            <div class="fcbc-list">
+              <template v-for="(item, index) in tableHeadTempDeep.filter(v => v.show && v.allLabel.includes(columnSelectInputFilter))" :key="item.allLabel">
+                <div class="table-head-select-item">
+                  <SvgIcon name="cus-content_field-column-select" size="12" color="#e4e4e4"/>
+                  <CusEllipsis class="label" :value="item.allLabel"/>
+                  <SvgIcon class="__hover" name="close_2" size="12" color="#e4e4e4" @click="item.show = false"/>
+                </div>
+              </template>
+            </div>
+            <div class="fcbc-bottom">
+              <div>已选 {{ tableHeadTempDeep.filter(v => v.show).length }} 条</div>
+              <div v-if="tableHeadTempDeep.filter(v => v.show).length > 0" @click="tableHeadTempDeep.forEach(v => v.show = false)" class="clear __hover">清空</div>
+            </div>
+          </div>
+        </div>
+      </div>
+      <template #footer>
+        <div class="buttons">
+          <div class="cancel __hover" @click="showFieldColumn = false">取消</div>
+          <div class="submit __hover" @click="$emit('update:tableHead', tableHeadTemp), showFieldColumn = false">确定</div>
+        </div>
+      </template>
+    </el-dialog>
+    <el-dialog
+        v-model="showContentFixed"
+        width="790px"
+        align-center
+        modal-class="field-dialog"
+        :close-on-click-modal="false"
+        :show-close="false"
+    >
+      <template #header>
+        <span>表格冻结</span>
+        <SvgIcon class="__hover" name="close_2" size="16" @click="showContentFixed = false"/>
+      </template>
+      <div class="content-fixed">
+        <div class="cf-item">
+          <el-checkbox v-model="tempContentFixed.full">内容</el-checkbox>
+        </div>
+        <div class="cf-item">
+          <el-checkbox v-model="tempContentFixed.leftFixed">表格左侧</el-checkbox>
+        </div>
+        <div class="cf-item">
+          <el-checkbox v-model="tempContentFixed.rightFixed">表格右侧</el-checkbox>
+        </div>
+      </div>
+      <template #footer>
+        <div class="buttons">
+          <div class="cancel __hover" @click="showContentFixed = false">取消</div>
+          <div class="submit __hover" @click="handleContentFixed">确定</div>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script lang="ts">
+  import {
+    defineComponent,
+    computed,
+    onMounted,
+    ref,
+    reactive,
+    watch,
+    getCurrentInstance,
+    ComponentInternalInstance,
+    toRefs
+  } from 'vue'
+  import {useStore} from 'vuex'
+  import {useRouter} from 'vue-router'
+
+  export default defineComponent({
+    name: 'CusContent',
+    components: {},
+    props: {
+      tableHead: {
+        required: true,
+        default: () => []
+      },
+      text: {},
+      showText: {
+        default: false
+      },
+      searchTip: {
+        default: '请输入查询关键词'
+      },
+      full: {
+        required: true,
+        default: false
+      }
+    },
+    setup(props, { emit }) {
+      const store = useStore();
+      const router = useRouter();
+      const that = (getCurrentInstance() as ComponentInternalInstance).appContext.config.globalProperties
+      const state = reactive({
+        textTemp: '',
+        showFieldIn: false,
+        showFieldColumn: false,
+        showContentFixed: false,
+        columnAllInputFilter: '',
+        columnSelectInputFilter: '',
+        tableHeadTemp: [],
+        contentFixed: {
+          full: props.full,
+          leftFixed: true,
+          rightFixed: true
+        },
+        tempContentFixed: {
+          full: props.full,
+          leftFixed: true,
+          rightFixed: true
+        }
+      });
+      watch(() => props.text, (nV: any) => {
+        state.textTemp = nV
+      })
+      watch(() => state.showFieldColumn, (n) => {
+        if (n) {
+          state.tableHeadTemp = JSON.parse(JSON.stringify(props.tableHead))
+        }
+      })
+      watch(() => state.showContentFixed, (n) => {
+        if (n) {
+          state.tempContentFixed = JSON.parse(JSON.stringify(state.contentFixed))
+        }
+      })
+      const tableHeadTempDeep = computed(() => {
+        const list: any = []
+        const deep = (arr: any[], pLabel: string = '') => {
+          arr.forEach(v => {
+            v.allLabel = pLabel ? `${pLabel}-${v.label}` : v.label
+            if (!v.tempFixed && v.fixed) {
+              v.tempFixed = JSON.parse(JSON.stringify(v.fixed))
+            }
+            if (v.children && v.children.length > 0) {
+              deep(v.children, v.allLabel)
+            } else {
+              list.push(v)
+            }
+          })
+        }
+        deep(state.tableHeadTemp)
+        return list
+      })
+
+      const columnIsAllCpt = computed(() => {
+        return tableHeadTempDeep.value.every(v => v.show)
+      })
+
+      const handleCheckAllChange = () => {
+        if (columnIsAllCpt.value) {
+          tableHeadTempDeep.value.forEach(v => {
+            v.show = false
+          })
+        } else {
+          tableHeadTempDeep.value.forEach(v => {
+            v.show = true
+          })
+        }
+      }
+      const handleContentFixed = () => {
+        state.contentFixed = JSON.parse(JSON.stringify(state.tempContentFixed))
+        tableHeadTempDeep.value.forEach((v: any) => {
+          if (v.tempFixed === 'left') {
+            v.fixed = state.contentFixed.leftFixed ? 'left' : false
+          } else if (v.tempFixed === 'right') {
+            v.fixed = state.contentFixed.rightFixed ? 'right' : false
+          }
+        })
+        emit('update:full', state.contentFixed.full)
+        emit('update:tableHead', state.tableHeadTemp)
+        state.showContentFixed = false
+      }
+      onMounted(() => {
+        state.tableHeadTemp = JSON.parse(JSON.stringify(props.tableHead))
+        state.tempContentFixed = JSON.parse(JSON.stringify(state.contentFixed))
+      })
+      return {
+        ...toRefs(state),
+        tableHeadTempDeep,
+        handleCheckAllChange,
+        columnIsAllCpt,
+        handleContentFixed
+      }
+    },
+  })
+</script>
+
+<style scoped lang="scss">
+  .cus-content {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+    background-color: #F5FBFF;
+    box-sizing: border-box;
+    .cc-top-slot {
+      margin-bottom: 10px;
+      background-color: #FFFFFF;
+    }
+    .cc-field-out {
+      margin-bottom: 10px;
+      background-color: #FFFFFF;
+      height: auto;
+      padding: 14px 24px 0 24px;
+      .field-out-slot {
+        flex: 1;
+        :deep(.el-form-item) {
+          margin-bottom: 12px;
+        }
+      }
+    }
+    .cc-center-slot {
+      margin-bottom: 10px;
+      background-color: #FFFFFF;
+    }
+    .cc-bottom {
+      background-color: #FFFFFF;
+      flex: 1;
+      z-index: 1;
+      display: flex;
+      flex-direction: column;
+      padding: 12px 16px;
+      .operation {
+        display: flex;
+        justify-content: space-between;
+        margin-bottom: 12px;
+        .buttons {
+          display: flex;
+          :deep(>*) {
+            margin-right: 10px;
+          }
+        }
+        .filters {
+          height: 36px;
+          :deep(.el-dropdown) {
+            .tools {
+              width: 36px;
+              height: 36px;
+              box-sizing: border-box;
+              border-radius: 4px;
+              border: 1px solid #E4E4E4;
+              display: flex;
+              align-items: center;
+              justify-content: center;
+            }
+            .el-dropdown-menu {
+              padding: 8px 4px;
+            }
+            .el-dropdown-menu__item {
+              padding: 0;
+              .tools-item {
+                padding: 0 8px;
+                display: flex;
+                align-items: center;
+                width: 170px;
+                height: 40px;
+                box-sizing: border-box;
+                .svg-icon {
+                  margin-right: 8px;
+                }
+              }
+            }
+          }
+        }
+      }
+      .table {
+        flex: 1;
+        z-index: 1;
+        display: flex;
+      }
+    }
+    :deep(.field-dialog) {
+      .el-overlay-dialog {
+        overflow: hidden;
+        .el-dialog {
+          border-radius: 8px;
+          .el-dialog__header {
+            padding: 30px 28px 38px 28px;
+            margin: 0;
+            font-size: 18px;
+            font-family: Microsoft YaHei;
+            font-weight: bold;
+            color: #333333;
+            display: flex;
+            justify-content: space-between;
+          }
+          .el-dialog__body {
+            padding: 0 28px;
+          }
+          .el-dialog__footer {
+            padding: 22px 28px 20px 28px;
+            .buttons {
+              width: 100%;
+              display: flex;
+              justify-content: flex-end;
+              >div {
+                display: flex;
+                align-items: center;
+                justify-content: center;
+                box-sizing: border-box;
+              }
+              .cancel {
+                margin-right: 10px;
+                width: 72px;
+                height: 36px;
+                background: #F8F8F8;
+                border: 1px solid #E4E4E4;
+                border-radius: 4px;
+                font-size: 14px;
+                font-family: Microsoft YaHei;
+                font-weight: 400;
+                color: #999999;
+              }
+              .submit {
+                width: 72px;
+                height: 36px;
+                background: #0062E9;
+                border-radius: 4px;
+                font-size: 14px;
+                font-family: Microsoft YaHei;
+                font-weight: 400;
+                color: #FFFFFF;
+              }
+            }
+          }
+        }
+      }
+    }
+    .field-column {
+      display: flex;
+      justify-content: space-between;
+      .fc-block {
+        .fcb-title {
+          font-size: 14px;
+          font-family: Microsoft YaHei;
+          font-weight: 400;
+          color: #666666;
+          margin-bottom: 18px;
+        }
+        .fcb-content {
+          height: 488px;
+          border: 1px solid #E4E4E4;
+          border-radius: 4px;
+          display: flex;
+          flex-direction: column;
+          .fcbc-filter {
+            padding: 10px 12px 0 12px;
+          }
+          .check {
+            width: 18px;
+            height: 18px;
+            box-sizing: border-box;
+            border: 1px solid #d3d3d3;
+            border-radius: 2px;
+            margin: 0 10px;
+            cursor: pointer;
+            font-size: 16px;
+            &.active {
+              background-color: #6587ff;
+              border-color: #6587ff;
+              display: grid;
+              place-items: center;
+              &:after {
+                content: '✔';
+                color: #ffffff;
+                font-size: 14px;
+              }
+            }
+            &:hover {
+              opacity: 0.8;
+            }
+          }
+          .fcbc-bottom {
+            margin-top: auto;
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+            height: 40px;
+            min-height: 40px;
+            background: #FFFFFF;
+            border-top: 1px solid #E4E4E4;
+            border-radius: 0px 0px 4px 4px;
+            padding: 0 10px 0 12px;
+            font-size: 14px;
+            font-family: Microsoft YaHei;
+            font-weight: 400;
+            color: #666666;
+            box-sizing: border-box;
+          }
+        }
+        &.fc-all {
+          width: 480px;
+          .fcbc-list {
+            display: grid;
+            grid-template-columns: repeat(2, 1fr);
+            padding: 20px 12px;
+            row-gap: 22px;
+            column-gap: 30px;
+            overflow-y: auto;
+            .table-head-item {
+              height: 18px;
+              display: flex;
+              align-items: center;
+              line-height: 1;
+              .label {
+                width: 164px;
+              }
+            }
+          }
+          .fcbc-bottom {
+            >div:nth-child(2) {
+              display: flex;
+              align-items: center;
+              .check:after {
+                transform: translateY(-2px);
+              }
+            }
+          }
+        }
+        &.fc-select {
+          width: 244px;
+          .fcbc-list {
+            overflow-y: auto;
+            display: grid;
+            row-gap: 26px;
+            padding: 20px 12px;
+            grid-template-columns: repeat(1, 1fr);
+            .table-head-select-item {
+              display: flex;
+              align-items: center;
+              .svg-icon:first-child {
+                margin-right: 10px;
+              }
+              .label {
+                width: 164px;
+              }
+              .svg-icon:last-child {
+                margin-left: auto;
+              }
+            }
+          }
+          .fcbc-bottom {
+            .clear {
+              font-size: 14px;
+              font-family: Microsoft YaHei;
+              font-weight: 400;
+              color: #0062E9;
+            }
+          }
+        }
+      }
+    }
+    &.cus-content-full {
+      overflow-y: auto;
+      position: absolute;
+    }
+  }
+</style>

+ 115 - 0
src/components/cus/CusContentCard.vue

@@ -0,0 +1,115 @@
+<template>
+  <div class="cus-content-card" :class="{'cus-content-card-full': full !== false}">
+    <div class="ccc-top-slot" v-if="$slots.top">
+      <slot name="top"/>
+    </div>
+    <div class="ccc-field-out" id="field-out" v-if="$slots.fieldOut">
+      <div class="field-out-slot">
+        <slot name="fieldOut"/>
+      </div>
+    </div>
+    <div class="ccc-center-slot" v-if="$slots.center">
+      <slot name="center"/>
+    </div>
+
+    <div class="ccc-content">
+      <div class="buttons" v-if="$slots.buttons">
+        <slot name="buttons"/>
+      </div>
+      <div class="table">
+        <slot name="table"/>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+  import {
+    defineComponent,
+    computed,
+    onMounted,
+    ref,
+    reactive,
+    watch,
+    getCurrentInstance,
+    ComponentInternalInstance,
+    toRefs
+  } from 'vue'
+  import {useStore} from 'vuex'
+  import {useRouter} from 'vue-router'
+
+  export default defineComponent({
+    name: 'CusContentCard',
+    components: {},
+    props: {
+      full: {
+        default: false
+      }
+    },
+    setup(props, { emit }) {
+      const store = useStore();
+      const router = useRouter();
+      const that = (getCurrentInstance() as ComponentInternalInstance).appContext.config.globalProperties
+      const state = reactive({
+      });
+      return {
+        ...toRefs(state),
+      }
+    },
+  })
+</script>
+
+<style scoped lang="scss">
+  .cus-content-card {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+    background-color: #F5FBFF;
+    box-sizing: border-box;
+    .ccc-top-slot {
+      margin-bottom: 10px;
+      background-color: #FFFFFF;
+    }
+    .ccc-field-out {
+      margin-bottom: 10px;
+      background-color: #FFFFFF;
+      height: auto;
+      padding: 14px 24px 0 24px;
+      .field-out-slot {
+        flex: 1;
+        :deep(.el-form-item) {
+          margin-bottom: 12px;
+        }
+      }
+    }
+    .cc-center-slot {
+      margin-bottom: 10px;
+      background-color: #FFFFFF;
+    }
+    .ccc-content {
+      background-color: #FFFFFF;
+      flex: 1;
+      z-index: 1;
+      display: flex;
+      flex-direction: column;
+      padding: 12px 16px;
+      overflow: hidden;
+      .buttons {
+        margin-bottom: 12px;
+      }
+      .table {
+        flex: 1;
+        z-index: 1;
+        display: flex;
+        overflow-y: auto;
+      }
+    }
+    &.cus-content-card-full {
+      overflow-y: auto;
+      .ccc-content {
+        overflow: unset;
+      }
+    }
+  }
+</style>

+ 156 - 0
src/components/cus/CusDialog.vue

@@ -0,0 +1,156 @@
+<template>
+  <el-dialog
+      :style="`
+        --cus-dialog_height: ${height};
+        --cus-dialog_max-height: ${maxHeight};
+        --cus-dialog_min-height: ${minHeight};
+      `"
+      :modal-class="`
+        __cus-dialog
+        ${maxHeight === 'unset' ? '' : '__cus-dialog-auto-height'}
+      `"
+      v-bind="$attrs"
+      v-model="showDialog"
+      :title="title"
+      :width="width"
+      align-center
+      :before-close="beforeClose"
+      :show-close="false"
+  >
+    <template #header>
+      <div class="_cus-dialog-head">
+        <div class="__cdh-title">{{title}}</div>
+        <div class="__cdh-slot">
+          <slot name="head"/>
+        </div>
+        <div class="__cdh-close __hover" @click="CDClose()">
+          <SvgIcon name="close_2" size="14"/>
+        </div>
+      </div>
+    </template>
+    <div class="__cus-dialog-main" :class="{isFull: full !== false}">
+      <div class="__cus-dialog-content">
+        <slot/>
+      </div>
+      <div class="__cus-dialog-foot" :style="`justify-content: ${footAlign};padding: ${footPadding};`">
+        <slot name="foot" :close="CDClose" :submit="CDSubmit"/>
+        <template v-if="footAlign === 'center'">
+          <div v-if="showSubmit" class="__cus-dialog-foot_submit __hover" @click="CDSubmit">确定</div>
+          <div v-if="showClose" class="__cus-dialog-foot_cancel __hover" @click="CDClose()">取消</div>
+        </template>
+        <template v-else>
+          <div v-if="showClose" class="__cus-dialog-foot_cancel __hover" @click="CDClose()">取消</div>
+          <div v-if="showSubmit" class="__cus-dialog-foot_submit __hover" @click="CDSubmit">确定</div>
+        </template>
+      </div>
+    </div>
+  </el-dialog>
+</template>
+
+<script lang="ts">
+import {
+  defineComponent,
+  computed,
+  onMounted,
+  ref,
+  reactive,
+  watch,
+  getCurrentInstance,
+  ComponentInternalInstance,
+  toRefs,
+  nextTick
+} from 'vue'
+import {useStore} from 'vuex'
+import {useRouter, useRoute} from 'vue-router'
+import {ElMessage, ElMessageBox} from "element-plus";
+
+export default defineComponent({
+  name: 'CusDialog',
+  components: {},
+  props: {
+    show: {required: true, type: Boolean},
+    title: {default: ''},
+    width: {default: '50%'},
+    full: {default: false},
+    submitText: {default: '确定'},
+    showClose: {default: true},
+    showSubmit: {default: true},
+    footAlign: {default: 'center', validator(val: string) {
+      return ['center', 'right'].includes(val)
+    }},
+    footPadding: {default: '16px 26px'},
+    height: {default: '60%'},
+    maxHeight: {default: 'unset'},
+    minHeight: {default: 'unset'},
+    closeConfirm: {default: false},
+    closeConfirmText: {default: {
+        title: null,
+        message: null,
+        submit: null,
+        close: null,
+      }}
+  },
+  setup(props, {emit}) {
+    const store = useStore();
+    const router = useRouter();
+    const route = useRoute();
+    const that = (getCurrentInstance() as ComponentInternalInstance).appContext.config.globalProperties
+    const state = reactive({
+      showDialog: false,
+      closeConfirmTextTemp: {
+        title: '提示',
+        message: '请确认是否关闭?',
+        submit: '确定',
+        close: '取消',
+      }
+    })
+    watch(() => props.show, (n) => {
+      state.showDialog = n
+    })
+    const beforeClose = (done) => {
+      CDClose(done)
+    }
+    const closeConfirmTextCpt: any = computed(() => {
+      return {
+        title: that.$util.isValue(props.closeConfirmText.title) ? props.closeConfirmText.title : state.closeConfirmTextTemp.title,
+        message: that.$util.isValue(props.closeConfirmText.message) ? props.closeConfirmText.message : state.closeConfirmTextTemp.message,
+        submit: that.$util.isValue(props.closeConfirmText.submit) ? props.closeConfirmText.submit : state.closeConfirmTextTemp.submit,
+        close: that.$util.isValue(props.closeConfirmText.close) ? props.closeConfirmText.close : state.closeConfirmTextTemp.close,
+      }
+    })
+    const CDClose = async (done = () => {}) => {
+      if (props.closeConfirm !== false) {
+        await ElMessageBox.confirm(closeConfirmTextCpt.value.message, closeConfirmTextCpt.value.title, {
+          confirmButtonText: closeConfirmTextCpt.value.submit,
+          cancelButtonText: closeConfirmTextCpt.value.close,
+          type: "warning",
+        }).then(() => {
+          emit('update:show', false)
+          emit('close')
+          done()
+        }).catch(() => {})
+      } else {
+        emit('update:show', false)
+        emit('close')
+        done()
+      }
+    }
+    const CDSubmit = () => {
+      emit('submit')
+    }
+    onMounted(() => {
+      state.showDialog = props.show
+    })
+    return {
+      ...toRefs(state),
+      beforeClose,
+      CDClose,
+      CDSubmit,
+    }
+  },
+})
+</script>
+
+<style scoped lang="scss">
+
+</style>

+ 67 - 0
src/components/cus/CusEllipsis.vue

@@ -0,0 +1,67 @@
+<template>
+  <div class="cus-ellipsis" ref="ref_main">
+    <div class="cus-ellipsis-temp" ref="ref_temp">{{value}}</div>
+    <el-tooltip
+        :disabled="!isEllipsis"
+        :content="value"
+        placement="top"
+    >
+      <div class="__text-ellipsis">{{value}}</div>
+    </el-tooltip>
+  </div>
+</template>
+
+<script lang="ts">
+import {
+  defineComponent,
+  computed,
+  onMounted,
+  ref,
+  reactive,
+  watch,
+  getCurrentInstance,
+  ComponentInternalInstance,
+  toRefs,
+  nextTick
+} from 'vue'
+import {useStore} from 'vuex'
+import {useRouter, useRoute} from 'vue-router'
+
+export default defineComponent({
+  name: 'CusEllipsis',
+  components: {},
+  props: {
+    value: { required: true, type: String }
+  },
+  setup() {
+    const store = useStore();
+    const router = useRouter();
+    const route = useRoute();
+    const that = (getCurrentInstance() as ComponentInternalInstance).appContext.config.globalProperties
+    const state = reactive({})
+    const ref_main = ref()
+    const ref_temp = ref()
+    const isEllipsis = computed(() => {
+      return ref_temp.value?.clientWidth > ref_main.value?.clientWidth
+    })
+    return {
+      ...toRefs(state),
+      isEllipsis,
+      ref_main,
+      ref_temp
+    }
+  },
+})
+</script>
+
+<style scoped lang="scss">
+  .cus-ellipsis {
+    width: 100%;
+    .cus-ellipsis-temp {
+      word-break: break-all;
+      white-space: nowrap;
+      position: absolute;
+      visibility: hidden;
+    }
+  }
+</style>

+ 86 - 0
src/components/cus/CusForm.vue

@@ -0,0 +1,86 @@
+<template>
+  <div class="cus-form">
+    <el-form ref="ref_cusForm" :label-width="labelWidth">
+      <el-row style="display:flex; flex-wrap: wrap">
+        <slot/>
+      </el-row>
+    </el-form>
+  </div>
+</template>
+
+<script lang="ts">
+import {
+  defineComponent,
+  computed,
+  onMounted,
+  ref,
+  reactive,
+  watch,
+  getCurrentInstance,
+  ComponentInternalInstance,
+  toRefs,
+  nextTick,
+  provide
+} from 'vue'
+import {useStore} from 'vuex'
+import {useRouter, useRoute} from 'vue-router'
+
+export default defineComponent({
+  name: 'CusForm',
+  components: {},
+  props: {
+    labelWidth: {default: '100px'},
+    elementLoadingBackground: {}
+  },
+  setup(props, { emit }) {
+    const store = useStore();
+    const router = useRouter();
+    const route = useRoute();
+    const that = (getCurrentInstance() as ComponentInternalInstance).appContext.config.globalProperties
+    const state = reactive({})
+    const ref_cusForm: any = ref(null)
+    provide('element-loading-background', props.elementLoadingBackground)
+    const submit = () => {
+      return new Promise((resolve, reject) => {
+        const nodes: any = [];
+        const handleItem = (itemVue: any) => {
+          const _itemValue = itemVue?.__vueParentComponent?.parent
+          if (_itemValue?.type.name === "CusFormColumn") {
+            const errorMessage = _itemValue.setupState.handleValidate()
+            if (errorMessage) {
+              nodes.push({
+                node: _itemValue,
+                message: errorMessage
+              });
+            }
+          } else {
+            if (itemVue.childNodes && itemVue.childNodes.length > 0) {
+              itemVue.childNodes.forEach((v: any) => {
+                handleItem(v);
+              });
+            }
+          }
+        };
+        handleItem(ref_cusForm?.value?.$el);
+        if (nodes.length > 0) {
+          reject(nodes);
+        } else {
+          resolve(null);
+        }
+      });
+    }
+    const handleEnter = () => {
+      emit('handleEnter')
+    }
+    return {
+      ...toRefs(state),
+      ref_cusForm,
+      submit,
+      handleEnter
+    }
+  },
+})
+</script>
+
+<style scoped lang="scss">
+</style>

+ 369 - 0
src/components/cus/CusFormColumn.vue

@@ -0,0 +1,369 @@
+<template>
+  <el-col class="cus-form-column" :class="{
+    span1: filterSpan == 1,
+    span2: filterSpan == 2,
+    span3: filterSpan == 3,
+    span4: filterSpan == 4,
+    span5: filterSpan == 5,
+  }" :span="span" :offset="offset" ref="ref_cusFormColumn">
+    <el-form-item :label="label" :label-width="labelWidth" :class="{ required: required !== false }"
+                  :error="errorMessage" :required="required">
+      <slot name="cus" :handleValidate="handleValidate">
+        <template v-if="link === 'input'">
+          <InputCom
+              v-if="link === 'input'"
+              v-bind="$attrs"
+              :label="label"
+              :param="param"
+              @emitParam="val => {
+                $emit('update:param', val), handleValidate(val)
+              }"
+              @emitEnter="handleEnter"
+          >
+            <template v-if="$slots.prefix" #prefix>
+              <slot name="prefix"/>
+            </template>
+            <template v-if="$slots.suffix" #suffix>
+              <slot name="suffix"/>
+            </template>
+            <template v-if="$slots.prepend" #prepend>
+              <slot name="prepend"/>
+            </template>
+            <template v-if="$slots.append" #append>
+              <slot name="append"/>
+            </template>
+          </InputCom>
+        </template>
+        <template v-else-if="link === 'select'">
+          <SelectCom
+              v-bind="$attrs"
+              :label="label"
+              :param="param"
+              @emitParam="val => {
+                $emit('update:param', val), handleValidate(val)
+              }"
+          >
+            <slot/>
+          </SelectCom>
+        </template>
+        <template v-else-if="link === 'date'">
+          <DateCom
+              v-bind="$attrs"
+              :label="label"
+              :param="param"
+              @emitParam="val => {
+                $emit('update:param', val), handleValidate(val)
+              }"
+          >
+            <template #default="{cell}">
+              <slot v-bind="cell"/>
+            </template>
+          </DateCom>
+        </template>
+        <template v-else-if="link === 'datetime'">
+          <DateTimeCom
+              v-bind="$attrs"
+              :label="label"
+              :param="param"
+              @emitParam="val => {
+                $emit('update:param', val), handleValidate(val)
+              }"
+          >
+            <template #default="{cell}">
+              <slot v-bind="cell"/>
+            </template>
+          </DateTimeCom>
+        </template>
+        <template v-else-if="link === 'time'">
+          <TimeCom
+              v-bind="$attrs"
+              :label="label"
+              :param="param"
+              @emitParam="val => {
+                $emit('update:param', val), handleValidate(val)
+              }"
+          >
+          </TimeCom>
+        </template>
+        <template v-else-if="link === 'cascader'">
+          <CascaderCom
+              v-bind="$attrs"
+              :label="label"
+              :param="param"
+              @emitParam="val => {
+                $emit('update:param', val), handleValidate(val)
+              }"
+          >
+            <template v-if="$slots.default" #default="{node, data}">
+              <slot v-bind="{node, data}"/>
+            </template>
+          </CascaderCom>
+        </template>
+        <template v-else-if="link === 'switch'">
+          <SwitchCom
+              v-bind="$attrs"
+              :label="label"
+              :param="param"
+              @emitParam="val => {
+                $emit('update:param', val), handleValidate(val)
+              }"
+          >
+          </SwitchCom>
+        </template>
+        <template v-else-if="link === 'radio'">
+          <RadioCom
+              v-bind="$attrs"
+              :label="label"
+              :param="param"
+              @emitParam="val => {
+                $emit('update:param', val), handleValidate(val)
+              }"
+          >
+          </RadioCom>
+        </template>
+        <template v-else-if="link === 'checkbox'">
+          <CheckboxCom
+              v-bind="$attrs"
+              :label="label"
+              :param="param"
+              @emitParam="val => {
+                $emit('update:param', val), handleValidate(val)
+              }"
+          >
+          </CheckboxCom>
+        </template>
+        <template v-else-if="link === 'portOfRegistry'">
+          <PortOfRegistryCom
+              v-bind="$attrs"
+              :label="label"
+              :paramOne="paramOne"
+              :paramTwo="paramTwo"
+              @emitParam="(pOne, pTwo) => {
+                $emit('update:paramOne', pOne), $emit('update:paramTwo', pTwo), handleValidate(pOne, pTwo)
+              }"
+          />
+        </template>
+        <template v-else-if="link === 'residentMooringPoint'">
+          <ResidentMooringPointCom
+              v-bind="$attrs"
+              :label="label"
+              :paramOne="paramOne"
+              :paramTwo="paramTwo"
+              @emitParam="(pOne, pTwo) => {
+                $emit('update:paramOne', pOne), $emit('update:paramTwo', pTwo), handleValidate(pOne, pTwo)
+              }"
+          />
+        </template>
+        <template v-else-if="link === 'dept'">
+          <DeptCom
+              v-bind="$attrs"
+              :label="label"
+              :param="param"
+              @emitParam="val => {
+                $emit('update:param', val), handleValidate(val)
+              }"
+              @emitDefault="val => $emit('getDefault', val)"
+          />
+        </template>
+        <template v-else-if="link === 'upload'">
+          <UploadCom
+              v-bind="$attrs"
+              :label="label"
+              :param="param"
+              @emitParam="val => {
+                $emit('update:param', val), handleValidate(val)
+              }"
+          />
+        </template>
+      </slot>
+    </el-form-item>
+  </el-col>
+</template>
+
+<script lang="ts">
+import {
+  defineComponent,
+  computed,
+  onMounted,
+  ref,
+  reactive,
+  watch,
+  getCurrentInstance,
+  ComponentInternalInstance,
+  toRefs,
+  nextTick
+} from 'vue'
+import {useStore} from 'vuex'
+import {useRouter, useRoute} from 'vue-router'
+import InputCom from './cus-form-link/input.vue'
+import SelectCom from './cus-form-link/select.vue'
+import DateCom from './cus-form-link/date.vue'
+import DateTimeCom from './cus-form-link/datetime.vue'
+import TimeCom from './cus-form-link/time.vue'
+import CascaderCom from './cus-form-link/cascader.vue'
+import SwitchCom from './cus-form-link/switch.vue'
+import RadioCom from './cus-form-link/radio.vue'
+import CheckboxCom from './cus-form-link/checkbox.vue'
+import PortOfRegistryCom from './cus-form-link/portOfRegistry.vue'
+import ResidentMooringPointCom from './cus-form-link/residentMooringPoint.vue'
+import DeptCom from './cus-form-link/dept.vue'
+import UploadCom from './cus-form-link/upload.vue'
+
+export default defineComponent({
+  name: 'CusFormColumn',
+  components: {
+    InputCom,
+    SelectCom,
+    DateCom,
+    DateTimeCom,
+    TimeCom,
+    CascaderCom,
+    SwitchCom,
+    RadioCom,
+    CheckboxCom,
+    PortOfRegistryCom,
+    ResidentMooringPointCom,
+    DeptCom,
+    UploadCom,
+  },
+  props: {
+    span: {type: Number, default: 6},
+    filterSpan: { default: null },
+    offset: {type: Number, default: 0},
+    param: {
+      required: true
+    },
+    paramOne: {},
+    paramTwo: {},
+    label: {type: String, default: ''},
+    required: {default: false},
+    labelWidth: {type: String, default: ''},
+    link: {type: String, default: 'input', validator(val: string) {
+      return ['cascader', 'checkbox', 'date', 'datetime', 'input', 'radio', 'select', 'switch', 'portOfRegistry', 'residentMooringPoint', 'dept', 'time', 'upload'].includes(val)
+    }},
+    rules: {type: Array, default: () => []},
+    maxLength: {type: Number, default: null},
+    minLength: {type: Number, default: null},
+    defaultErrorMsg: {default: null}
+  },
+  setup(props) {
+    const store = useStore();
+    const router = useRouter();
+    const route = useRoute();
+    const that = (getCurrentInstance() as ComponentInternalInstance).appContext.config.globalProperties
+    const state = reactive({
+      errorMessage: null
+    })
+    const ref_cusFormColumn: any = ref(null)
+    const isValue = (val: any) => {
+      if (val === null || val === undefined || (typeof val === 'string' && val.trim() === '') || (typeof val === 'object' && val?.length === 0)) {
+        return false
+      }
+      return true
+    }
+    const rulesCpt = computed(() => {
+      const r = [...props.rules]
+      if (isValue(props.minLength)) {
+        r.unshift({
+          handle: (val: any) => !isValue(val) || (isValue(val) && String(val).length >= props.minLength),
+          message: `内容过短,字数需大于等于${props.minLength}`
+        })
+      }
+      if (isValue(props.maxLength)) {
+        r.unshift({
+          handle: (val: any) => !isValue(val) || (isValue(val) && String(val).length <= props.maxLength),
+          message: `内容过长,字数需小于等于${props.maxLength}`
+        })
+      }
+      const doStr = ['input'].includes(props.link) ? '输入' : '选择'
+      if (props.required !== false && !props.rules.some((v: any) => v.type === 'default')) {
+        if (['portOfRegistry', 'residentMooringPoint'].includes(props.link)) {
+          r.unshift({
+            handle: (pOne: any, pTwo: any) => isValue(pOne) && isValue(pTwo),
+            message: props.defaultErrorMsg ?? `请${doStr}${props.label}`
+          })
+        } else {
+          r.unshift({
+            handle: (val: any) => isValue(val),
+            message: props.defaultErrorMsg ?? `请${doStr}${props.label}`
+          })
+        }
+      }
+      return r
+    })
+    const handleValidate = (val: any = undefined, val2: any = undefined) => {
+      state.errorMessage = null
+      for (let i = 0; i < rulesCpt.value.length; i++) {
+        const item: any = rulesCpt.value[i]
+        if (['portOfRegistry', 'residentMooringPoint'].includes(props.link)) {
+          if (!item.handle(val === undefined ? props.paramOne : val, val2 === undefined ? props.paramTwo : val2)) {
+            state.errorMessage = item.message
+            break
+          }
+        } else {
+          if (!item.handle(val === undefined ? props.param : val)) {
+            state.errorMessage = item.message
+            break
+          }
+        }
+      }
+      return state.errorMessage
+    }
+    const handleEnter = () => {
+      const find = (itemVue: any) => {
+        if (itemVue?.type.name === "CusForm") {
+          itemVue.setupState.handleEnter()
+        } else {
+          find(itemVue.parent)
+        }
+      }
+      find(ref_cusFormColumn?.value?.$el?.__vueParentComponent)
+    }
+    return {
+      ...toRefs(state),
+      handleValidate,
+      handleEnter,
+      ref_cusFormColumn,
+    }
+  },
+})
+</script>
+
+<style scoped lang="scss">
+.cus-form-column {
+  &.span1 {
+    width: 20%;
+    max-width: 20%;
+    flex: unset;
+  }
+  &.span2 {
+    width: 40%;
+    max-width: 40%;
+    flex: unset;
+  }
+  &.span3 {
+    width: 60%;
+    max-width: 60%;
+    flex: unset;
+  }
+  &.span4 {
+    width: 80%;
+    max-width: 80%;
+    flex: unset;
+  }
+  &.span5 {
+    width: 100%;
+    max-width: 100%;
+    flex: unset;
+  }
+  :deep(.el-form-item) {
+    .el-form-item__label {
+      line-height: 1;
+      text-align: right;
+      display: flex;
+      align-items: center;
+      padding-left: 10px;
+    }
+  }
+}
+</style>

+ 92 - 0
src/components/cus/CusPopover.vue

@@ -0,0 +1,92 @@
+<template>
+  <el-popover
+      placement="top-start"
+      trigger="hover"
+      :content="value"
+      popper-class="cus-popover"
+  >
+    <template #reference>
+      <slot/>
+    </template>
+  </el-popover>
+</template>
+
+<script lang="ts">
+import {
+  defineComponent,
+  computed,
+  onMounted,
+  ref,
+  reactive,
+  watch,
+  getCurrentInstance,
+  nextTick,
+  ComponentInternalInstance, toRefs
+} from 'vue'
+  import {useStore} from 'vuex'
+  import {useRouter} from 'vue-router'
+
+  export default defineComponent({
+    name: 'CusPopover',
+    props: {
+      value: {
+        required: true
+      },
+    },
+    setup(props, { emit }) {
+      const store = useStore();
+      const router = useRouter();
+      const that = (getCurrentInstance() as ComponentInternalInstance).appContext.config.globalProperties
+    },
+  })
+</script>
+
+<style scoped lang="scss">
+  .cus-table-main {
+    width: 100%;
+    height: 100%;
+    $cus-page-height: 50px;
+    :deep(.cus-table) {
+      width: 100%;
+      //height: calc(100% - #{$cus-page-height} - 20px);
+      .el-checkbox__inner {
+        width: 24px;
+        height: 24px;
+        &:after {
+          width: 6px;
+          height: 14px;
+          left: 8px;
+        }
+      }
+      .el-table__header-wrapper, .el-table__fixed-right, .el-table__fixed {
+        .cell {
+          font-weight: 700;
+          font-size: 16px;
+          color: #333333;
+          height: 26px;
+        }
+        th {
+          background-color: #fafafa;
+        }
+      }
+      .el-table__body-wrapper, .el-table__fixed-body-wrapper {
+        .cell {
+          height: 26px;
+          font-size: 16px;
+          font-family: 微软雅黑;
+          font-weight: 400;
+          color: #797979;
+        }
+      }
+    }
+    .cus-table-page {
+      display: flex;
+      justify-content: flex-end;
+      align-items: flex-end;
+      height: $cus-page-height;
+      margin: 0;
+      padding: 0;
+    }
+  }
+
+</style>

+ 86 - 0
src/components/cus/CusSearchButtons.vue

@@ -0,0 +1,86 @@
+<template>
+  <div class="search-buttons">
+    <div class="search __hover" @click="$emit('handleSearch')">查 询</div>
+    <div class="reset __hover" @click="$emit('handleReset')">重 置</div>
+    <div class="expand __hover" v-if="expandValue !== null" @click="$emit('update:expandValue', !expandValue)">
+      {{expandValue ? '收起' : '展开'}}
+      <SvgIcon name="arrow_2" size="14" :rotate="expandValue ? 270 : 90"/>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import {
+  defineComponent,
+  computed,
+  onMounted,
+  ref,
+  reactive,
+  watch,
+  getCurrentInstance,
+  nextTick,
+  ComponentInternalInstance, toRefs
+} from 'vue'
+  import {useStore} from 'vuex'
+  import {useRouter} from 'vue-router'
+
+  export default defineComponent({
+    name: 'CusSearchButtons',
+    props: {
+      expandValue: {
+        default: null
+      }
+    },
+    setup(props, { emit }) {
+      const store = useStore();
+      const router = useRouter();
+      const that = (getCurrentInstance() as ComponentInternalInstance).appContext.config.globalProperties
+      return {
+      }
+    },
+  })
+</script>
+
+<style scoped lang="scss">
+  .search-buttons {
+    margin-left: auto;
+    display: flex;
+    align-items: flex-start;
+    justify-content: flex-end;
+    margin-bottom: 12px;
+    >div {
+      margin-right: 12px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      font-size: 14px;
+      font-family: Microsoft YaHei;
+      font-weight: 400;
+      box-sizing: border-box;
+      &:last-child {
+        margin-right: 0;
+      }
+    }
+    .search {
+      width: 74px;
+      height: 32px;
+      background: #0062E9;
+      border-radius: 4px;
+      color: #FFFFFF;
+    }
+    .reset {
+      width: 74px;
+      height: 32px;
+      border: 1px solid #0062E9;
+      border-radius: 4px;
+      color: #3070CF;
+    }
+    .expand {
+      color: #0062E9;
+      margin-top: 8px;
+      .svg-icon {
+        margin-left: 9px;
+      }
+    }
+  }
+</style>

+ 122 - 0
src/components/cus/CusTab.vue

@@ -0,0 +1,122 @@
+<template>
+  <div class="cus-tab" :class="`cus-tab-${type}`">
+    <template v-for="item in tabs">
+      <div class="cus-tab-item __hover" :class="{active: item.value === param}" @click="$emit('update:param', item.value)">{{item.name}}</div>
+    </template>
+    <div class="cus-tab-slot">
+      <slot/>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import {
+  defineComponent,
+  computed,
+  onMounted,
+  ref,
+  reactive,
+  watch,
+  getCurrentInstance,
+  ComponentInternalInstance,
+  toRefs,
+  nextTick
+} from 'vue'
+import {useStore} from 'vuex'
+import {useRouter, useRoute} from 'vue-router'
+
+export default defineComponent({
+  name: 'CusTab',
+  components: {},
+  props: {
+    tabs: {
+      required: true
+    },
+    param: {
+      required: true
+    },
+    type: {default: 'type1', validator(val: string) {
+      return ['type1', 'type2'].includes(val)
+    }},
+  },
+  setup(props, {emit}) {
+    const store = useStore();
+    const router = useRouter();
+    const route = useRoute();
+    const that = (getCurrentInstance() as ComponentInternalInstance).appContext.config.globalProperties
+    const state = reactive({})
+    return {
+      ...toRefs(state)
+    }
+  },
+})
+</script>
+
+<style scoped lang="scss">
+  .cus-tab {
+    height: 40px;
+    width: 100%;
+    border-bottom: 1px solid #DCDFE5;;
+    display: flex;
+    align-items: center;
+    box-sizing: border-box;
+    .cus-tab-item {
+      height: 100%;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      position: relative;
+      font-size: 14px;
+      font-family: PingFang SC-Regular, PingFang SC;
+      font-weight: 400;
+      color: #606266;
+      margin-right: 16px;
+      &:last-child {
+        margin-right: 0;
+      }
+      &.active {
+        &:after {
+          content: '';
+          position: absolute;
+          bottom: 0;
+        }
+      }
+    }
+    &.cus-tab-type1 {
+      .cus-tab-item {
+        padding: 0 4px;
+        &.active {
+          color: #0062E9;
+          &:after {
+            width: 100%;
+            height: 2px;
+            bottom: -1px;
+            background-color: #0062E9;
+          }
+        }
+      }
+    }
+    &.cus-tab-type2 {
+      .cus-tab-item {
+        padding: 0 4px;
+        min-width: 77px;
+        margin-right: 6px;
+        &.active {
+          font-weight: 500;
+          color: #0062E9;
+          &:after {
+            width: 100%;
+            height: 5px;
+            bottom: -3px;
+            background-image: url("@/assets/images/cus/cus-tab_type2-active.png");
+            background-repeat: no-repeat;
+            background-size: 100% 100%;
+          }
+        }
+      }
+    }
+    .cus-tab-slot {
+      margin-left: auto;
+    }
+  }
+</style>

+ 395 - 0
src/components/cus/CusTable.vue

@@ -0,0 +1,395 @@
+<template>
+  <div class="cus-table" :class="{'cus-table-normal': !otherStyle, 'cus-table-full': full !== false}">
+    <div class="cus-table-main" ref="ref_tableMain">
+      <el-table
+          v-bind="$attrs"
+          class="ct-table"
+          ref="ref_table"
+          :data="tableData"
+          :border="false"
+          @sort-change="handleSort"
+          @filter-change="handleFilter"
+          @selection-change="v => $emit('update:selected', v)"
+      >
+        <el-table-column v-if="selected" type="selection" :width="selectWidth" align="center" width="52" :selectable="selectable"/>
+        <el-table-column v-if="singled" :width="singledWidth" align="center" width="52" class-name="single-column">
+          <template #default="scope">
+            <div class="single-circle" :class="{active: scope.row[singledKey] === singled[singledKey]}" @click.stop="$emit('update:singled', scope.row)">
+              <div class="single-circle-in"></div>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column  v-if="showIndex" type="index" label="序号" align="center" fixed="left" width="60"/>
+        <template v-for="(item, index) in tempTableHead" :key="index">
+          <template v-if="item.children && item.children.length > 0">
+            <el-table-column :label="item.label" v-if="item.show" align="center">
+              <template v-for="(item2, index2) in item.children">
+                <template v-if="item2.children && item.children.length > 0">
+                  <el-table-column :label="item2.label" v-if="item2.show" align="center">
+                    <template v-for="(item3, index3) in item2.children">
+                      <CusTableColumn :item="item3">
+                        <template #[`${item3.value}-column`]="{column}">
+                          <slot :name="`${item3.value}-column`" :column="column">
+                          </slot>
+                        </template>
+                        <template #[`${item3.value}-column-value`]="{scope}">
+                          <slot :name="`${item3.value}-column-value`" :scope="scope">
+                          </slot>
+                        </template>
+                      </CusTableColumn>
+                    </template>
+                  </el-table-column>
+                </template>
+                <template v-else>
+                  <CusTableColumn :item="item2">
+                    <template #[`${item2.value}-column`]="{column}">
+                      <slot :name="`${item2.value}-column`" :column="column">
+                      </slot>
+                    </template>
+                    <template #[`${item2.value}-column-value`]="{scope}">
+                      <slot :name="`${item2.value}-column-value`" :scope="scope">
+                      </slot>
+                    </template>
+                  </CusTableColumn>
+                </template>
+              </template>
+            </el-table-column>
+          </template>
+          <template v-else>
+            <CusTableColumn :item="item">
+              <template #[`${item.value}-column`]="{column}">
+                <slot :name="`${item.value}-column`" :column="column">
+                </slot>
+              </template>
+              <template #[`${item.value}-column-value`]="{scope}">
+                <slot :name="`${item.value}-column-value`" :scope="scope">
+                </slot>
+              </template>
+            </CusTableColumn>
+          </template>
+        </template>
+      </el-table>
+    </div>
+    <div class="ct-page">
+      <div class="total">
+        <template v-if="$util.isValue(selected?.length)">
+          已选中 {{ selected.length }} 条 /
+        </template>
+        共 {{ total }} 条
+      </div>
+      <el-pagination
+          v-if="noPage === false"
+          ref="ref_tablePage"
+          class="__cus-pagination"
+          :currentPage="page"
+          :page-size="pageSize"
+          background
+          :page-sizes="pageSizes"
+          :layout="pageLayoutCpt"
+          :total="Number(total)"
+          @size-change="handleSizeChange"
+          @current-change="handleCurrentChange"
+      />
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import {
+  defineComponent,
+  computed,
+  onMounted,
+  ref,
+  reactive,
+  watch,
+  getCurrentInstance,
+  nextTick,
+  ComponentInternalInstance, toRefs
+} from 'vue'
+  import {useStore} from 'vuex'
+  import {useRouter} from 'vue-router'
+
+  export default defineComponent({
+    name: 'CusTable',
+    props: {
+      tableData: {
+        required: true
+      },
+      tableHead: {
+        default: () => [],
+        required: true
+      },
+      total: {
+        required: true
+      },
+      page: {},
+      pageSize: {},
+      pageSizes: {
+        default: () => [10, 20, 30, 50, 100]
+      },
+      selected: {
+        default: null
+      },
+      selectWidth: {
+        default: 50
+      },
+      singledWidth: {
+        default: 50
+      },
+      singled: {
+        default: null
+      },
+      singledKey: {
+        default: 'id'
+      },
+      noPage: {
+        default: false
+      },
+      showIndex: {
+        default: true
+      },
+      otherStyle: {
+        default: false
+      },
+      noLayout: {
+        default: () => []
+      },
+      full: {
+        default: false
+      },
+      selectable: {}
+    },
+    setup(props, { emit }) {
+      const store = useStore();
+      const router = useRouter();
+      const that = (getCurrentInstance() as ComponentInternalInstance).appContext.config.globalProperties
+      const ref_table = ref();
+      const ref_tableMain = ref();
+      const ref_tablePage = ref();
+      const state = reactive({
+        tempTableHead: <any>[],
+        pageLayout: ['sizes', 'prev', 'pager', 'next', 'jumper']
+      });
+      watch(() => props.tableHead, (nVal: any) => {
+        formatTableHead(nVal)
+      }, {deep: true})
+      // watch(() => props.tableData, (nVal) => {
+      //   ref_table.value?.doLayout()
+      // }, {deep: true})
+      const pageLayoutCpt = computed(() => {
+        return state.pageLayout.filter((v: string) => props.noLayout.every((s: string) => v !== s)).join(',')
+      })
+
+      const formatTableHead = (tableHead: Array<any>) => {
+        const result = tableHead
+        const deep = (arr: any[]) => {
+          let flag = 0
+          arr.forEach(v => {
+            if (v.children && v.children.length > 0) {
+              v.show = deep(v.children)
+            }
+            if (v.show) {
+              flag++
+            }
+          })
+          return flag > 0
+        }
+        deep(result)
+        state.tempTableHead = result
+      }
+      const handleSizeChange = (val: Number) => {
+        emit('handlePage', {
+          page: props.page,
+          pageSize: val
+        })
+      }
+      const handleCurrentChange = (val: Number) => {
+        emit('handlePage', {
+          page: val,
+          pageSize: props.pageSize
+        })
+      }
+      const handleSort = ({ column, prop, order }: any) => {
+        emit('handleSort', {
+          key: prop,
+          value: order
+        })
+      }
+      const handleFilter = (val: any) => {
+        const key = Object.keys(val)[0]
+        let value = val[key].length > 0 ? val[key][0] : null
+        emit('handleFilter', {key, value})
+      }
+      const resetFilter = (key: any) => {
+        key ? ref_table.value.clearFilter([key]) : ref_table.value.clearFilter()
+      }
+      onMounted(() => {
+        formatTableHead(props.tableHead)
+      })
+      return {
+        handleSizeChange,
+        handleCurrentChange,
+        handleSort,
+        resetFilter,
+        handleFilter,
+        ref_table,
+        ref_tableMain,
+        ref_tablePage,
+        ...toRefs(state),
+        pageLayoutCpt
+      }
+    },
+  })
+</script>
+
+<style scoped lang="scss">
+.cus-table {
+  width: 100%;
+  height: 100%;
+  max-height: 100%;
+  display: flex;
+  flex-direction: column;
+  $cus-page-height: 32px;
+  $cus-page-mt: 25px;
+  position: relative;
+  .cus-table-main {
+    width: 100%;
+    height: calc(100% - #{$cus-page-height} - #{$cus-page-mt}) !important;
+    position: absolute;
+    :deep(.ct-table) {
+      height: 100%;
+    }
+  }
+  :deep(.ct-page) {
+    height: $cus-page-height;
+    font-size: 14px;
+    font-family: Microsoft YaHei;
+    font-weight: 400;
+    color: #999999;
+    display: flex;
+    justify-content: space-between;
+    .total {
+      display: flex;
+      align-items: center;
+    }
+  }
+  &.cus-table-normal {
+    .cus-table-main {
+      :deep(.ct-table) {
+        width: 100%;
+        //height: calc(100% - #{$cus-page-height} - 20px);
+        .el-checkbox__inner {
+          &:after {
+          }
+        }
+        $borderColor: #FFFFFF;
+        $borderWidth: 2px;
+        //  表格左边框
+        &::before, .el-table__border-left-patch {
+          display: none;
+        }
+        //  表格上边框
+        .el-table__inner-wrapper::after {
+          display: none;
+        }
+        //  表格右边框
+        &.el-table--border::after {
+          display: none;
+        }
+        //  表格下边框
+        .el-table__inner-wrapper::before {
+          display: none;
+        }
+        .el-table__header-wrapper, .el-table__fixed-right, .el-table__fixed {
+          .el-table__header {
+            tr {
+              >th {
+                border-left: $borderWidth solid $borderColor;
+                border-top: $borderWidth solid $borderColor;
+                border-right: none;
+                border-bottom: none;
+                background-color: #F5F5F5;
+                height: 52px;
+                .cell {
+                  font-size: 14px;
+                  font-family: 微软雅黑;
+                  font-weight: 400;
+                  color: #666666;
+                }
+              }
+              &:first-child {
+                >th {
+                  border-top: none;
+                  &:first-child {
+                    border-left: none;
+                  }
+                }
+              }
+            }
+          }
+        }
+        .el-table__body-wrapper, .el-table__fixed-body-wrapper {
+          .el-table__body {
+            .el-table__row {
+              >td {
+                border-right: none;
+                border-bottom-color: #F4F4F4;
+                .cell {
+                  font-size: 14px;
+                  font-family: 微软雅黑;
+                  font-weight: 400;
+                  color: #666666;
+                  white-space: nowrap;
+                  overflow: hidden;
+                  text-overflow: ellipsis;
+                  word-break: break-all;
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+    :deep(.ct-page) {
+      margin-top: auto;
+    }
+  }
+  &.cus-table-full {
+    .cus-table-main {
+      position: unset;
+      height: 100% !important;
+    }
+    :deep(.ct-page) {
+      margin-top: $cus-page-mt;
+    }
+  }
+}
+
+:deep(.single-column) {
+  .cell {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    .single-circle {
+      width: 16px;
+      height: 16px;
+      border: 1px solid rgba(96, 98, 102, 0.6);
+      border-radius: 50%;
+      cursor: pointer;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      &.active {
+        background-color: #22a5fe;
+        border-color: #22a5fe;
+      }
+      .single-circle-in {
+        width: 6px;
+        height: 6px;
+        border-radius: 50%;
+        background-color: #FFFFFF;
+      }
+    }
+  }
+}
+</style>

+ 156 - 0
src/components/cus/CusTableCard.vue

@@ -0,0 +1,156 @@
+<template>
+  <div class="cus-table-card-main" :class="{'cus-table-card-full': full !== false}">
+    <div class="cus-table-card-content">
+      <div class="cus-table-card-content-list">
+        <template v-for="(item, index) in data">
+          <div class="model" :style="`margin-left: ${index % col === 0 ? '0px' : colMargin};margin-bottom: ${index > ((data.length / col - 1) * col) - 1 ? '0px' : rowMargin};`">
+            <slot name="model" :info="item"/>
+          </div>
+        </template>
+      </div>
+    </div>
+    <div class="ctc-page">
+      <div class="total">
+        <template v-if="$util.isValue(selectLength)">
+          已选中 {{ selectLength }} 条 /
+        </template>
+        共 {{ total }} 条
+      </div>
+      <el-pagination
+          v-if="noPage === false"
+          ref="ref_tablePage"
+          class="__cus-pagination"
+          :currentPage="page"
+          :page-size="pageSize"
+          background
+          :page-sizes="pageSizes"
+          :layout="pageLayoutCpt"
+          :total="Number(total)"
+          @size-change="handleSizeChange"
+          @current-change="handleCurrentChange"
+      />
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import {
+  defineComponent,
+  computed,
+  onMounted,
+  ref,
+  reactive,
+  watch,
+  getCurrentInstance,
+  nextTick,
+  ComponentInternalInstance, toRefs
+} from 'vue'
+  import {useStore} from 'vuex'
+  import {useRouter} from 'vue-router'
+
+  export default defineComponent({
+    name: 'CusTableCard',
+    props: {
+      total: {
+        required: true
+      },
+      page: {},
+      pageSize: {},
+      pageSizes: {
+        default: () => [10, 20, 30, 50, 100]
+      },
+      noPage: {
+        default: false
+      },
+      col: {
+        default: 2
+      },
+      colMargin: {
+        default: '20px'
+      },
+      rowMargin: {
+        default: '20px'
+      },
+      data: {
+        required: true,
+        default: () => []
+      },
+      noLayout: {
+        default: () => []
+      },
+      full: {
+        default: false
+      },
+      selectLength: {
+        default: null
+      },
+    },
+    setup(props, { emit }) {
+      const store = useStore();
+      const router = useRouter();
+      const that = (getCurrentInstance() as ComponentInternalInstance).appContext.config.globalProperties
+      const state = reactive({
+        pageLayout: ['sizes', 'prev', 'pager', 'next', 'jumper']
+      });
+      const pageLayoutCpt = computed(() => {
+        return state.pageLayout.filter((v: string) => props.noLayout.every((s: string) => v !== s)).join(',')
+      })
+      const handleSizeChange = (val: Number) => {
+        emit('handlePage', {
+          page: props.page,
+          pageSize: val
+        })
+      }
+      const handleCurrentChange = (val: Number) => {
+        emit('handlePage', {
+          page: val,
+          pageSize: props.pageSize
+        })
+      }
+      onMounted(() => {
+      })
+      return {
+        handleSizeChange,
+        handleCurrentChange,
+        ...toRefs(state),
+        pageLayoutCpt
+      }
+    },
+  })
+</script>
+
+<style scoped lang="scss">
+  .cus-table-card-main {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+    $cus-page-height: 32px;
+    $colP: v-bind(col);
+    .cus-table-card-content {
+      overflow-y: auto;
+      flex: 1;
+      .cus-table-card-content-list {
+        display: flex;
+        flex-wrap: wrap;
+        .model {
+          width: calc((100% - (v-bind(colMargin) * (v-bind(col) - 1))) / v-bind(col));
+        }
+      }
+    }
+    :deep(.ctc-page) {
+      height: $cus-page-height;
+      margin-top: 25px;
+      font-size: 14px;
+      font-family: Microsoft YaHei;
+      font-weight: 400;
+      color: #999999;
+      display: flex;
+      justify-content: space-between;
+      .total {
+        display: flex;
+        align-items: center;
+      }
+    }
+  }
+</style>

+ 72 - 0
src/components/cus/CusTableColumn.vue

@@ -0,0 +1,72 @@
+<template>
+  <template v-if="item?.show">
+    <slot :name="`${item?.value}-column`" :column="item">
+      <el-table-column
+        :show-overflow-tooltip="!item?.popover"
+        :label="item?.label"
+        :prop="item?.value"
+        :column-key="item?.value"
+        :sortable="item?.sort ? 'custom' : false"
+        :width="item?.width ? item?.width : 'auto'"
+        :align="item?.align ? item?.align : 'center'"
+        :header-align="item?.headerAlign ? item?.headerAlign : 'center'"
+        :fixed="item?.fixed ? item?.fixed : false"
+        :filters="item?.filters ? item?.filters : null"
+        :filter-multiple="false"
+      >
+        <template #default="scope">
+          <template v-if="item.html">
+            <div v-html="scope?.row[item?.value]"></div>
+          </template>
+          <template v-else>
+            <slot :name="`${item?.value}-column-value`" :scope="scope">
+              <CusPopover v-if="item?.popover" :value="scope?.row[item?.value]">
+                <div class="__text-ellipsis">{{ scope?.row[item?.value] }}</div>
+              </CusPopover>
+              <template v-else>
+                {{ scope?.row[item?.value] }}
+              </template>
+            </slot>
+          </template>
+        </template>
+      </el-table-column>
+    </slot>
+  </template>
+</template>
+
+<script lang="ts">
+import {
+  defineComponent,
+  computed,
+  onMounted,
+  ref,
+  reactive,
+  watch,
+  getCurrentInstance,
+  ComponentInternalInstance,
+} from "vue";
+import { useStore } from "vuex";
+import { useRouter } from "vue-router";
+
+export default defineComponent({
+  name: "CusTableColumn",
+  props: {
+    item: {
+      required: true
+    },
+  },
+  setup() {
+    const store = useStore();
+    const router = useRouter();
+    const that = (getCurrentInstance() as ComponentInternalInstance)?.appContext
+      ?.config?.globalProperties;
+    return {};
+  },
+});
+</script>
+
+<style scoped lang="scss">
+  .cus-table-column-popover {
+
+  }
+</style>

+ 117 - 0
src/components/cus/CusTree.vue

@@ -0,0 +1,117 @@
+<template>
+  <div class="cus-tree" ref="ref_cusTree" v-if="refresh">
+    <CusTreeItem
+        :data="data"
+        :options="{
+          label, value, children, left, expendTrueImg, expendFalseImg
+        }"
+    >
+      <template #default="{data}">
+        <slot :data="data"/>
+      </template>
+    </CusTreeItem>
+  </div>
+</template>
+
+<script lang="ts">
+  import {
+    defineComponent,
+    onMounted,
+    ref,
+    toRefs,
+    reactive,
+    watch,
+    getCurrentInstance,
+    ComponentInternalInstance,
+    computed, nextTick
+  } from "vue";
+  import { useStore } from "vuex";
+  import { useRouter } from "vue-router";
+  import { ElMessage, ElMessageBox } from "element-plus";
+  import CusTreeItem from "./CusTreeItem.vue";
+  import expendTrueImg from '@/assets/images/cus/to-bottom.png'
+  import expendFalseImg from '@/assets/images/cus/to-right.png'
+  export default defineComponent({
+    name: "CusTree",
+    components: {
+      CusTreeItem
+    },
+    props: {
+      data: {required: true},
+      label: {default: 'label'},
+      value: {default: ''},
+      children: {default: 'children'},
+      left: {default: 20},
+      expend: {default: false},
+      expendTrueImg: {default: expendTrueImg},
+      expendFalseImg: {default: expendFalseImg},
+      filterMethod: {default: () => {}}
+    },
+    setup(props) {
+      const store = useStore();
+      const that = (getCurrentInstance() as ComponentInternalInstance).appContext.config.globalProperties;
+      const state = reactive({
+        _dataBack: props.data,
+        _data: props.data,
+        refresh: true
+      });
+      watch(() => props.data, (n) => {
+        initData(n)
+        filter()
+      })
+
+      const initData = (list) => {
+        const deep = (arr) => {
+          arr.forEach(v => {
+            v._expend = props.expend ? true : false
+            if (v[props.children]?.length > 0) {
+              deep(v[props.children])
+            }
+          })
+        }
+        deep(list)
+      }
+      const filter = (isExpend = null) => {
+        const deep = (arr) => {
+          arr.forEach(v => {
+            v._count = 0
+            if (props.filterMethod(v)) {
+              if (isExpend !== null) {
+                v._expend = isExpend
+              }
+              v._count++
+            }
+            if (v[props.children]?.length > 0) {
+              deep(v[props.children])
+              if (v[props.children].filter(c => c._count > 0).length > 0) {
+                if (isExpend !== null) {
+                  v._expend = isExpend
+                }
+                v._count++
+              }
+            }
+          })
+        }
+        deep(props.data)
+        state.refresh = false
+        setTimeout(() => {
+          state.refresh = true
+        }, 100)
+      }
+      onMounted(() => {
+        initData(props.data)
+        filter()
+      })
+      return {
+        ...toRefs(state),
+        filter,
+      }
+    },
+  });
+</script>
+<style scoped lang="scss">
+.cus-tree {
+  width: 100%;
+  height: calc(100vh);
+}
+</style>

+ 109 - 0
src/components/cus/CusTreeItem.vue

@@ -0,0 +1,109 @@
+<template>
+  <div class="cus-tree-item">
+    <template v-for="item in data">
+      <template v-if="item._count > 0">
+        <div
+            class="cus-tree-item-slot"
+            :style="`padding-left: ${options.left * level}px;`"
+        >
+          <div class="expend-img" @click="item._expend = !item._expend, $forceUpdate()">
+            <template v-if="item?.[options.children]?.length > 0">
+              <img v-if="item._expend" :src="options.expendTrueImg"/>
+              <img v-else :src="options.expendFalseImg"/>
+            </template>
+          </div>
+          <slot :data="item">
+            <div
+                class="cus-tree-item-label"
+            >
+              {{item[options.label]}}
+            </div>
+          </slot>
+        </div>
+        <CusTreeItem
+            v-if="item?.[options.children]?.length > 0"
+            v-show="item._expend"
+            :data="item[options.children]"
+            :options="options"
+            :level="level + 1"
+        >
+          <template #default="{data}">
+            <slot :data="data"/>
+          </template>
+        </CusTreeItem>
+      </template>
+    </template>
+  </div>
+</template>
+
+<script lang="ts">
+  import {
+    defineComponent,
+    onMounted,
+    ref,
+    toRefs,
+    reactive,
+    watch,
+    getCurrentInstance,
+    ComponentInternalInstance,
+    computed, nextTick,
+  } from "vue";
+  import { useStore } from "vuex";
+  import { useRouter } from "vue-router";
+  import { ElMessage, ElMessageBox } from "element-plus";
+  export default defineComponent({
+    name: "CusTreeItem",
+    components: {
+    },
+    props: {
+      data: {
+        required: true
+      },
+      options: {
+        required: true
+      },
+      level: {
+        default: 0
+      }
+    },
+    setup(props) {
+      const store = useStore();
+      const that = (getCurrentInstance() as ComponentInternalInstance).appContext.config.globalProperties;
+      const state = reactive({
+      });
+      onMounted(() => {
+      })
+      return {
+        ...toRefs(state),
+      }
+    },
+  });
+</script>
+<style scoped lang="scss">
+.cus-tree {
+  width: 100%;
+  height: calc(100vh);
+  .cus-tree-item-slot {
+    display: flex;
+    align-items: center;
+    .cus-tree-item-label {
+      width: 100%;
+      cursor: pointer;
+      &:hover {
+        background-color: #F5F7FA;
+      }
+    }
+    .expend-img {
+      width: 16px;
+      height: 16px;
+      margin-right: 5px;
+      >img {
+        cursor: pointer;
+        width: 100%;
+        height: 100%;
+      }
+    }
+  }
+
+}
+</style>

+ 139 - 0
src/components/cus/README.md

@@ -0,0 +1,139 @@
+
+
+# CusContent 列表布局组件
+props
+~~~~
+tableHead:CusTable的表头数据-用来加载字段筛选
+showText:是否显示复合查询
+text:复合查询文本输入框绑定变量
+searchTip:复合查询默认提示placeholder
+full:内容铺满
+~~~~
+emit
+~~~~
+handleReset:重置按钮回调方法
+handleSearch:查询按钮回调方法
+handleText(text):复合查询文本输入值改变时回调方法
+~~~~
+slot
+~~~~
+top:顶部其他内容插槽
+fieldOut:筛选条件插槽
+center:筛选条件余表格中间其他内容插槽
+buttons:左侧操作按钮插槽
+fieldIn:高级筛选条件插槽
+table:表格位置插槽
+~~~~
+
+# CusTable 表格组件
+props
+~~~~
+tableData:表格数据
+tableHead:表头数据 {
+    value 必须:变量
+    label 必须:名称
+    show:是否默认显示
+    sort:是否开启排序
+    popover:是否可以悬浮提示(鼠标可以滑入复制)
+    width:宽度
+    align:对齐位置
+    headerAlign:表头对齐位置
+    filters:开启列排序时的数据
+
+    多级表头时采用嵌套的方式,最多三级表头
+    如 tableHead: [
+        {
+            label:父表头,
+            children: [
+                表头, 表头, 表头
+            ]
+        }
+    ]
+}
+total:列表总数变量
+page:列表当前页变量
+pageSize:列表页大小变量
+selected可不传:多选列表时绑定的数组变量,
+noPage 开启即可隐藏分页组件
+otherStyle:非通用列表时传true不加载样式
+showIndex:展示序号列
+singled: 单选绑定对象
+singledKey: 单选唯一标识
+:pageSizes="[1, 3, 5]" 页码大小
+:noLayout="['sizes', 'total']"  分页不采用的布局标识
+full:表格不出现滚动条,内容铺满
+~~~~
+emit
+~~~~
+handlePage({page, pageSize}):列表分页改变时回调方法
+handleSort({key, value}):列表排序改变时回调方法
+handleFilter({key, value}):列过滤改变时回调方法
+~~~~
+slot
+~~~~
+{列变量}-column:表格列默认使用CusTableColumn,如需自定义el-table-column,使用该插槽
+{列变量}-column-value:表格列值需要翻译,样式修改等操作时,使用该插槽,如果还需悬浮提示的话,需要在内部嵌套CusPopover
+~~~~
+ref外部方法
+~~~~
+resetFilter(key):清空列过滤状态,key列变量,不传则全部清空
+~~~~
+
+# CusPopover 悬浮提示组件
+props
+~~~~
+value:提示的内容
+~~~~
+slot
+~~~~
+默认:触发内容
+~~~~
+
+# CusSearchButtons 查询按钮组
+props
+~~~~
+expandValue:展开状态绑定的参数,不传则不显示展开按钮
+~~~~
+emit
+~~~~
+handleReset:重置按钮回调方法
+handleSearch:查询按钮回调方法
+~~~~
+
+# CusForm 表单
+props
+~~~~
+labelWidth:表单标题宽度,默认100px
+~~~~
+emit
+~~~~
+handleEnter:input文本域回车统一触发方法
+submit:表单提交校验
+~~~~
+
+
+# CusFormColumn 表单细项
+详见demo:/views/demo/form/*.vue
+props
+~~~~
+span: el-col的布局
+filterSpan: 列表采用5等分布局时的栅格,[1,2,3,4,5]对应n/5
+offset: el-col的布局
+link: input、cascader、checkbox、date、datetime、radio、select、switch、portOfRegistry(船籍港)、residentMooringPoint(常驻停泊点)、dept(部门树)
+all:link为dept时,默认为当前部门及子部门,传值则为所有部门
+param: 通用的参数
+paramOne: 双下拉时第一级参数
+paramTwo: 双下拉时第二级参数
+label: 标题
+required: 是否必填
+labelWidth: 标题宽度
+rules: 校验规则
+maxLength: 长度校验值
+minLength: 长度校验值
+$attrs:elementPlus各属性
+~~~~
+emit
+~~~~
+handleValidate:触发校验
+getDefault:link为dept时,默认获取的顶级部门
+~~~~

+ 125 - 0
src/components/cus/cus-form-link/cascader.vue

@@ -0,0 +1,125 @@
+<template>
+  <div v-loading="loading" :element-loading-background="elementLoadingBackground" style="width: 100%;">
+    <el-cascader
+       style="width: 100%;"
+       v-bind="$attrs"
+       v-model="paramVal"
+       :placeholder="$attrs.placeholder ? $attrs.placeholder : `请选择${label}`"
+       :options="options"
+       clearable
+       filterable
+       :props="propsCpt"
+       :teleported="false"
+    >
+      <template #default="{ node, data }">
+        <slot v-bind="{node, data}">
+          <div class="cascader-item __text-ellipsis">
+            <template v-if="panelMaxWidth">
+              <el-tooltip
+                  class="box-item"
+                  effect="dark"
+                  :content="data[propsCpt.label]"
+                  placement="top"
+              >
+                <span>{{ data[propsCpt.label] }}</span>
+              </el-tooltip>
+            </template>
+            <template v-else>
+              <span>{{ data[propsCpt.label] }}</span>
+            </template>
+          </div>
+        </slot>
+      </template>
+    </el-cascader>
+  </div>
+</template>
+
+<script lang="ts">
+import {
+  defineComponent,
+  computed,
+  onMounted,
+  ref,
+  reactive,
+  watch,
+  getCurrentInstance,
+  ComponentInternalInstance,
+  toRefs,
+  nextTick,
+  inject
+} from 'vue'
+import {useStore} from 'vuex'
+import {useRouter, useRoute} from 'vue-router'
+
+export default defineComponent({
+  name: '',
+  components: {},
+  props: {
+    param: {},
+    label: {},
+    options: { type: Array, default: () => [] },
+    props: {},
+    panelMaxWidth: {
+      default: null
+    },
+    static: {default: false}
+  },
+  setup(props, { emit }) {
+    const store = useStore();
+    const router = useRouter();
+    const route = useRoute();
+    const that = (getCurrentInstance() as ComponentInternalInstance).appContext.config.globalProperties
+    const state = reactive({
+      paramVal: props.param,
+      defaultProps: {
+        expandTrigger: 'hover' as const,
+        checkStrictly: true,
+        emitPath: false,
+        label: 'label',
+        value: 'value'
+      },
+      loading: true,
+      elementLoadingBackground: inject('element-loading-background', null)
+    })
+    watch(() => state.paramVal, (n) => {
+      emit('emitParam', n)
+    })
+    watch(() => props.param, (n) => {
+      state.paramVal = n
+    })
+    watch(() => [props.options, props.static], () => {
+      state.loading = false
+    })
+    const propsCpt = computed(() => {
+      return props.props ? props.props : state.defaultProps
+    })
+    onMounted(() => {
+      if (props.static !== false || props.options?.length > 0) {
+        state.loading = false
+      }
+    })
+    return {
+      ...toRefs(state),
+      propsCpt
+    }
+  },
+})
+</script>
+
+<style scoped lang="scss">
+:deep(.el-cascader__dropdown) {
+  .el-cascader-node__label {
+    $cmw: v-bind(panelMaxWidth);
+    @if($cmw) {
+      max-width: $cmw;
+    }
+    .cascader-item {
+      width: 100%;
+    }
+  }
+  .el-cascader__suggestion-item {
+    height: auto !important;
+  }
+}
+
+</style>

+ 90 - 0
src/components/cus/cus-form-link/checkbox.vue

@@ -0,0 +1,90 @@
+<template>
+  <div v-loading="loading" :element-loading-background="elementLoadingBackground">
+    <el-checkbox-group
+        v-bind="$attrs"
+        v-model="paramVal"
+        @change="handleChange"
+    >
+      <el-checkbox
+          v-for="item in options"
+          :label="item[valueKey]"
+      >
+        {{ item[labelKey] }}
+      </el-checkbox>
+    </el-checkbox-group>
+  </div>
+</template>
+
+<script lang="ts">
+import {
+  defineComponent,
+  computed,
+  onMounted,
+  ref,
+  reactive,
+  watch,
+  getCurrentInstance,
+  ComponentInternalInstance,
+  toRefs,
+  nextTick,
+  inject
+} from 'vue'
+import {useStore} from 'vuex'
+import {useRouter, useRoute} from 'vue-router'
+
+export default defineComponent({
+  name: '',
+  components: {},
+  props: {
+    param: {},
+    label: {},
+    options: { type: Array, default: () => [] },
+    labelKey: { type: String, default: 'dictLabel' },
+    valueKey: { type: String, default: 'dictValue' },
+    static: {default: false}
+  },
+  setup(props, { emit }) {
+    const store = useStore();
+    const router = useRouter();
+    const route = useRoute();
+    const that = (getCurrentInstance() as ComponentInternalInstance).appContext.config.globalProperties
+    const state = reactive({
+      paramVal: props.param,
+      loading: true,
+      elementLoadingBackground: inject('element-loading-background', null)
+    })
+    watch(() => state.paramVal, (n) => {
+      emit('emitParam', n)
+    })
+    watch(() => props.param, (n) => {
+      state.paramVal = n
+    })
+    watch(() => [props.options, props.static], () => {
+      state.loading = false
+    })
+    const optionsMapCpt = computed(() => {
+      const map = new Map()
+      props.options?.forEach((v: any) => {
+        map.set(v[props.valueKey], v)
+      })
+      return map
+    })
+    const handleChange = (val: any) => {
+      emit('getObject', val.map((v: any) => optionsMapCpt.value.get(v)))
+    }
+    onMounted(() => {
+      if (props.static !== false || props.options?.length > 0) {
+        state.loading = false
+      }
+    })
+    return {
+      ...toRefs(state),
+      optionsMapCpt,
+      handleChange
+    }
+  },
+})
+</script>
+
+<style scoped lang="scss">
+</style>

+ 113 - 0
src/components/cus/cus-form-link/date.vue

@@ -0,0 +1,113 @@
+<template>
+  <el-date-picker
+      style="width: 100%;"
+      :type="type"
+      v-bind="$attrs"
+      v-model="paramVal"
+      clearable
+      :placeholder="$attrs.placeholder ? $attrs.placeholder : `请选择${label}`"
+      :valueFormat="valueFormatCpt"
+      :format="formatCpt"
+      unlink-panels
+  >
+    <template #default="cell">
+      <slot name="default" :cell="cell"></slot>
+    </template>
+  </el-date-picker>
+</template>
+
+<script lang="ts">
+import {
+  defineComponent,
+  computed,
+  onMounted,
+  ref,
+  reactive,
+  watch,
+  getCurrentInstance,
+  ComponentInternalInstance,
+  toRefs,
+  nextTick
+} from 'vue'
+import {useStore} from 'vuex'
+import {useRouter, useRoute} from 'vue-router'
+
+export default defineComponent({
+  name: '',
+  components: {},
+  props: {
+    param: {},
+    label: {},
+    type: {
+      type: String,
+      default: 'date',
+      validator(val: string) {
+        return ['date', 'year', 'week', 'month', 'dates', 'monthrange', 'daterange'].includes(val)
+      }
+    },
+    valueFormat: { type: String, default: ''},
+    format: { type: String, default: ''},
+  },
+  setup(props, { emit }) {
+    const store = useStore();
+    const router = useRouter();
+    const route = useRoute();
+    const that = (getCurrentInstance() as ComponentInternalInstance).appContext.config.globalProperties
+    const state = reactive({
+      paramVal: props.param
+    })
+    watch(() => state.paramVal, (n) => {
+      emit('emitParam', n)
+    })
+    watch(() => props.param, (n) => {
+      state.paramVal = n
+    })
+    const formatCpt = computed(() => {
+      let val = props.format
+      if (!val) {
+        switch (props.type) {
+          case 'year':
+            val = 'YYYY'
+            break;
+          case 'month':
+            val = 'YYYY-DD'
+            break;
+          case 'week':
+            val = '[第]ww[周]'
+            break;
+          default:
+            val = 'YYYY-MM-DD'
+        }
+      }
+      return val
+    })
+    const valueFormatCpt = computed(() => {
+      let val = props.valueFormat
+      if (!val) {
+        switch (props.type) {
+          case 'year':
+            val = 'YYYY'
+            break;
+          case 'month':
+            val = 'YYYY-DD'
+            break;
+          case 'week':
+            val = ''
+            break;
+          default:
+            val = 'YYYY-MM-DD'
+        }
+      }
+      return val
+    })
+    return {
+      ...toRefs(state),
+      formatCpt,
+      valueFormatCpt,
+    }
+  },
+})
+</script>
+
+<style scoped lang="scss">
+</style>

+ 73 - 0
src/components/cus/cus-form-link/datetime.vue

@@ -0,0 +1,73 @@
+<template>
+  <el-date-picker
+      style="width: 100%;"
+      :type="type"
+      v-bind="$attrs"
+      v-model="paramVal"
+      clearable
+      :placeholder="$attrs.placeholder ? $attrs.placeholder : `请选择${label}`"
+      :valueFormat="valueFormat"
+      :format="format"
+      unlink-panels
+  >
+    <template #default="cell">
+      <slot name="default" :cell="cell"></slot>
+    </template>
+  </el-date-picker>
+</template>
+
+<script lang="ts">
+import {
+  defineComponent,
+  computed,
+  onMounted,
+  ref,
+  reactive,
+  watch,
+  getCurrentInstance,
+  ComponentInternalInstance,
+  toRefs,
+  nextTick
+} from 'vue'
+import {useStore} from 'vuex'
+import {useRouter, useRoute} from 'vue-router'
+
+export default defineComponent({
+  name: '',
+  components: {},
+  props: {
+    param: {},
+    label: {},
+    type: {
+      type: String,
+      default: 'datetime',
+      validator(val: string) {
+        return ['datetime', 'datetimerange'].includes(val)
+      }
+    },
+    valueFormat: { type: String, default: 'YYYY-MM-DD HH:mm:ss'},
+    format: { type: String, default: 'YYYY-MM-DD HH:mm:ss'},
+  },
+  setup(props, { emit }) {
+    const store = useStore();
+    const router = useRouter();
+    const route = useRoute();
+    const that = (getCurrentInstance() as ComponentInternalInstance).appContext.config.globalProperties
+    const state = reactive({
+      paramVal: props.param
+    })
+    watch(() => state.paramVal, (n) => {
+      emit('emitParam', n)
+    })
+    watch(() => props.param, (n) => {
+      state.paramVal = n
+    })
+    return {
+      ...toRefs(state),
+    }
+  },
+})
+</script>
+
+<style scoped lang="scss">
+</style>

+ 83 - 0
src/components/cus/cus-form-link/dept.vue

@@ -0,0 +1,83 @@
+<template>
+  <div v-loading="loading" :element-loading-background="elementLoadingBackground" style="width: 100%;">
+    <el-tree-select
+        style="width: 100%;"
+        v-bind="$attrs"
+        v-model="paramVal"
+        :placeholder="$attrs.placeholder ? $attrs.placeholder : `请选择${label}`"
+        :data="optionsCpt"
+        clearable
+        filterable
+        :check-strictly="checkStrictly"
+        node-key="deptId"
+        :props="{
+          label: 'deptName',
+        }"
+    />
+  </div>
+</template>
+
+<script lang="ts">
+import {
+  defineComponent,
+  computed,
+  onMounted,
+  ref,
+  reactive,
+  watch,
+  getCurrentInstance,
+  ComponentInternalInstance,
+  toRefs,
+  nextTick,
+  inject
+} from 'vue'
+import {useStore} from 'vuex'
+import {useRouter, useRoute} from 'vue-router'
+
+export default defineComponent({
+  name: '',
+  components: {},
+  props: {
+    param: {},
+    label: {},
+    all: { default: false },
+    checkStrictly: { default: true }
+  },
+  setup(props, { emit }) {
+    const store = useStore();
+    const router = useRouter();
+    const route = useRoute();
+    const that = (getCurrentInstance() as ComponentInternalInstance).appContext.config.globalProperties
+    const state = reactive({
+      paramVal: props.param,
+      loading: true,
+      elementLoadingBackground: inject('element-loading-background', null)
+    })
+    watch(() => state.paramVal, (n) => {
+      emit('emitParam', n)
+    })
+    watch(() => props.param, (n) => {
+      state.paramVal = n
+    })
+    const optionsCpt = computed(() => {
+      return props.all === false ? store.state.app.deptTree : store.state.app.deptAllTree
+    })
+    onMounted(() => {
+      Promise.all([
+        store.dispatch('app/LOAD_DEPT_TREE'),
+        store.dispatch('app/LOAD_DEPT_ALL_TREE')
+      ]).then(() => {
+        state.loading = false
+        emit('emitDefault', optionsCpt.value[0])
+      })
+    })
+    return {
+      ...toRefs(state),
+      optionsCpt
+    }
+  },
+})
+</script>
+
+<style scoped lang="scss">
+</style>

+ 77 - 0
src/components/cus/cus-form-link/input.vue

@@ -0,0 +1,77 @@
+<template>
+  <el-input
+      v-bind="$attrs"
+      v-model="paramVal"
+      :type="type"
+      clearable
+      :placeholder="$attrs.placeholder ? $attrs.placeholder : `请输入${label}`"
+      @keyup.enter.native="$emit('emitEnter')"
+  >
+    <template v-if="$slots.prefix" #prefix>
+      <slot name="prefix"/>
+    </template>
+    <template v-if="$slots.suffix" #suffix>
+      <slot name="suffix"/>
+    </template>
+    <template v-if="$slots.prepend" #prepend>
+      <slot name="prepend"/>
+    </template>
+    <template v-if="$slots.append" #append>
+      <slot name="append"/>
+    </template>
+  </el-input>
+</template>
+
+<script lang="ts">
+import {
+  defineComponent,
+  computed,
+  onMounted,
+  ref,
+  reactive,
+  watch,
+  getCurrentInstance,
+  ComponentInternalInstance,
+  toRefs,
+  nextTick
+} from 'vue'
+import {useStore} from 'vuex'
+import {useRouter, useRoute} from 'vue-router'
+
+export default defineComponent({
+  name: '',
+  components: {},
+  props: {
+    param: {},
+    label: {},
+    type: {
+      type: String,
+      default: 'text',
+      validator(val: string) {
+        return ['text', 'textarea', 'password'].includes(val)
+      }
+    }
+  },
+  setup(props, { emit }) {
+    const store = useStore();
+    const router = useRouter();
+    const route = useRoute();
+    const that = (getCurrentInstance() as ComponentInternalInstance).appContext.config.globalProperties
+    const state = reactive({
+      paramVal: props.param
+    })
+    watch(() => state.paramVal, (n) => {
+      emit('emitParam', n)
+    })
+    watch(() => props.param, (n) => {
+      state.paramVal = n
+    })
+    return {
+      ...toRefs(state),
+    }
+  },
+})
+</script>
+
+<style scoped lang="scss">
+</style>

+ 0 - 0
src/components/cus/cus-form-link/portOfRegistry.vue


部分文件因为文件数量过多而无法显示