Browse Source

移除原utils ,改用注入方式

Caner 3 years ago
parent
commit
26042428c0

+ 3 - 0
README.md

@@ -95,3 +95,6 @@ import { onMounted, ref, computed} from 'vue'
 * 颜色赋值使用 `var(--color-a)` 写法,无特殊说明不允许直接赋值
 * `class, id` 命名必须使用短线连接单词方式,如 `form-label`, `list-item-title` 等
 * 多层级类名嵌套时,必须使用`scss`嵌套写法,以免污染其他样式
+
+### Notice
+* 框架使用解构,注入方式

+ 0 - 1
package.json

@@ -9,7 +9,6 @@
     "preview": "vite preview"
   },
   "dependencies": {
-    "js-md5": "^0.7.3",
     "naive-ui": "^2.33.5",
     "pinia": "^2.0.18",
     "vue": "^3.2.37",

+ 208 - 0
src/services/export.service.ts

@@ -0,0 +1,208 @@
+import { injectable, Service } from './service'
+import * as XLSX from 'xlsx'
+import html2canvas from 'html2canvas'
+import JSPDF from 'jspdf'
+
+/** 文件导出方法 */
+@injectable
+export default class EexportService extends Service {
+
+    /**
+     * 自动下载
+     * @param url 保存地址|blob
+     * @param saveName 文件名
+     */
+    download(url: string | Blob | object, saveName: string) {
+        if (typeof url === 'object' && url instanceof Blob) {
+            url = URL.createObjectURL(url) // 创建blob地址
+        }
+        const aLink = document.createElement('a')
+        aLink.href = url as any
+        aLink.download = saveName || '' // HTML5新增的属性,指定保存文件名,可以不要后缀,注意,file:///模式下不会生效
+        let event
+        if (window.MouseEvent) event = new MouseEvent('click')
+        else {
+            event = document.createEvent('MouseEvents')
+            event.initMouseEvent('click', true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null)
+        }
+        aLink.dispatchEvent(event)
+    }
+
+    /**
+     * 解析excel表格
+     * @param file 文件
+     * @returns
+     */
+    exp2json = async (file: File) => 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)
+        }
+    })
+
+    /**
+     * 导出excel表格
+     * @param arr 数据是数组包含的对象
+     * @param fileName 名字
+     */
+    exp2excel = (arr: object[], fileName: string, cellMerges?: Array<any>) => {
+        const sheet = XLSX.utils.json_to_sheet(arr)
+        // excel宽高设置
+        sheet['!cols'] = arr.map(() => ({ wch: 30 }))
+        if (cellMerges) {
+            sheet['!merges'] = cellMerges // <====合并单元格
+        }
+        // 转blob
+        const sheet2blob = (sheets: any, sheetName = 'sheet1') => {
+            const workbook = {
+                SheetNames: [sheetName],
+                Sheets: {} as any
+            }
+            workbook.Sheets[sheetName] = sheets
+            // 生成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) => {
+                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(wbout)], { type: 'application/octet-stream' })
+
+            return blob
+        }
+        this.download(sheet2blob(sheet), `${fileName}.xlsx`)
+    }
+
+    /**
+     * dom导出excel
+     * @param domID domID
+     * @param fileName 文件名
+     */
+    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' })
+        this.download(blob, `${fileName}.xlsx`)
+    }
+
+    /**
+     * 导出PDF
+     * @param domID 需要输出PDF的页面id
+     * @param fileName 文件名
+     * @param type  默认A4分页
+     * @param wMultiple 宽倍数
+     * @param hMultiple 高倍数
+     * @returns
+     */
+    exp2pdf = async (domID: string, fileName: string, type = 'A4', wMultiple = null, hMultiple = null) => {
+        const dom = document.getElementById(domID)
+        if (!dom) return
+        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)
+        }
+    }
+
+    /**
+     * 导出PNG
+     * @param domID 需要输出PDF的页面id
+     * @param fileName 文件名
+     * @param bkcolor 背景色
+     */
+    exp2png = async (domID: string, fileName: string, bkcolor: string) => {
+        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, // 缩放3倍,使得图片更加清晰=>越清晰图片越大
+            width: imgWidth,
+            height: imgHeight,
+            imageTimeout: 5000 // 设置图片的超时,设置0为禁用
+        })
+
+        // 两种下载方式url + blob
+        let imgURL = canvas.toDataURL('image/png') as any
+        if (typeof imgURL === 'object' && imgURL instanceof Blob) {
+            imgURL = URL.createObjectURL(imgURL) // 创建blob地址
+            this.download(imgURL, fileName)
+        } else {
+            // url  +  请求得到blob
+            const htmlrq = new XMLHttpRequest() as any
+            htmlrq.open('GET', imgURL, true)
+            htmlrq.responseType = 'blob'
+            htmlrq.onload = (e: { target: { status: number; response: Blob | MediaSource; } }) => {
+                if (e.target.status === 200) {
+                    imgURL = URL.createObjectURL(e.target.response) // 创建blob地址
+                    this.download(imgURL, fileName)
+                } else {
+                    console.error('下载错误')
+                }
+            }
+            htmlrq.send()
+        }
+    }
+}

