Caner 2 years ago
parent
commit
0f94d5cd73

+ 2 - 0
.env.production

@@ -0,0 +1,2 @@
+VITE_SERVER_URL=https://jkbj.scrbg.com:5013
+# VITE_SERVER_URL=https://eas.any8.cc:8010

+ 1 - 0
.eslintignore

@@ -0,0 +1 @@
+audio-pusher.ts

+ 91 - 0
.eslintrc.json

@@ -0,0 +1,91 @@
+{
+  "root": true,
+  "env": {
+    "node": true,
+    "browser": true,
+    "es2021": true
+  },
+  "parserOptions": {
+    "ecmaVersion": "latest",
+    "parser": "@typescript-eslint/parser",
+    "sourceType": "module"
+  },
+  "extends": [
+    "plugin:vue/vue3-recommended",
+    "airbnb-base"
+  ],
+  "rules": {
+    "import/prefer-default-export": "off",
+    "no-use-before-define": "off",
+    "no-console": 0, // 禁用打印
+    "comma-dangle": [
+      2,
+      "never"
+    ], // 禁止使用拖尾逗号
+    "no-extra-semi": 2, // 禁止不必要的分号
+    "array-bracket-spacing": [
+      2,
+      "always"
+    ], // 指定数组的元素之间要以空格隔开
+    "jsx-quotes": 0, // 强制使用单引号
+    "max-len": 0, // 强制一行的最大长度
+    "semi": [
+      2,
+      "never"
+    ], // 禁止使用分号
+    "no-unused-vars": 0,
+    "no-unneeded-ternary": 2, // 禁止不必要的嵌套 var isYes = answer === 1 ? true : false;
+    "no-unreachable": 2, // 不能有无法执行的代码
+    "no-unused-expressions": 1, // 禁止无用的表达式
+    "linebreak-style": [
+      0,
+      "error",
+      "windows"
+    ],
+    "import/no-unresolved": 0,
+    "import/extensions": 0,
+    "import/no-absolute-path": 0,
+    "import/no-extraneous-dependencies": [
+      "error",
+      {
+        "devDependencies": true
+      }
+    ],
+    "class-methods-use-this": 0,
+    "no-mixed-operators": 0,
+    "eol-last": 0,
+    "import/newline-after-import": 0,
+    "vue/multi-word-component-names": 0,
+    "no-param-reassign": 0,
+    "no-restricted-syntax": 0,
+    "no-underscore-dangle": 0,
+    "no-plusplus": 0,
+    "no-bitwise": 0,
+    "guard-for-in": 0,
+    "func-names": 0,
+    "import/order": 0,
+    "vue/no-deprecated-slot-attribute": 0,
+    "vue/v-on-event-hyphenation": 0,
+    "vue/no-deprecated-filter": 0,
+    "vue/require-explicit-emits": 0,
+    "vue/no-v-html": 0,
+    "vue/order-in-components": 0,
+    "vue/no-reserved-component-names": 0,
+    "no-promise-executor-return": 0,
+    "no-sparse-arrays": 0,
+    "no-nested-ternary": 0,
+    "no-continue": 0,
+    "complexity": 0,
+    "no-return-await": 0,
+    "max-classes-per-file": 0,
+    "consistent-return": 0,
+    "no-shadow": 0,
+    "no-undef": 0,
+    "no-throw-literal": 0,
+    "no-loss-of-precision": 0,
+    "no-prototype-builtins": 0,
+    "no-fallthrough": 0,
+    "no-case-declarations": 0,
+    "no-cond-assign": 0
+  }
+}

+ 5 - 1
.gitignore

@@ -27,4 +27,8 @@ build/Release
 # Dependency directory
 # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
 node_modules
-
+dist
+.env.development
+.DS_Store
+package-lock.json
+*.lock

+ 81 - 2
README.md

@@ -1,3 +1,82 @@
-# busRoute
+# 路桥报警系统
 
-公交路线
+## 目录结构
+```
+| - `src`
+|   - `pages`                     入口目录
+|           - `assets`            静态文件
+|           - `components`        组件
+|           - `store`             存储
+|           - `views`             页面
+|           - `service`           服务
+|           - `App.vue`           模板
+|           - `main.ts`           入口文件
+|   - `components`                全局组件
+|   - `services`                  全局服务
+|   - `assets`                    全局静态文件
+```
+## 编写规范
+```
+import { onMounted, ref, computed} from 'vue'
+<script setup>
+    <!-- 函数new区  -->
+    const test = new DataServer()
+
+    <!-- data 声明区 -->
+    const test=ref(1)
+    const test1:computed(()=>test)
+
+    <!-- fn 声明区 -->
+    function name(){
+      console.log(123)
+    }
+    test().then(res=>{})
+
+    <!-- vue 方法区 -->
+    onMounted(() => {test()})
+
+</script>
+<template></template>
+<style></style>
+```
+### TS
+* 不允许的操作
+  * `any` 类型
+  * `JSON.parse` 等危险操作时不嵌套异常处理
+  * 单个文件中的脚本代码总行数不允许超过`1024`行
+* 命名
+  * 文件 小写字母命名,多个词之间以 `-`连接,不允许大写字母及驼峰式
+  * 变量
+    * 小写字母开头的驼峰式,如 `userName`
+    * 必须使用英文单词或有意义的拼音
+    * 单字符变量名只允许在循环、循环回调中使用,可使用 `i,j,k,m,n,t,v`等
+  * 函数
+    * 小写字母开头的驼峰式,如 `shouUserName`
+    * `.service.ts` 文件中方法命名建议:
+      * 集合类数据获取采用 `loadUsers` 之类的命名,即以 `load`开头并以获取数据名结尾
+      * 单个数据获取采用 `fetchTemplate` 之类的命名,以`fetch`开头并以数据名结尾
+      * 保存数据采用 `saveFlow` 之类的命名
+      * 删除数据采用 `removeTemplate` 之类的命名
+  * 类及类型
+    * 大写字母开头的驼峰式,如 `UserService`
+    * 私有变量以下划线接小写字母开头的驼峰式,如 `_innerType`
+  * 全局常量,下划线连接大写字母,如 `GROUP_TYPES`
+* 类型及注释
+  * 注释写法分以下几类
+  /** 这种用于简单描述函数作用 或 类成员变量用处 */
+  // 这种用于描述函数内变量或代码段作用
+  /**
+   * 这种用于描述类、复杂函数用途及参数等
+   */
+* 函数调用
+  * async 在没有其它函数调用的情况下,写成.then
+
+### CSS
+* 不允许使用无封装或不唯一的全局类样式,必须使用时需备注说明并慎重选择类名
+* 颜色赋值使用 `var(--color-a)` 写法,无特殊说明不允许直接赋值
+* `class, id` 命名必须使用短线连接单词方式,如 `form-label`, `list-item-title` 等
+* 多层级类名嵌套时,必须使用`scss`嵌套写法,以免污染其他样式
+
+### Notice
+* 框架使用解构,注入方式
+* 自行增加.env.development 测试环境变量

+ 12 - 0
index.html

@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>home</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/pages/main.ts"></script>
+  </body>
+</html>

+ 34 - 0
package.json

@@ -0,0 +1,34 @@
+{
+  "name": "vite-vue3-ts-template",
+  "private": true,
+  "version": "0.0.0",
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "vue-tsc --noEmit && vite build",
+    "preview": "vite preview"
+  },
+  "dependencies": {
+    "naive-ui": "^2.35.0",
+    "pinia": "^2.1.7",
+    "pinia-plugin-persist": "^1.0.0",
+    "vue": "^3.3.11",
+    "vue-router": "^4.2.5"
+  },
+  "devDependencies": {
+    "@types/node": "^20.10.4",
+    "@typescript-eslint/parser": "^6.14.0",
+    "@vitejs/plugin-vue": "^4.5.2",
+    "eslint": "^8.55.0",
+    "eslint-config-airbnb-base": "^15.0.0",
+    "eslint-plugin-import": "^2.29.0",
+    "eslint-plugin-vue": "^9.19.2",
+    "sass": "^1.69.3",
+    "typescript": "^5.3.3",
+    "vite": "^5.0.8",
+    "vite-plugin-compression": "^0.5.1",
+    "vite-plugin-eslint": "^1.8.0",
+    "vite-plugin-svg-icons": "^2.0.1",
+    "vue-tsc": "^1.8.25"
+  }
+}

+ 6 - 0
src/assets/icons/add.svg

@@ -0,0 +1,6 @@
+<svg t="1699277626262" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5500"
+  width="200" height="200">
+  <path
+    d="M939.939489 459.072557 562.339502 459.072557 562.339502 83.519182 462.055494 83.519182 462.055494 459.072557 84.455507 459.072557 84.455507 559.356564 462.055494 559.356564 462.055494 939.003164 562.339502 939.003164 562.339502 559.356564 939.939489 559.356564Z"
+    fill="#1AB99B" p-id="5501"></path>
+</svg>

+ 46 - 0
src/assets/naive-theme.ts