+ 245 - 0
src/services/fn.service.ts

@@ -0,0 +1,245 @@
+import { injectable, Service } from './service'
+
+/** 常用方法 */
+@injectable
+export default class FnService extends Service {
+
+    /**
+      * 格式化日期
+      * @param date Date
+      * @param fmt 日期格式
+      * @returns 
+      */
+    fomartTime(date: Date, fmt = 'yyyy-MM-dd hh:mm:ss') {
+        if (!date) return ''
+        const o = {
+            'M+': date.getMonth() + 1,                 //月份 
+            'd+': date.getDate(),                    //日 
+            'h+': date.getHours(),                   //小时 
+            'm+': date.getMinutes(),                 //分 
+            's+': date.getSeconds(),                 //秒 
+            'q+': Math.floor((date.getMonth() + 3) / 3), //季度 
+            'S': date.getMilliseconds()             //毫秒 
+        } as Any
+        if (/(y+)/.test(fmt)) {
+            fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length))
+        }
+        for (let k in o) {
+            if (new RegExp('(' + k + ')').test(fmt)) {
+                fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (('00' + o[k]).substr(('' + o[k]).length)))
+            }
+        }
+        return fmt
+    }
+
+    /**
+      * 距当前时间点的时长
+      * @prama time 13位时间戳
+      * @return str x秒 / x分钟 / x小时
+      */
+    timeDuration(datetime: string) {
+        if (datetime == null) return ''
+
+        datetime = datetime.replace(/-/g, '/')
+
+        const time = new Date()
+        let outTime = new Date(datetime)
+        if (/^[1-9]\d*$/.test(datetime)) {
+            outTime = new Date(parseInt(datetime) * 1000)
+        }
+
+        if (
+            time.getTime() < outTime.getTime() ||
+            time.getFullYear() !== outTime.getFullYear()
+        ) {
+            return this.fomartTime(outTime, 'yyyy-MM-dd hh:mm')
+        }
+
+        if (time.getMonth() !== outTime.getMonth()) {
+            return this.fomartTime(outTime, 'MM-dd hh:mm')
+        }
+
+        if (time.getDate() !== outTime.getDate()) {
+            const day = outTime.getDate() - time.getDate()
+            if (day === -1) {
+                return this.fomartTime(outTime, '昨天 hh:mm')
+            }
+
+            if (day === -2) {
+                return this.fomartTime(outTime, '前天 hh:mm')
+            }
+
+            return this.fomartTime(outTime, 'MM-dd hh:mm')
+        }
+
+        const diff = time.getTime() - outTime.getTime()
+
+        if (time.getHours() !== outTime.getHours() || diff > 30 * 60 * 1000) {
+            return this.fomartTime(outTime, 'hh:mm')
+        }
+
+        let minutes = outTime.getMinutes() - time.getMinutes()
+        if (minutes === 0) {
+            return '刚刚'
+        }
+
+        minutes = Math.abs(minutes)
+        return `${minutes}分钟前`
+    }
+
+    /**
+     * 简单防抖
+     * @param fn 
+     * @param interval 
+     * @returns 
+     */
+    debounce(fn: (...arg: Any[]) => Any, duration = 300) {
+        let timer = -1
+        return function (this: unknown, ...args: Any[]) {
+            if (timer > -1) {
+                clearTimeout(timer)
+            }
+            timer = window.setTimeout(() => {
+                fn.bind(this)(...args)
+                timer = -1
+            }, duration)
+        }
+    }
+
+    /**
+     * 简单节流
+     * @param fn 
+     * @param interval 
+     * @returns 
+     */
+    throttle(fn: (...arg: Any[]) => Any, interval = 300) {
+        let lock = false
+        return function (this: unknown, ...args: Any[]) {
+            if (lock) return
+            lock = true
+            setTimeout(() => lock = false, interval)
+            fn.bind(this)(...args)
+        }
+    }
+
+    /**
+     * base64 转file
+     * @param dataurl base64
+     * @param filename 
+     * @returns 
+     */
+    base64ImgtoFile(base64: string, filename = 'file') {
+        const arr = base64.split(',') as Any
+        const mime = arr[0].match(/:(.*?);/)[1]
+        const suffix = mime.split('/')[1]
+        const bstr = atob(arr[1])
+        let n = bstr.length
+        const u8arr = new Uint8Array(n)
+        while (n--) {
+            u8arr[n] = bstr.charCodeAt(n)
+        }
+        return new File([u8arr], `${filename}.${suffix}`, {
+            type: mime
+        })
+    }
+
+    /**
+     *  关键字去重
+     * @param arr 数组
+     * @param key 关键字
+     * @returns 去重后
+     */
+    unique(arr: Array<any>, key: string) {
+        const res = []
+        const obj = {} as any
+        for (let i = 0; i < arr.length; i++) {
+            if (!obj[arr[i][key]]) {
+                res.push(arr[i])
+                obj[arr[i][key]] = true
+            }
+        }
+        return res
+    }
+
+    /**
+     * 同步睡眠
+     * @param ms 毫秒
+     */
+    sleep = (ms: number) => new Promise((resolve) => { setTimeout(resolve, ms) })
+
+    /**
+     * 多维数组转一维
+     * @param arr
+     * @returns
+     */
+    flatten: Any = (arr: Array<any>) => [].concat(...arr.map((x) => (Array.isArray(x) ? this.flatten(x) : x)))
+
+    /**
+     * 多字段匹配
+     * @param  data 数组|数组对象
+     * @param  key
+     * @returns
+     */
+    search(data: Array<any>, key: string) {
+        const list = data.filter((el) => {
+            let bt = ''
+            bt += el.name
+            bt += el.eg
+            return bt.match(key)
+        })
+        return list
+    }
+
+    /**
+    * 格式化掌子面里程
+    * @param num 里程
+    * @returns
+    */
+    mileage2string(num: number) {
+        const a = Math.floor(num / 1000).toString()
+        const ab = parseFloat((num % 1000).toFixed(1))
+        const b = Math.floor(num % 1000).toString()
+        const c = b.length === 1 ? `00${ab}` : b.length === 2 ? `0${ab}` : ab
+        return `${a}+${c}`
+    }
+
+    /**
+     * 对象数组去重
+     * @param arr 传入数组
+     * @param key 需要对比的键
+     * @returns 去重后的数组
+     */
+    uniqueArr<T = any>(arr: T[], key: keyof T) {
+        const newArr = [] as T[]
+        const valueList = [] as any[]
+
+        for (const item of arr) {
+            if (valueList.indexOf(item[key]) === -1) {
+                valueList.push(item[key])
+                newArr.push(item)
+            }
+        }
+        return newArr
+    }
+
+    /**
+     * 数组按指定key值分组
+     * @param {*} array
+     * @param {*} id
+     * @returns
+     */
+    groupBy(array: Array<any>, id: string) {
+        const groups = {} as any
+        array.forEach((o) => {
+            let group = JSON.stringify(o[id])
+            if (typeof o[id] === 'string') {
+                group = o[id]
+            }
+            groups[group] = groups[group] || []
+            groups[group].push(o)
+        })
+        // return Object.values(groups);
+        return groups
+    }
+    
+}