@@ -0,0 +1,46 @@
+import { GlobalThemeOverrides } from 'naive-ui'
+const themeOverrides: GlobalThemeOverrides = {
+  common: {
+    primaryColor: '#2791FEFF',
+    primaryColorHover: '#67B2FEFF',
+    primaryColorPressed: '#1F74CBFF',
+    primaryColorSuppl: '#93C8FEFF',
+    errorColorHover: '#EC787BFF',
+    errorColor: '#E8575AFF',
+    errorColorPressed: '#B94548FF',
+    errorColorSuppl: '#EE898BFF',
+    warningColor: '#FAAD38FF',
+    warningColorPressed: '#C88A2CFF',
+    textColorBase: '#17233EFF',
+    textColor1: 'rgba(80, 90, 110, 1)',
+    textColor2: 'rgba(128, 134, 148, 1)',
+    textColor3: 'rgba(196, 200, 206, 1)',
+    placeholderColor: 'rgba(196, 200, 206, 1)',
+    iconColor: 'rgba(196, 200, 206, 1)',
+    iconColorHover: 'rgba(103, 178, 254, 1)',
+    iconColorPressed: 'rgba(31, 116, 203, 1)',
+    iconColorDisabled: 'rgba(196, 200, 206, 1)',
+    dividerColor: 'rgba(220, 222, 226, 1)',
+    borderColor: 'rgba(196, 200, 206, 1)',
+    closeIconColor: 'rgba(80, 90, 110, 1)',
+    closeIconColorHover: 'rgba(103, 178, 254, 1)',
+    closeIconColorPressed: 'rgba(31, 116, 203, 1)',
+    closeColorHover: 'rgba(0, 0, 0, 0.1)',
+    closeColorPressed: 'rgba(0, 0, 0, 0.15)',
+    scrollbarColor: 'rgba(126, 133, 160, 1)',
+    scrollbarColorHover: 'rgba(103, 178, 254, 1)',
+    tagColor: '#F5F5F5FF',
+    inputColorDisabled: 'rgba(233, 234, 236, 1)',
+    fontSize: '14px',
+    fontSizeMini: '12px',
+    fontSizeTiny: '12px',
+    fontSizeSmall: '14px',
+    fontSizeMedium: '14px',
+    fontSizeLarge: '15px',
+    fontSizeHuge: '16px'
+  },
+  Icon: {
+    color: 'white'
+  }
+}
+export default themeOverrides

+ 16 - 0
src/assets/native-plugin.ts

@@ -0,0 +1,16 @@
+import {
+  create,
+  NConfigProvider,
+  NNotificationProvider,
+  NButton
+} from 'naive-ui'
+
+const naive = create({
+  components: [
+    NConfigProvider,
+    NNotificationProvider,
+    NButton
+  ]
+})
+
+export default naive

+ 27 - 0
src/components/icon.vue

@@ -0,0 +1,27 @@
+<script setup lang="ts">
+import { computed } from 'vue'
+
+const props = defineProps<{
+  name: string,
+  size?: number,
+  color?: string
+}>()
+const symbolId = computed(() => `#icon-${props.name}`)
+const newColor = computed(() => `${props.color ?? '#ccc'}`)
+const newSize = computed(() => `${props.size ?? 16}`)
+</script>
+<template>
+  <svg
+    aria-hidden="true"
+    :font-size="newSize"
+    :width="newSize"
+    :height="newSize"
+  >
+
+    <use
+      :href="symbolId"
+      :fill="newColor"
+      :fill-rule="undefined"
+    />
+  </svg>
+</template>

+ 38 - 0
src/components/loading.vue

@@ -0,0 +1,38 @@
+<template>
+  <div class="bg">
+    <div class="loader" />
+  </div>
+</template>
+<style lang="scss" scoped>
+    .bg{
+        width: 100vw;
+        height: 100vh;
+        position: fixed;
+        top: 0;
+        left: 0;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        background: rgba(0, 0, 0, 0.5);
+    }
+    .loader {
+
+        border: 5px solid #f3f3f3;
+        border-radius: 50%;
+        border-top: 5px solid #3498db;
+        width: 30px;
+        height: 30px;
+        -webkit-animation: spin 2s linear infinite;
+        animation: spin 2s linear infinite;
+    }
+
+    @-webkit-keyframes spin {
+        0% { -webkit-transform: rotate(0deg); }
+        100% { -webkit-transform: rotate(360deg); }
+    }
+
+    @keyframes spin {
+        0% { transform: rotate(0deg); }
+        100% { transform: rotate(360deg); }
+    }
+</style>

+ 32 - 0
src/pages/App.vue

@@ -0,0 +1,32 @@
+<template>
+  <loading v-if="show" />
+  <n-config-provider
+    preflight-style-disabled
+    inline-theme-disabled
+    :theme-overrides="themeOverrides"
+    :locale="zhCN"
+    :date-locale="dateZhCN"
+  >
+    <n-notification-provider>
+      <router-view />
+    </n-notification-provider>
+  </n-config-provider>
+</template>
+<script setup lang='ts'>
+import loading from '@/components/loading.vue'
+import useStore from './store/index'
+import { computed } from 'vue'
+import { zhCN, dateZhCN } from 'naive-ui'
+import Theme from '@/assets/naive-theme'
+
+const store = useStore()
+const show = computed(() => store.loading)
+const themeOverrides = Theme
+</script>
+<style>
+html,
+body {
+  margin: 0;
+  padding: 0;
+}
+</style>

+ 51 - 0
src/pages/main.ts