+ 0 - 0
src/utils/rem.ts → src/services/rem.ts


+ 313 - 0
src/services/request.service.ts

@@ -0,0 +1,313 @@
+import { injectable, Service } from './service'
+const urls = import.meta.env.VITE_PROXY_URL
+/** api接口返回值类型 */
+declare type NetResult = {
+  success: boolean,
+  data: Any,
+  msg: string,
+  code: number
+}
+
+const NET_ERRORS = {
+  400: '请求格式错误',
+  401: '权限错误',
+  404: '接口未定义',
+  405: '请求方法无效',
+  500: '系统错误'
+} as Any
+
+/**
+ * API网络请求服务
+ */
+@injectable
+export default class NetService extends Service {
+  /**
+   * post方法请求接口
+   * @param url 接口地址
+   * @param params 接口参数
+   */
+  post(url: string, params: Any = {}, timeout = 6000): Promise<NetResult> {
+    const newToken = ''
+    return new Promise((resolve) => {
+      const controller = new AbortController()
+      const { signal } = controller
+      setTimeout(() => {
+        controller.abort()
+      }, timeout)
+      fetch(urls + url, {
+        signal,
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json',
+          Authorization: newToken
+        },
+        body: JSON.stringify(params)
+      }).then(res => {
+        if (res.status === 401) {
+          throw res.status
+        } else if (res.status === 404) {
+          // throw new Error(`接口'${url}'未定义`)
+          this.throw(`接口'${url}'未定义`, import.meta.url, 404)
+        }
+        return res.json()
+      }).then(res => {
+        if (res.code !== 200) {
+          // throw new Error(res.msg)
+          this.throw(res.msg, import.meta.url, 404)
+        }
+        resolve({
+          success: res?.success,
+          data: res?.data || null,
+          msg: res?.msg || null,
+          code: res?.code || -1
+        })
+      }).catch((err: Any) => {
+        if (err.code === 20) {
+          throw '网络请求超时'
+        }
+        if (err === 401) {
+          throw err
+        }
+        resolve({
+          success: false,
+          data: null,
+          msg: `接口 [${url}] 错误: ${err.message || err}`,
+          code: -1
+        })
+        throw err
+      })
+    })
+  }
+
+  /**
+   * get请求api接口
+   * @param url 接口地址
+   */
+  get(url: string, params: Any = {}, timeout = 6000): Promise<NetResult> {
+    let urlParams = ''
+    const newToken = ''
+    for (const key in params) {
+      if (params[key]) {
+        urlParams += (urlParams.includes('?') ? '&' : '?')
+        urlParams += key + '=' + params[key]
+      }
+    }
+    return new Promise((resolve) => {
+      const controller = new AbortController()
+      const { signal } = controller
+      setTimeout(() => {
+        controller.abort()
+      }, timeout)
+      fetch(urls + url + urlParams, {
+        signal,
+        method: 'get',
+        headers: {
+          'Content-Type': 'application/json',
+          Authorization: newToken
+        }
+      }).then(res => {
+        if (res.status !== 200) {
+          throw res.status
+        }
+        return res.json()
+      }).then(res => {
+        resolve({
+          success: res?.success,
+          data: res?.data || null,
+          msg: res?.msg || null,
+          code: res?.code || -1
+        })
+      }).catch((err: Any) => {
+        if (err.code === 20) {
+          throw '网络请求超时'
+        }
+        let error = err?.message || err
+        if (typeof err === 'number') {
+          if (err === 401 || err === 404) {
+            throw err
+          }
+          error = NET_ERRORS[err]
+        }
+        resolve({
+          success: false,
+          data: null,
+          msg: `接口 [${url.split('?')[0]}] 错误: ${error}`,
+          code: -1
+        })
+      })
+    })
+  }
+
+  /**
+   * del请求api接口
+   * @param url 接口地址
+   */
+  del(url: string, timeout = 6000): Promise<NetResult> {
+    const newToken = ''
+    return new Promise((resolve) => {
+      const controller = new AbortController()
+      const { signal } = controller
+      setTimeout(() => {
+        controller.abort()
+      }, timeout)
+      fetch(urls + url, {
+        signal,
+        method: 'DELETE',
+        headers: {
+          'Content-Type': 'application/json',
+          Authorization: newToken
+        }
+      }).then(res => {
+        if (res.status !== 200) {
+          throw res.status
+        }
+        return res.json()
+      }).then(res => {
+        resolve({
+          success: res?.success,
+          data: res?.data || null,
+          msg: res?.msg || null,
+          code: res?.code || -1
+        })
+      }).catch((err: Any) => {
+        if (err.code === 20) {
+          throw '网络请求超时'
+        }
+        let error = err?.message
+        if (typeof err === 'number') {
+          if (err === 401 || err === 404) {
+            throw err
+          }
+          error = NET_ERRORS[err]
+        }
+        resolve({
+          success: false,
+          data: null,
+          msg: `接口 [${url.split('?')[0]}] 错误: ${error}`,
+          code: -1
+        })
+      })
+    })
+  }
+
+  /**
+   * post方法请求接口
+   * @param url 接口地址
+   * @param params 接口参数
+   */
+  put(url: string, params: Any = {}, timeout = 6000): Promise<NetResult> {
+    const newToken = ''
+    return new Promise((resolve) => {
+      const controller = new AbortController()
+      const { signal } = controller
+      setTimeout(() => {
+        controller.abort()
+      }, timeout)
+      fetch(urls + url, {
+        signal,
+        method: 'put',
+        headers: {
+          'Content-Type': 'application/json',
+          Authorization: newToken
+        },
+        body: JSON.stringify(params)
+      }).then(res => {
+        if (res.status === 401) {
+          throw res.status
+        } else if (res.status === 404) {
+          // throw new Error(`接口'${url}'未定义`)
+          this.throw(`接口'${url}'未定义`, import.meta.url, 404)
+        }
+        return res.json()
+      }).then(res => {
+        if (res.code !== 200) {
+          // throw new Error(res.msg)
+          this.throw(res.msg, import.meta.url, 404)
+        }
+        resolve({
+          success: res?.success,
+          data: res?.data || null,
+          msg: res?.msg || null,
+          code: res?.code || -1
+        })
+      }).catch((err: Any) => {
+        if (err.code === 20) {
+          throw '网络请求超时'
+        }
+        if (err === 401) {
+          throw err
+        }
+        resolve({
+          success: false,
+          data: null,
+          msg: `接口 [${url}] 错误: ${err.message}`,
+          code: -1
+        })
+        throw err
+      })
+    })
+  }
+
+
+  /**
+   * upload上传文件接口
+   * @param url 接口地址
+   * @param params 接口参数
+   */
+  upload(url: string, params: Any, timeout = 6000): Promise<NetResult> {
+    const newToken = ''
+    return new Promise((resolve) => {
+      const controller = new AbortController()
+      const { signal } = controller
+      setTimeout(() => {
+        controller.abort()
+      }, timeout)
+
+      fetch(urls + url, {
+        signal,
+        method: 'POST',
+        headers: { Authorization: newToken },
+        body: params
+      }).then(res => {
+        if (res.status === 401) {
+          throw res.status
+        } else if (res.status === 404) {
+          // throw new Error(`接口'${url}'未定义`)
+          this.throw(`接口'${url}'未定义`, import.meta.url, 404)
+        }
+        return res.json()
+      }).then(res => {
+        if (res.code !== 200) {
+          // throw new Error(res.msg)
+          this.throw(res.msg, import.meta.url, 404)
+        }
+        resolve({
+          success: res?.success,
+          data: res?.data || null,
+          msg: res?.msg || null,
+          code: res?.code || -1
+        })
+      }).catch((err: Any) => {
+        if (err.code === 20) {
+          throw '网络请求超时'
+        }
+        if (err === 401) {
+          throw err
+        }
+        resolve({
+          success: false,
+          data: null,
+          msg: `接口 [${url}] 错误: ${err.message || err}`,
+          code: -1
+        })
+        throw err
+      })
+    })
+  }
+
+
+  /** token 续期 */
+  async refresh() {
+    await this.put('/upms/v3/auth/check_token')
+  }
+}

+ 63 - 0
src/services/service.ts

@@ -0,0 +1,63 @@
+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, {
+      value: async function (...args: Any[]) {
+        const key = funcKey + JSON.stringify(args)
+        if (runnerMap[key]) {
+          return await new Promise(res => {
+            runnerMap[key]?.push((result: Any) => res(result))
+          })
+        } else {
+          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]
+  }

+ 161 - 0
src/services/vue.directive.service.ts

@@ -0,0 +1,161 @@
+import { injectable, Service } from './service'
+
+/** vue 自定义指令 */
+@injectable
+export default class directiveService extends Service {
+    /** 长按 */
+    longpress() {
+        return {
+            beforeMount(el: { $duration: number | undefined; addEventListener: (arg0: string, arg1: { (e: { type: string; button: number; preventDefault: () => void; }): void; (e: { type: string; button: number; preventDefault: () => void; }): void; (): void; (): void; (): void; }) => void; }, binding: { value: any; arg: number; }) {
+                const cb = binding.value;
+                el.$duration = binding.arg || 3000; // 获取长按时长, 默认3秒执行长按事件
+                if (typeof cb !== 'function') return console.warn('v-longpress指令必须接收一个回调函数');
+                let timer: string | number | NodeJS.Timeout | null | undefined = null;
+                const add: Any = (e: { type: string; button: number; preventDefault: () => void; }) => {
+                    // 排除点击与右键情况, event.button: 0-左键  2-右键
+                    if (e.type === 'click' && e.button !== 0) return;
+                    e.preventDefault();
+                    if (timer === null) {
+                        timer = setTimeout(() => {
+                            cb('start');
+                            timer = null;
+                        }, el.$duration)
+                    }
+                }
+                const cancel = () => {
+                    if (timer !== null) {
+                        clearTimeout(timer);
+                        timer = null;
+                    }
+                    cb('end');
+                }
+
+                // 添加计时器
+                el.addEventListener('mousedown', add);
+                el.addEventListener('touchstart', add);
+                // 取消计时器
+                el.addEventListener('mouseout', cancel);
+                el.addEventListener('touchend', cancel)
+                el.addEventListener('touchcancel', cancel)
+            },
+            updated(el: { $duration: any; }, binding: { arg: any; }) {
+                // 可以实时更新时长
+                el.$duration = binding.arg;
+            },
+            unmounted(el: { removeEventListener: (arg0: string, arg1: { (): void; (): void; (): void; (): void; (): void; (): void; }) => void; }) {
+                el.removeEventListener('mousedown', () => { });
+                el.removeEventListener('touchstart', () => { });
+                el.removeEventListener('click', () => { });
+                el.removeEventListener('mouseout', () => { });
+                el.removeEventListener('touchend', () => { });
+                el.removeEventListener('touchcancel', () => { });
+            }
+        }
+    }
+
+    /** 拖拽 */
+    drag() {
+        return {
+            mounted(el: HTMLElement | null) {
+                const oDiv = el // 当前元素
+                el!.style.pointerEvents = 'none' // 防止触发点击事件
+                oDiv!.onmousedown = e => {
+                    // 找父级 是否是absolute来进行移动
+                    let target = oDiv
+                    while (window.getComputedStyle(target!).position !== 'absolute' && target !== document.body) {
+                        target = target!.parentElement
+                    }
+                    // 找父级 是否是relative来进行宽度计算
+                    let targetParent = target!.parentElement
+                    while (window.getComputedStyle(targetParent!).position !== 'relative' && targetParent !== document.body) {
+                        targetParent = targetParent!.parentElement
+                    }
+
+                    document.onselectstart = () => {
+                        return false
+                    }
+
+                    // 鼠标按下,计算当前元素距离可视区的距离
+                    const disX = e.clientX - target!.offsetLeft
+                    const disY = e.clientY - target!.offsetTop
+                    const cW = +window.getComputedStyle(target!).width.replace('px', '')
+                    const cH = +window.getComputedStyle(target!).height.replace('px', '')
+                    const maxW = +window.getComputedStyle(targetParent!).width.replace('px', '')
+                    const maxH = +window.getComputedStyle(targetParent!).height.replace('px', '')
+                    document.onmousemove = e => {
+                        // 通过事件委托,计算移动的距离
+                        // 因为浏览器里并不能直接取到并且使用clientX、clientY,所以使用事件委托在内部做完赋值
+                        el!.style.pointerEvents = 'none'
+                        const l = e.clientX - disX
+                        const t = e.clientY - disY
+                        const cl = maxW - cW
+                        const ct = maxH - cH
+                        // 计算移动当前元素的位置,并且给该元素样式中的left和top值赋值
+                        target!.style.left = (l < 0 ? 0 : l > cl ? cl : l) + 'px'
+                        target!.style.top = (t < 0 ? 0 : t > ct ? ct : t) + 'px'
+                    }
+                    document.onmouseup = e => {
+                        el!.style.pointerEvents = 'none'
+                        document.onmousemove = null
+                        document.onmouseup = null
+                        document.onselectstart = null
+                    }
+                    // return false不加的话可能导致黏连,拖到一个地方时div粘在鼠标上不下来,相当于onmouseup失效
+                    return false
+                }
+            }
+        }
+
+    }
+
+    /** 点击其它地方消失 */
+    clickoutside() {
+        return {
+            beforeMount(el: { contains: (arg0: EventTarget | null) => any; }, binding: { value: () => any; }) {
+                document.addEventListener('click', (e) => {
+                    el.contains(e.target) && binding.value();
+                }, false)
+            },
+            unmounted() {
+                document.removeEventListener('click', () => { })
+            }
+        }
+    }
+
+    /** 复制 */
+    copy() {
+        return {
+            beforeMount(el: { targetContent: string; addEventListener: (arg0: string, arg1: () => void) => void; }, binding: { arg: Any; value: Any; }) {
+                el.targetContent = binding.value;
+                const success = binding.arg;
+                el.addEventListener('click', () => {
+                    if (!el.targetContent) return console.warn('没有需要复制的目标内容');
+                    // 创建textarea标签
+                    const textarea = document.createElement('textarea');
+                    // 设置相关属性
+                    textarea.readOnly = true;
+                    textarea.style.position = 'fixed';
+                    textarea.style.top = '-99999px';
+                    // 把目标内容赋值给它的value属性
+                    textarea.value = el.targetContent;
+                    // 插入到页面
+                    document.body.appendChild(textarea);
+                    // 调用onselect()方法
+                    textarea.select();
+                    // 把目标内容复制进剪贴板, 该API会返回一个Boolean
+                    const res = document.execCommand('Copy');
+                    res && success ? success(el.targetContent) : console.log('复制成功,剪贴板内容:' + el.targetContent);
+                    // 移除textarea标签
+                    document.body.removeChild(textarea);
+                })
+            },
+            updated(el: { targetContent: any; }, binding: { value: any; }) {
+                // 实时更新最新的目标内容
+                el.targetContent = binding.value;
+            },
+            unmounted(el: { removeEventListener: (arg0: string, arg1: () => void) => void; }) {
+                el.removeEventListener('click', () => { })
+            }
+        }
+    }
+}

+ 0 - 202
src/utils/export-file.ts

@@ -1,202 +0,0 @@
-import * as XLSX from 'xlsx'
-import html2canvas from 'html2canvas'
-import JSPDF from 'jspdf'
-
-/**
- * 自动下载
- * @param url 保存地址|blob
- * @param saveName 文件名
- */
-const download = (url: string | Blob | object, saveName: string) => {
-  if (typeof url === 'object' && url instanceof Blob) {
-    url = URL.createObjectURL(url) // 创建blob地址
-  }
-  const aLink = document.createElement('a')
-  aLink.href = url as any
-  aLink.download = saveName || '' // HTML5新增的属性,指定保存文件名,可以不要后缀,注意,file:///模式下不会生效
-  let event
-  if (window.MouseEvent) event = new MouseEvent('click')
-  else {
-    event = document.createEvent('MouseEvents')
-    event.initMouseEvent('click', true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null)
-  }
-  aLink.dispatchEvent(event)
-}
-
-/**
- * 解析excel表格
- * @param file 文件
- * @returns
- */
-export const exp2json = async (file: File) => 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)
-  }
-})
-
-/**
- * 导出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(() => ({ wch: 30 }))
-  if (cellMerges) {
-    sheet['!merges'] = cellMerges // <====合并单元格
-  }
-  // 转blob
-  const sheet2blob = (sheets: any, sheetName = 'sheet1') => {
-    const workbook = {
-      SheetNames: [ sheetName ],
-      Sheets: {} as any
-    }
-    workbook.Sheets[sheetName] = sheets
-    // 生成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) => {
-      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(wbout) ], { type: 'application/octet-stream' })
-
-    return blob
-  }
-  download(sheet2blob(sheet), `${fileName}.xlsx`)
-}
-
-/**
- * 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' })
-  download(blob, `${fileName}.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
-  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)
-  }
-}
-
-/**
- * 导出PNG
- * @param domID 需要输出PDF的页面id
- * @param fileName 文件名
- * @param bkcolor 背景色
- */
-export const exp2png = async (domID: string, fileName: string, bkcolor: string) => {
-  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, // 缩放3倍,使得图片更加清晰=>越清晰图片越大
-    width: imgWidth,
-    height: imgHeight,
-    imageTimeout: 5000 // 设置图片的超时,设置0为禁用
-  })
-
-  // 两种下载方式url + blob
-  let imgURL = canvas.toDataURL('image/png') as any
-  if (typeof imgURL === 'object' && imgURL instanceof Blob) {
-    imgURL = URL.createObjectURL(imgURL) // 创建blob地址
-    download(imgURL, fileName)
-  } else {
-    // url  +  请求得到blob
-    const htmlrq = new XMLHttpRequest() as any
-    htmlrq.open('GET', imgURL, true)
-    htmlrq.responseType = 'blob'
-    htmlrq.onload = (e: { target: { status: number; response: Blob | MediaSource; } }) => {
-      if (e.target.status === 200) {
-        imgURL = URL.createObjectURL(e.target.response) // 创建blob地址
-        download(imgURL, fileName)
-      } else {
-        console.error('下载错误')
-      }
-    }
-    htmlrq.send()
-  }
-}