@@ -0,0 +1,51 @@
+import { createApp } from 'vue'
+import App from './App.vue'
+import { createPinia } from 'pinia'
+import naive from '@/assets/native-plugin'
+import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
+import Icon from '@/components/icon.vue'
+import 'virtual:svg-icons-register'
+import piniaPersist from 'pinia-plugin-persist'
+
+const store = createPinia()
+store.use(piniaPersist)
+
+// 动态路由
+const routes = Object.values(import.meta.glob('./views/*/route.ts', { eager: true, import: 'default' })) as unknown as RouteRecordRaw[]
+routes.push({ path: '/:path(.*)', redirect: '/' })
+
+const app = createApp(App)
+const router = createRouter({
+  history: createWebHashHistory(),
+  routes
+})
+app.component('Icon', Icon)
+// 路由守卫
+// router.beforeEach((to, from, next) => {
+//   // do something
+//   next()
+// })
+app.use(store)
+  .use(naive)
+  .use(router)
+
+router.isReady().then(() => {
+  const vm = app.mount('#app')
+  app.config.errorHandler = (err: any) => {
+    if (err.reason === 401) {
+      router.replace('/')
+    } else {
+      console.log('提升', err)
+    }
+  }
+  window.addEventListener('unhandledrejection', (evt) => {
+    console.error(evt)
+    evt.preventDefault()
+    app.config.errorHandler?.(evt, vm, '')
+  })
+  window.addEventListener('error', (evt) => {
+    console.error(evt)
+    evt.preventDefault()
+    app.config.errorHandler?.(evt.error, vm, '')
+  })
+})

+ 30 - 0
src/pages/store/index.ts

@@ -0,0 +1,30 @@
+import { defineStore } from 'pinia'
+
+export interface UserInfo {
+  id: number,
+  name: string,
+  opmId: number,
+  satoken: string
+}
+
+// id必填,且需要唯一
+const useStore = defineStore('index', {
+  state: () => ({
+    userInfro: {} as UserInfo,
+    token: '',
+    isCheckPermission: false
+  }),
+  actions: {
+    setUserInfo(data: UserInfo) {
+      this.userInfro = data
+      this.token = data.satoken || ''
+    },
+    setCheckPermission(data: boolean) {
+      this.isCheckPermission = data
+    }
+  },
+  persist: {
+    enabled: true // true 表示开启持久化保存
+  }
+})
+export default useStore

+ 25 - 0
src/pages/views/home/index.vue

@@ -0,0 +1,25 @@
+<script setup lang="ts">
+import NetService from '@/services/net.service'
+import { useRouter } from 'vue-router'
+const netService = new NetService()
+const router = useRouter()
+function sendAudio() {
+  console.log(123)
+  router.push('/test')
+  // netService.post('/sys/login', { userAccount: 1, userPwd: 'c4ca4238a0b923820dcc509a6f75849b' })
+}
+</script>
+<template>
+  <div style="position: relative;">
+    <n-button
+      type="primary"
+      @click="sendAudio"
+    >
+      我不是全局组件
+    </n-button>
+    <icon
+      name="add"
+      :size="20"
+    />
+  </div>
+</template>

+ 10 - 0
src/pages/views/home/route.ts

@@ -0,0 +1,10 @@
+import { RouteRecordRaw } from 'vue-router'
+import home from './index.vue'
+
+export default {
+  path: '/',
+  meta: {
+    authorize: true
+  },
+  component: home
+} as RouteRecordRaw

+ 3 - 0
src/pages/views/test/index.vue

@@ -0,0 +1,3 @@
+<template>
+  <div>3D测试</div>
+</template>

+ 10 - 0
src/pages/views/test/route.ts

@@ -0,0 +1,10 @@
+import { RouteRecordRaw } from 'vue-router'
+
+export default {
+  path: '/test',
+  meta: {
+    authorize: true
+  },
+  component: () => import('./index.vue' as any),
+  children: []
+} as RouteRecordRaw

+ 23 - 0
src/services/downloadFile.ts

@@ -0,0 +1,23 @@
+export const downloadFile = (url: string | Blob, fileName: string) => {
+    const download = (blob: Blob, fileName: string) => {
+        const a = document.createElement('a')
+        document.body.appendChild(a)
+        a.style.display = 'none'
+        // 使用获取到的blob对象创建的url
+        const url = window.URL.createObjectURL(blob)
+        a.href = url
+        // 指定下载的文件名
+        a.download = fileName
+        a.click()
+        document.body.removeChild(a)
+        // 移除blob对象的url
+        window.URL.revokeObjectURL(url)
+    }
+    let newUrl: any = ''
+    if (url instanceof Blob) {
+        newUrl = URL.createObjectURL(newUrl) // 创建blob地址
+        download(newUrl, fileName)
+    } else {
+        fetch(newUrl).then((res) => res.blob()).then((blob) => download(blob, fileName))
+    }
+}

+ 172 - 0
src/services/export2file.ts