+ 0 - 139
src/utils/js-fn.ts

@@ -1,139 +0,0 @@
-/**
- *  关键字去重
- * @param arr 数组
- * @param key 关键字
- * @returns 去重后
- */
-export const unique = (arr: Array<any>, key: string) => {
-  const res = []
-  const obj = {} as any
-  for (let i = 0; i < arr.length; i++) {
-    if (!obj[arr[i][key]]) {
-      res.push(arr[i])
-      obj[arr[i][key]] = true
-    }
-  }
-  return res
-}
-
-/**
- * 同步睡眠
- * @param ms 毫秒
- */
-export const sleep = (ms: number) => new Promise((resolve) => { setTimeout(resolve, ms) })
-
-/**
- * 多维数组转一维
- * @param arr
- * @returns
- */
-export const flatten: any = (arr: Array<any>) => [].concat(
-  ...arr.map((x) => (Array.isArray(x) ? flatten(x) : x))
-)
-
-/**
- * 多字段匹配
- * @param  data 数组|数组对象
- * @param  key
- * @returns
- */
-export const search = (data: Array<any>, key: string) => {
-  const list = data.filter((el) => {
-    let bt = ''
-    bt += el.name
-    bt += el.eg
-    return bt.match(key)
-  })
-  return list
-}
-
-/**
- * 格式化日期
- * @param date Date
- * @param fmt 日期格式
- * @returns
- */
-export const fomartTime = (date: Date, fmt = 'yyyy-MM-dd hh:mm:ss') => {
-  if (!date) return ''
-  const o = {
-    'M+': date.getMonth() + 1, // 月份
-    'd+': date.getDate(), // 日
-    'h+': date.getHours(), // 小时
-    'm+': date.getMinutes(), // 分
-    's+': date.getSeconds(), // 秒
-    'q+': Math.floor((date.getMonth() + 3) / 3), // 季度
-    S: date.getMilliseconds() // 毫秒
-  } as any
-  if (/(y+)/.test(fmt)) {
-    fmt = fmt.replace(RegExp.$1, (`${date.getFullYear()}`).substr(4 - RegExp.$1.length))
-  }
-  for (const k in o) {
-    if (new RegExp(`(${k})`).test(fmt)) {
-      fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? (o[k]) : ((`00${o[k]}`).substr((`${o[k]}`).length)))
-    }
-  }
-  return fmt
-}
-
-/**
-* 格式化掌子面里程
-* @param num 里程
-* @returns
-*/
-export const mileage2string = (num: number) => {
-  const a = Math.floor(num / 1000).toString()
-  const ab = parseFloat((num % 1000).toFixed(1))
-  const b = Math.floor(num % 1000).toString()
-  const c = b.length === 1 ? `00${ab}` : b.length === 2 ? `0${ab}` : ab
-  return `${a}+${c}`
-}
-
-/**
- * 对象数组去重
- * @param arr 传入数组
- * @param key 需要对比的键
- * @returns 去重后的数组
- */
-export const uniqueArr = <T = any>(arr: T[], key: keyof T) => {
-  const newArr = [] as T[]
-  const valueList = [] as any[]
-
-  for (const item of arr) {
-    if (valueList.indexOf(item[key]) === -1) {
-      valueList.push(item[key])
-      newArr.push(item)
-    }
-  }
-  return newArr
-}
-
-/**
- * 数组按指定key值分组
- * @param {*} array
- * @param {*} id
- * @returns
- */
-export const groupBy = (array: Array<any>, id: string) => {
-  const groups = {} as any
-  array.forEach((o) => {
-    let group = JSON.stringify(o[id])
-    if (typeof o[id] === 'string') {
-      group = o[id]
-    }
-    groups[group] = groups[group] || []
-    groups[group].push(o)
-  })
-  // return Object.values(groups);
-  return groups
-}
-
-/**
-  * 解析后的浮点数转时间戳
-  * @param num 时间浮点
-  */
-export const number2times = (num: number) => {
-  const utc = (num - 25569) * 86400
-  const date = new Date(utc * 1000).getTime()
-  const newtime = (date - 8 * 3600 * 1000)
-  return newtime
-}