@@ -0,0 +1,172 @@
+import html2canvas from "html2canvas";
+import JSPDF from "jspdf";
+import { downloadFile } from "./downloadFile";
+import * as XLSX from 'xlsx'
+
+/**
+ * 导出PDF
+ * @param domID 需要输出PDF的页面id
+ * @param fileName 文件名
+ * @param type  默认A4分页
+ * @param wMultiple 宽倍数
+ * @param hMultiple 高倍数
+ * @returns 
+ */
+export const exp2pdf = async (domID: string, fileName: string, type = 'A4', wMultiple = null, hMultiple = null) => {
+    const dom = document.getElementById(domID)
+    if (!dom) return
+    // loading
+    const domHeight = dom.offsetHeight // 获取DOM高度
+    const domWidth = dom.offsetWidth // 获取DOM宽度
+    const canvas = await html2canvas(dom, {
+        logging: false,
+        useCORS: true, // 允许图片跨域   
+        scale: 1.5,
+        width: wMultiple ? wMultiple * domWidth : undefined,
+        height: hMultiple ? hMultiple * domHeight : undefined
+    })
+
+    if (type === 'A4') {
+        // A4分页
+        const pdf = new JSPDF("p", "mm", "a4") // A4纸,纵向
+        const ctx = canvas.getContext("2d") as any
+        const a4w = 200;
+        const a4h = 277 // A4大小,210mm x 297mm,四边各保留20mm的边距
+        const imgHeight = Math.floor(a4h * canvas.width / a4w) // 按A4显示比例换算一页图像的像素高度
+        let renderedHeight = 0
+        while (renderedHeight < canvas.height) {
+            const page = document.createElement("canvas")
+            page.width = canvas.width
+            page.height = Math.min(imgHeight, canvas.height - renderedHeight) // 可能内容不足一页
+            // 用getImageData剪裁指定区域,并画到前面创建的canvas对象中
+            page.getContext("2d")?.putImageData(ctx.getImageData(0, renderedHeight, canvas.width, Math.min(imgHeight, canvas.height - renderedHeight)), 0, 0)
+            pdf.addImage(page.toDataURL("image/jpeg", 1.0), "JPEG", 10, 10, a4w, Math.min(a4h, a4w * page.height / page.width)) // 添加图像到页面,保留10mm边距
+            renderedHeight += imgHeight
+            if (renderedHeight < canvas.height) { pdf.addPage() } // 如果后面还有内容,添加一个空页
+            // delete page;
+        }
+        pdf.save(fileName)
+
+    } else {
+        // 整张
+        const pdf = new JSPDF('p', 'px', [domWidth, domHeight])
+        pdf.addImage(canvas.toDataURL("image/jpeg", 1.0), "JPEG", 10, 10, domWidth, domHeight)
+        pdf.save(fileName)
+    }
+    // loading
+}
+
+/**
+ * 导出PNG
+ * @param domID 需要输出PDF的页面id
+ * @param fileName 文件名
+ * @param bkcolor 背景色
+ */
+export const exp2png = async (domID: string, fileName: string, bkcolor: string) => {
+    // loading
+    window.scroll(0, 0) // 首先先顶部
+    const design = document.getElementById(domID) as HTMLElement
+    if (!design) return
+    const imgHeight = design.offsetHeight // 获取DOM高度
+    const imgWidth = design.offsetWidth // 获取DOM宽度
+    const scale = window.devicePixelRatio <= 3 ? 3 : window.devicePixelRatio // 获取设备像素比
+    const canvas = await html2canvas(design, {
+        backgroundColor: bkcolor, // 设置背景颜色
+        useCORS: true, // 允许图片跨域
+        scale: scale, // 缩放3倍,使得图片更加清晰=>越清晰图片越大
+        width: imgWidth,
+        height: imgHeight,
+        imageTimeout: 5000 // 设置图片的超时,设置0为禁用
+    })
+    const imgURL = canvas.toDataURL('image/png') as any
+    downloadFile(imgURL,fileName)
+    // loading
+}
+
+/**
+ * 解析excel表格
+ * @param file 文件
+ * @returns 
+ */
+export const exp2json = async (file: File) => {
+    return await new Promise((resolve, reject) => {
+        try {
+            const reader = new FileReader()
+            reader.onload = (e) => {
+                const wb = XLSX.read(e.target?.result, {
+                    type: 'binary'
+                }) // 读取完成的数据
+                // 转成json header解析第一行标题
+                const data = XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]], { header: 1 })
+                resolve(data)
+            }
+            reader.readAsBinaryString(file)
+        } catch (error) {
+            console.log('解析错误')
+            reject(error)
+        }
+    })
+}
+
+/**
+ * dom导出excel
+ * @param domID domID
+ * @param fileName 文件名
+ */
+export const dom2excel = (domID: string, fileName: string) => {
+    const dom = document.getElementsByTagName(domID)
+    if (!dom) return
+    const wb = XLSX.utils.table_to_book(dom[0])
+    const baty = XLSX.write(wb, { bookType: 'xlsx', bookSST: false, type: 'binary' })
+    // 字符串转ArrayBuffer
+    const s2ab = (s: any) => {
+        const buf = new ArrayBuffer(s.length)
+        const view = new Uint8Array(buf)
+        for (let i = 0; i !== s.length; ++i) view[i] = s.charCodeAt(i) & 0xFF
+        return buf
+    }
+    const blob = new Blob([s2ab(baty)], { type: 'application/octet-stream' })
+    downloadFile(blob, fileName + '.xlsx')
+}
+
+/**
+ * array导出excel表格
+ * @param arr 数据是数组包含的对象
+ * @param fileName 名字
+ */
+export const exp2excel = (arr: object[], fileName:string,cellMerges?:Array<any>) => {
+    const sheet = XLSX.utils.json_to_sheet(arr);
+    // excel宽高设置
+    sheet["!cols"] = arr.map(() => {
+        return { wch: 30 }
+    })
+    if(cellMerges){
+        sheet['!merges'] = cellMerges; // <====合并单元格
+    }
+    // 转blob
+    const sheet2blob = (sheet: any, sheetName='sheet1') => {
+        const workbook = {
+            SheetNames: [sheetName],
+            Sheets: {} as any
+        };
+        workbook.Sheets[sheetName] = sheet;
+        // 生成excel的配置项
+        const wopts = {
+            bookType: 'xlsx', // 要生成的文件类型
+            bookSST: false, // 是否生成Shared String Table,官方解释是,如果开启生成速度会下降,但在低版本IOS设备上有更好的兼容性
+            type: 'binary'
+        } as any
+        const wbout = XLSX.write(workbook, wopts);
+        // 字符串转ArrayBuffer
+        const s2ab = (s: string) => {
+            let buf = new ArrayBuffer(s.length);
+            let view = new Uint8Array(buf);
+            for (let i = 0; i != s.length; ++i) view[i] = s.charCodeAt(i) & 0xFF;
+            return buf;
+        }
+        const blob = new Blob([s2ab(wbout)], { type: "application/octet-stream" });
+
+        return blob;
+    }
+    downloadFile(sheet2blob(sheet), fileName + '.xlsx')
+}

+ 87 - 0
src/services/laglat2gauss.ts

@@ -0,0 +1,87 @@
+// 大地坐标与经纬度坐标互转
+
+
+function GaussToBL(X,Y){
+    let ProjNo;
+    let ZoneWide; 带宽
+    let output = new Array(2);
+    let longitude1,latitude1, longitude0, X0,Y0, xval,yval;//latitude0,
+    let e1,e2,f,a, ee, NN, T,C, M, D,R,u,fai, iPI;
+    iPI = 3.14159265358979324/180.0; 3.1415926535898/180.0;
+    // a = 6378245.0; f = 1.0/298.3; //54年北京坐标系参数
+    a=6378140.0; f=1.0/298.257; //80年西安坐标系参数
+    ZoneWide = 6; 6度带宽
+    ProjNo = parseInt(X/1000000) ; //查找带号
+    longitude0 = (ProjNo-1) * ZoneWide + ZoneWide / 2;
+    longitude0 = longitude0 * iPI ; //中央经线
+
+
+    X0 = ProjNo*1000000+500000;
+    Y0 = 0;
+    xval = X-X0; yval = Y-Y0; //带内大地坐标
+    e2 = 2*f-f*f;
+    e1 = (1.0-Math.sqrt(1-e2))/(1.0+Math.sqrt(1-e2));
+    ee = e2/(1-e2);
+    M = yval;
+    u = M/(a*(1-e2/4-3*e2*e2/64-5*e2*e2*e2/256));
+    fai = u+(3*e1/2-27*e1*e1*e1/32)*Math.sin(2*u)+(21*e1*e1/16-55*e1*e1*e1*e1/32)*Math.sin(4*u) +(151*e1*e1*e1/96)*
+        Math.sin(6*u)+(1097*e1*e1*e1*e1/512)*Math.sin(8*u);
+    C = ee*Math.cos(fai)*Math.cos(fai);
+    T = Math.tan(fai)*Math.tan(fai);
+    NN = a/Math.sqrt(1.0-e2*Math.sin(fai)*Math.sin(fai));
+    R = a*(1-e2)/Math.sqrt((1-e2*Math.sin(fai)*Math.sin(fai))*(1-e2*Math.sin(fai)*Math.sin(fai))*(1-e2*Math.sin
+    (fai)*Math.sin(fai)));
+    D = xval/NN;
+    //计算经度(Longitude) 纬度(Latitude)
+    longitude1 = longitude0+(D-(1+2*T+C)*D*D*D/6+(5-2*C+28*T-3*C*C+8*ee+24*T*T)*D
+        *D*D*D*D/120)/Math.cos(fai);
+    latitude1 = fai -(NN*Math.tan(fai)/R)*(D*D/2-(5+3*T+10*C-4*C*C-9*ee)*D*D*D*D/24
+        +(61+90*T+298*C+45*T*T-256*ee-3*C*C)*D*D*D*D*D*D/720);
+    //转换为度 DD
+    output[0] = longitude1 / iPI;
+    output[1] = latitude1 / iPI;
+    return output;
+}
+
+//经纬度=>高斯投影
+function BLToGauss(longitude, latitude){
+    let ProjNo=0;
+    let ZoneWide; 带宽
+    let ret=Array(2);
+    let longitude1,latitude1, longitude0,latitude0, X0,Y0, xval,yval;
+    let a,f, e2,ee, NN, T,C,A, M, iPI;
+    iPI = 0.0174532925199433; 3.1415926535898/180.0;
+    ZoneWide = 6; 6度带宽
+    // a=6378245.0; f=1.0/298.3; //54年北京坐标系参数
+    a=6378140.0; f=1/298.257; //80年西安坐标系参数
+    ProjNo = parseInt(longitude / ZoneWide) ;
+    longitude0 = ProjNo * ZoneWide + ZoneWide / 2;
+    longitude0 = longitude0 * iPI ;
+    latitude0 = 0;
+    longitude1 = longitude * iPI ; //经度转换为弧度
+    latitude1 = latitude * iPI ; //纬度转换为弧度
+    e2=2*f-f*f;
+    ee=e2*(1.0-e2);
+    NN=a/Math.sqrt(1.0-e2*Math.sin(latitude1)*Math.sin(latitude1));
+    T=Math.tan(latitude1)*Math.tan(latitude1);
+    C=ee*Math.cos(latitude1)*Math.cos(latitude1);
+    A=(longitude1-longitude0)*Math.cos(latitude1);
+    M=a*((1-e2/4-3*e2*e2/64-5*e2*e2*e2/256)*latitude1-(3*e2/8+3*e2*e2/32+45*e2*e2
+        *e2/1024)*Math.sin(2*latitude1)
+        +(15*e2*e2/256+45*e2*e2*e2/1024)*Math.sin(4*latitude1)-(35*e2*e2*e2/3072)*Math.sin(6*latitude1));
+    xval = NN*(A+(1-T+C)*A*A*A/6+(5-18*T+T*T+72*C-58*ee)*A*A*A*A*A/120);
+    yval = M+NN*Math.tan(latitude1)*(A*A/2+(5-T+9*C+4*C*C)*A*A*A*A/24
+        +(61-58*T+T*T+600*C-330*ee)*A*A*A*A*A*A/720);
+    X0 = 1000000*(ProjNo+1)+500000;
+    Y0 = 0;
+    xval = xval+X0; yval = yval+Y0;
+    ret[0]=xval;
+    ret[1]=yval;
+
+    return ret;
+}
+
+export default {
+    GaussToBL,
+    BLToGauss,
+}