+ 0 - 121
src/utils/request.ts

@@ -1,121 +0,0 @@
-/** api接口返回值类型 */
-declare type NetResult = {
-  success: boolean,
-  data: any,
-  msg: string,
-  code: number
-}
-
-/**
- * post方法请求接口
- * @param url 接口地址
- * @param params 接口参数
- */
-export function POST(url: string, params: any = {}, timeout = 6000): Promise<NetResult> {
-  return new Promise((resolve) => {
-    const controller = new AbortController()
-    const { signal } = controller
-    setTimeout(() => {
-      controller.abort()
-    }, timeout)
-    fetch(`/api${url}`, {
-      signal,
-      method: 'POST',
-      headers: {
-        'Content-Type': 'application/json',
-        Authorization: 'Bearer '
-      },
-      body: JSON.stringify(params)
-    }).then((res: any) => resolve(res)).catch((err: any) => resolve(err))
-  })
-}
-
-/**
- * upload上传文件接口
- * @param url 接口地址
- * @param params 接口参数
- */
-export function UPLOAD(url: string, params: any, timeout = 6000): Promise<NetResult> {
-  return new Promise((resolve) => {
-    const controller = new AbortController()
-    const { signal } = controller
-    setTimeout(() => {
-      controller.abort()
-    }, timeout)
-
-    fetch(`/api${url}`, {
-      signal,
-      method: 'POST',
-      headers: { Authorization: 'Bearer ' },
-      body: params
-    }).then((res: any) => resolve(res)).catch((err: any) => resolve(err))
-  })
-}
-
-/**
- * get请求api接口
- * @param url 接口地址
- */
-export function GET(url: string, timeout = 6000): Promise<NetResult> {
-  return new Promise((resolve) => {
-    const controller = new AbortController()
-    const { signal } = controller
-    setTimeout(() => {
-      controller.abort()
-    }, timeout)
-    fetch(`/api${url}`, {
-      signal,
-      method: 'get',
-      headers: {
-        'Content-Type': 'application/json',
-        Authorization: 'Bearer '
-      }
-    }).then((res: any) => resolve(res)).catch((err: any) => resolve(err))
-  })
-}
-
-/**
- * del请求api接口
- * @param url 接口地址
- */
-export function DEL(url: string, timeout = 6000): Promise<NetResult> {
-  return new Promise((resolve) => {
-    const controller = new AbortController()
-    const { signal } = controller
-    setTimeout(() => {
-      controller.abort()
-    }, timeout)
-    fetch(`/api${url}`, {
-      signal,
-      method: 'DELETE',
-      headers: {
-        'Content-Type': 'application/json',
-        Authorization: 'Bearer '
-      }
-    }).then((res: any) => resolve(res)).catch((err: any) => resolve(err))
-  })
-}
-
-/**
- * post方法请求接口
- * @param url 接口地址
- * @param params 接口参数
- */
-export function PUT(url: string, params: any = {}, timeout = 6000): Promise<NetResult> {
-  return new Promise((resolve) => {
-    const controller = new AbortController()
-    const { signal } = controller
-    setTimeout(() => {
-      controller.abort()
-    }, timeout)
-    fetch(`/api${url}`, {
-      signal,
-      method: 'put',
-      headers: {
-        'Content-Type': 'application/json',
-        Authorization: 'Bearer '
-      },
-      body: JSON.stringify(params)
-    }).then((res: any) => resolve(res)).catch((err: any) => resolve(err))
-  })
-}

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

@@ -10,8 +10,6 @@ declare module '*.vue' {
   export default component
 }
 
-declare module 'js-md5'
 declare module 'xlsx'
 declare module 'html2canvas'
 declare module 'jspdf'
-declare module '@/utils/*'