+ 91 - 0
src/services/net.service.ts

@@ -0,0 +1,91 @@
+import { injectable, Service } from './service'
+
+/** api接口返回值类型 */
+declare type NetResult = {
+  success: boolean,
+  data: Any
+}
+
+/**
+ * API网络请求服务
+ */
+@injectable
+export default class NetService extends Service {
+  protected url = import.meta.env.VITE_PROXY_URL
+
+  /**
+   * post方法请求接口
+   * @param url 接口地址
+   * @param params 接口参数
+   */
+  post(url: string, params: Any = {}, timeout = 6000): Promise<any> {
+    return this.fetch(url, { method: 'POST', body: JSON.stringify(params) }, timeout)
+  }
+
+  /**
+   * get请求api接口
+   * @param url 接口地址
+   */
+  get(url: string, timeout = 6000): Promise<NetResult> {
+    return this.fetch(url, { method: 'GET' }, timeout)
+  }
+
+  /**
+   * del请求api接口
+   * @param url 接口地址
+   */
+  del(url: string, timeout = 6000): Promise<NetResult> {
+    return this.fetch(url, { method: 'DELETE' }, timeout)
+  }
+
+  /**
+   * post方法请求接口
+   * @param url 接口地址
+   * @param params 接口参数
+   */
+  put(url: string, params: Any = {}, timeout = 6000): Promise<NetResult> {
+    return this.fetch(url, { method: 'PUT', body: JSON.stringify(params) }, timeout)
+  }
+
+  /** token 续期 */
+  async refresh(token: string) {
+    this.post('', { token }).then((res) => {
+      console.log('刷新token', res)
+    })
+  }
+
+  /**
+   * fetch
+   * @param url
+   * @param opt
+   * @param timeout 0 默认不超时
+   * @returns
+   */
+  private fetch(url: string, opt: RequestInit, timeout = 0): Promise<NetResult> {
+    return new Promise((resolve) => {
+      const controller = new AbortController()
+      const { signal } = controller
+      if (timeout) {
+        setTimeout(() => {
+          controller.abort()
+        }, timeout)
+      }
+      fetch(this.url + url, {
+        signal,
+        ...opt,
+        headers: opt.headers || { 'Content-Type': 'application/json' }
+      }).then((res) => {
+        if (res.status === 500) return { success: false, data: '请求错误' }
+        return res.json()
+      }).then((res) => {
+        const obj = { data: res.data, success: res.data instanceof Object }
+        if (!obj.success && obj.data.includes('need authrize')) {
+          throw 401
+        }
+        resolve(obj)
+      }).catch((er) => {
+        throw er
+      })
+    })
+  }
+}

+ 64 - 0
src/services/service.ts

@@ -0,0 +1,64 @@
+export class ServiceError extends Error {
+  code?: number
+
+  origin?: string
+
+  constructor(message: string, origin?: string, code?: number) {
+    super(message)
+    this.code = code
+    this.origin = origin
+    this.stack = `${this.message}\n${this.origin}\n${this.code || ''}`
+  }
+
+  toString() {
+    return this.message
+  }
+}
+
+export class Service {
+  throw(message: string, origin?: string, code?: number) {
+    throw new ServiceError(message, origin, code)
+  }
+}
+
+export function injectable<T extends { new(..._args: any[]): {} }> (Ctor: T) {
+  let instance!: any
+  return new Proxy(Ctor, {
+    construct(t, args) {
+      if (!instance) {
+        instance = new Ctor(args)
+        // console.log('instance ' + Ctor.name)
+      }
+      return instance
+    }
+  })
+}
+const runnerMap: { [key: string]: ((_res: any) => void)[] | undefined } = {}
+/**
+   * 互斥注解
+   * 用于保证某个方法同一时间只有单次调用
+   */
+export function mutex(target: any, property: string) {
+  const oriFn = target[property]
+  const funcKey = `${target.constructor.name}-${property}`
+  Object.defineProperty(target, property, {
+    async value(...args: any[]) {
+      const key = funcKey + JSON.stringify(args)
+      if (runnerMap[key]) {
+        return await new Promise((res) => {
+          runnerMap[key]?.push((result: any) => res(result))
+        })
+      }
+      runnerMap[key] = []
+
+      setTimeout(() => {
+        runnerMap[key] = undefined
+      }, 4000)
+      const res = await Reflect.apply(oriFn, this, args || [])
+      runnerMap[key]?.forEach((fn) => fn(res))
+      runnerMap[key] = undefined
+      return res
+    }
+  })
+  return target[property]
+}

+ 12 - 0
src/vite-env.d.ts

@@ -0,0 +1,12 @@
+/// <reference types="vite/client" />
+
+/** 代指任意类型 请不要随意使用 */
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+declare type Any = any
+
+declare module '*.vue' {
+  import type { DefineComponent } from 'vue'
+  const component: DefineComponent<{}, {}, Any>
+  export default component
+}
+

+ 37 - 0
tsconfig.json

@@ -0,0 +1,37 @@
+{
+  "compilerOptions": {
+    "target": "ESNext",
+    "useDefineForClassFields": true,
+    "experimentalDecorators": true,
+    "module": "ESNext",
+    "moduleResolution": "Node",
+    "strict": true,
+    "jsx": "preserve",
+    "sourceMap": true,
+    "resolveJsonModule": true,
+    "isolatedModules": true,
+    "esModuleInterop": true,
+    "lib": [
+      "ESNext",
+      "DOM"
+    ],
+    "skipLibCheck": true,
+    "baseUrl": "./",
+    "paths": {
+      "@/*": [
+        "src/*"
+      ]
+    },
+  },
+  "include": [
+    "src/**/*.ts",
+    "src/**/*.d.ts",
+    "src/**/*.tsx",
+    "src/**/*.vue"
+  ],
+  "references": [
+    {
+      "path": "./tsconfig.node.json"
+    }
+  ]
+}

+ 9 - 0
tsconfig.node.json

@@ -0,0 +1,9 @@
+{
+  "compilerOptions": {
+    "composite": true,
+    "module": "ESNext",
+    "moduleResolution": "Node",
+    "allowSyntheticDefaultImports": true
+  },
+  "include": ["vite.config.ts"]
+}

+ 76 - 0
vite.config.ts

@@ -0,0 +1,76 @@
+/* eslint-disable import/no-extraneous-dependencies */
+import { defineConfig, loadEnv } from 'vite'
+import vue from '@vitejs/plugin-vue'
+import eslint from 'vite-plugin-eslint'
+import viteCompression from 'vite-plugin-compression'
+import { resolve } from 'path'
+import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
+
+// https://vitejs.dev/config/
+export default ({ mode }) => {
+  const env = loadEnv(mode, process.cwd())
+  return defineConfig({
+    base: './',
+    resolve: {
+      alias: {
+        /*
+          路径别名
+          若为文件系统路径必须是绝对路径的形式,否则将以别名原样呈现,不会解析为文件系统路径路径
+        */
+        '@': resolve(__dirname, './src')
+      }
+    },
+    plugins: [
+      vue(), viteCompression(),
+      eslint({ fix: true, include: [ '**/*.ts', '**/*.vue' ] }),
+      createSvgIconsPlugin({
+        iconDirs: [ resolve(__dirname, './src/assets/icons') ],
+        // Specify symbolId format
+        symbolId: 'icon-[dir]-[name]'
+      })
+    ],
+    server: {
+      host: '0.0.0.0',
+      port: 6888,
+      open: true,
+      strictPort: false,
+      https: false,
+      // proxy: {
+      //   '/api': {
+      //     target: env.VITE_PROXY_URL,
+      //     changeOrigin: true,
+      //     rewrite: (path) => path.replace(/^\/api/, ''),
+      //     secure: false,
+      //     headers: {
+      //       Referer: 'https://example.com'
+      //     }
+      //   }
+      // }
+    },
+    esbuild: {
+      drop: mode === 'development' ? [ 'debugger' ] : [ 'debugger', 'console' ] // build 移除打印
+    },
+    build: {
+      rollupOptions: {
+        input: {
+          index: resolve(__dirname, 'index.html')
+        },
+        output: { // 静态资源分类打包
+          chunkFileNames: 'js/[hash].js',
+          entryFileNames: 'js/[hash].js',
+          assetFileNames: 'assets/[ext]/[hash].[ext]',
+          // 拆分node_modules包
+          manualChunks: (id: any) => {
+            if (id.includes('node_modules')) {
+              return id.toString().split('node_modules/')[1].split('/')[0].toString()
+            }
+            return ''
+          }
+        }
+      }
+    },
+    define: {
+      __VUE_OPTIONS_API__: false
+    }
+  })
+}