首页
留言
友链
关于
Search
1
思源笔记docker私有化部署及使用体验分享
2,421 阅读
2
windows11 远程提示:为安全考虑,已锁定该用户帐户,原因是登录尝试或密码更改尝试过多。
1,110 阅读
3
Pointer-Focus:一款功能强大的教学、录屏辅助软件
615 阅读
4
解决 nginxProxyManager 申请证书时的SSL失败问题
610 阅读
5
使用cspell对项目做拼写规范检查
581 阅读
Web前端
CSS
JavaScript
交互
Vue
小程序
后端
运维
项目
生活
其他
转载
软件
职场
登录
Search
标签搜索
docker
DevOps
magic-boot
Linux
酷壳
RabbitMQ
gitlab
Node
git
工具
MybatisPlus
clickhouse
Syncthing
规范
前端
产品
nginx
markdown
axios
H5
朱治龙
累计撰写
139
篇文章
累计收到
7
条评论
首页
栏目
Web前端
CSS
JavaScript
交互
Vue
小程序
后端
运维
项目
生活
其他
转载
软件
职场
页面
留言
友链
关于
搜索到
9
篇与
Vue
的结果
2023-06-24
将vue-cli搭建的Vue前端工程更新构建工具为pnpm+vite经验分享
缘起我们团队负责的超算云服务控制台前端工程(console-ui)从提交第一行代码(2021-03-18)至今已 2 年多时间,两年多的时间内随着需求的增加,前端工程也越来越庞大,陈旧的基础设施暴露出了如下问题:1、Node.js 版本仍然使用 2021 年发布的 14.x,而最新的 Node 大版本已更新到20.x,仍然使用旧版本导致一些构建工具版本不能随意更新,也同时留下一定的安全隐患。2、项目目前仍使用两年前发行的 Webpack4.46.0 版本作为项目构建工具,随着项目逐渐庞大,Webpack 在开发时编译项目时间通常达到 2 分钟以上,严重影响开发效率。3、前端生态日新月异,守着老旧的生态,不便于团队成长。借本次升级 console-ui 核心 vue版本的契机,考虑同时升级到新的构建工具。Vite 从 2021 年发布以来,在构建速度及开发体验上一直口碑不错,经过两年多的迭代,也趋于稳定,完全可以应用于产线了。且根据官方的介绍 Vite 4.3 开始的版本比之前的版本在开发及构建速度上又有了成倍的提升:发行日志见链接:https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md开始折腾1、升级 Node.js目前 console-ui 构建还是用的Node.js 14.x, 而现在 Node.js 的 LTS 版本是18.x,按照 Node.js 的版本发行规律,预计目前最新的 20.x 版本在今年的 10 月份将成为下一个 LTS 版本,且 Node.js 的向下兼容性较强,所以目前咱们可以拥抱 Node.js 20.x 相关生态。所以本次升级就直接选用 Node.js 20.x 了。开发机是使用nvm管理 Node.js 多版本的。使用如下命令安装并使用Node.js 20:nvm install 20.3.0 nvm use 20.3.02、升级构建工具在升级过程如下:创建初始工程使用官方开始页面提供的命令初始化一个初始工程:pnpm create vite console-ui --template vue3、console-ui 已有功能最简验证3.1、支持多主题console-ui的设定是可以打包不同主题的页面,构建时根据 env 变量打包不同主题的版本,然后采用不同主题文件。经验证vite也支持,vite.config.js 核心配置代码如下:css: { preprocessorOptions: { scss: { additionalData: `@import "@/styles/themes/${env.VITE_APP_DIST_MODE}.scss";` } } }由于不同的主题有些不同的资源文件(如favicon.ico、主题相关的图片等)需要引用,在这里我们在public 目录下新建2个目录用于管理不同主题资源文件的管理,然后配置不同的publicDir:publicDir: `./public/${distMode}`,3.2、验证多页面支持console-ui提供针对普通用户的 UI 界面,同时也提供管理员使用的维护界面,这两套界面在原工程是使用多页面机制实现的。经验证,vite 也能很好的支持,核心配置代码如下:plugins: [ createHtmlPlugin({ minify: true, pages: [ { entry: '/src/main.js', filename: 'index.html', template: 'index.html', injectOptions: { data: { title: '超算云服务控制台' } } }, { entry: 'src/pages/admin/index.js', filename: 'admin.html', template: 'admin.html', injectOptions: { data: { title: '超算运服务控制台-系统维护' } } } ] }) ]4、开始整合整合时会涉及一些资源文件引用的改造,一般改动会比较多,为不影响主分支开发,建议单独开个分支处理。整合过程比较繁琐,就不一一罗列了,主要涉及如下事项:替换 ENV 变量:vue-cli 工程的环境变量都是以Vue_APP 开头的变量,可以全局替换为VITE_APP开头,而且 Webpack 工程一般使用 process.env.XXX 访问ENV变量,而 Vite 是使用 import.meta.env.XXX 访问 ENV 变量,为了少改动,可以使用如下方式继续使用process.env.XXX 方式访问:define: { 'process.env': { ...env } }调整资源引用问题:Webpack 项目多数使用require('xxx')的方式在 JS 里引入资源,确实特别的方便,但是这一套在 Vite支持不是很好,即便有插件支持(如vite-plugin-commonjs)但依然没有 Webpack 的 require 机制强大。一些不需要参与打包的资源可以放在 public 目录,然后使用URL路径访问,参与打包的文件,可改为 import XXX from 'xxxx.png'的方式引入整体改造过程提交记录截图如下:优化整合后发现打包的文件比原 Webpack 打包的 dist 目录更大,前端工程层面的优化,主要以从以下几点着手处理:压缩:可采用vite-plugin-compression插件对打包后的文件进行了压缩处理静态资源上CDN:优化过程主要使用rollup-plugin-visualizer 对打包后的文件进行分析,对一些占用打包空间较大的文件使用CDN引入的方式进行优化按需引入:如lodash、moment.js等,尽量按需引入,减少非必须的语言包等,能很大程度的减小打包体积。附上本次改造核心的完整 vite.config.js 文件内容:import path from 'path' import vitePluginCommonjs from 'vite-plugin-commonjs' import { createHtmlPlugin } from 'vite-plugin-html' import { visualizer } from 'rollup-plugin-visualizer' import viteCompression from 'vite-plugin-compression' import externalGlobals from 'rollup-plugin-external-globals' // import eslintPlugin from 'vite-plugin-eslint' import { defineConfig, loadEnv } from 'vite' import legacy from '@vitejs/plugin-legacy' import vue2 from '@vitejs/plugin-vue2' export default ({ mode }) => { const env = loadEnv(mode, process.cwd()) const distMode = env.VITE_APP_DIST_MODE || 'paratera' const cdn = { scripts: mode.includes('development') ? [] : [ 'https://statics.paratera.com/console/libs/vue.2.7.14.min.js', // 'https://cdn.jsdelivr.net/npm/vue-router@3.6.5/dist/vue-router.min.js', // 'https://cdn.jsdelivr.net/npm/vuex@3.6.2/dist/vuex.min.js', 'https://statics.paratera.com/console/libs/element-ui.2.15.13.min.js', 'https://statics.paratera.com/console/libs/exceljs.4.3.0.min.js' ] } // https://vitejs.dev/config/ const viteConfig = { base: '/', publicDir: `./public/${distMode}`, define: { 'process.env': { ...env } }, plugins: [ vue2(), // eslintPlugin(), createHtmlPlugin({ minify: true, pages: [ { entry: '/src/main.js', filename: 'index.html', template: 'index.html', injectOptions: { data: { title: '超算云服务控制台', cdn }, tags: [ { injectTo: 'body-prepend', tag: 'div', attrs: { id: 'tag1' } } ] } }, { entry: 'src/pages/admin/index.js', filename: 'admin.html', template: 'admin.html', injectOptions: { data: { title: '超算运服务控制台-系统维护', cdn // injectScript: `<script src="./inject.js"></script>`, } } } ] }), vitePluginCommonjs(), legacy({ targets: ['ie >= 11'], additionalLegacyPolyfills: ['regenerator-runtime/runtime'] }), viteCompression({ verbose: true, // 是否在控制台输出压缩结果 disable: false, // 是否禁用,相当于开关在这里 threshold: 10240, // 体积大于 threshold 才会被压缩,单位 b,1b=8B, 1B=1024KB 那我们这里相当于 9kb多吧,就会压缩 algorithm: 'gzip', // 压缩算法,可选 [ 'gzip' , 'brotliCompress' ,'deflate' , 'deflateRaw'] ext: '.gz' // 文件后缀 }) ], resolve: { extensions: ['.js', '.vue', '.json'], alias: [ { find: /@\/.+/, replacement: (val) => { return val.replace(/^@/, path.resolve(__dirname, './src/')) } }, { // this is required for the SCSS modules find: /^~(.*)$/, replacement: '$1' } ] }, css: { preprocessorOptions: { scss: { additionalData: `@import "@/styles/themes/${distMode}.scss";` } } }, server: { port: 8686 }, build: { // target: 'es2015', // cssTarget: 'chrome80', // brotliSize: false, // chunkSizeWarningLimit: 2000, // commonjsOptions: { // // 改为 ture 后就会转化 require 语法 // transformMixedEsModules: true // }, rollupOptions: { input: { index: path.resolve(process.cwd(), 'index.html'), admin: path.resolve(process.cwd(), 'admin.html') }, external: [ // 'vue', 'element-ui', 'exceljs' ], plugins: [ externalGlobals({ vue: 'Vue', // 'vue-router': 'VueRouter', // vuex: 'Vuex', 'element-ui': 'ELEMENT', 'exceljs': 'ExcelJS' }) ], output: { chunkFileNames: 'assets/js/chunks/[name].[hash].js', assetFileNames: (chunkInfo) => { // 用后缀名称进行区别处理 // 处理其他资源文件名 e.g. css png 等 let subDir = 'images' const extName = path.extname(chunkInfo.name) if (['.css'].includes(extName)) { subDir = 'css' } if (['.woff', '.ttf'].includes(extName)) { subDir = 'fonts' } return `assets/${subDir}/[name].[hash].[ext]` }, // 入口文件名 entryFileNames: 'assets/js/[name].[hash].js' } } } } const lifecycle = process.env.npm_lifecycle_event if (lifecycle.includes(':report')) { viteConfig.plugins.push(visualizer({ open: true, brotliSize: true, gzipSize: true, filename: './dist/report.html' })) } return defineConfig(viteConfig) } 升级前后 package.json 比对:成效对比项优化前(Webpack)优化后(Vite)安装依赖66.60s17.2开发启动110228ms2536ms打包部署123.77s1m 57s对比截图安装依赖:Webpack ↓Vite ↓开发启动:Webpack ↓Vite ↓打包截图:Webpack ↓Vite ↓
2023年06月24日
46 阅读
0 评论
0 点赞
2023-06-10
随动帮助文档实现
背景介绍我们项目提供了在线的项目文档,便于用户在使用过程中有什么问题可以通过直接查文档解决,也在一定程度上减少了公司运营及客服成本。在实际使用过程中,用户反馈造作文档的使用不是很方便,基本上要打开新Tab页签,不能方便的一遍对照文档,一边操作。解决方案为解决以上问题,在参考阿里云及腾讯云相关产品的文档模式后,引入了随动帮助文档机制。该机制主要实现模式如下:1、在相关功能指定位置添加文档入口2、单击文档名称后,在页面左下角显示文档弹出层(一下简称「文档查看器」),主要是系统的大部分功能使用右侧展开的抽屉组件 (Drawer)进行显示。3、打开的文档查看器,可垂直方向最大,可上下左右四边及四个角落通过拖拽调整大小4、可拖拽待页面任意位置5、文档查看器不影响页面中其他组件的交互该功能是系统每个功能模块都有可能会用到的,所以将起提取为一个公共组件。组件只需传入功能标识,然后调用内容管理系统的接口获取数据,如果有数据则显示文档入口,由于一个功能极有可能会对应有多篇帮助文档,用户从文档入口单击将展开文档列表,用户单击需要查看的文档,则在左下角显示文档。实现过程示例中均使用模拟的数据,实际数据在生产环境调用内容管理系统的接口获取。实现文档查看器组件HelpModal.vue文档查看器的拖拽及大小调整均通过指令控制<template> <el-dialog ref="dlg" v-dragAndResize :modal="false" top="auto" :visible.sync="isShow" custom-class="help-modal-wrap" width="400px" :close-on-click-modal="false" append-to-body > <div slot="title" class="dialog-title"> <h3>功能指南</h3> <div class="panel-actions"> <em class="panel-action console-svg svg-vertical-expand margin-right-xs" :class="{ 'svg-vertical-expand': !verticalMaxed, 'svg-vertical-collapse': verticalMaxed }" :title="verticalMaxed ? '还原' :'垂直最大化'" @click="setDlgMaxHeight" /> </div> </div> <div class="dlg-body-inner"> <div class="content-wrap"> <h2 class="article-title"> 如何使用一键转区 </h2> <div class="article-subtitle"> <span class="article-time">更新时间:2022-08-09 10:10</span> </div> <div class="article-content"> <p style="line-height:normal"> <span style="font-family:'arial' , 'helvetica' , sans-serif;font-size:16px"> 可使用【一键转区】功能,将超算账号的数据转移至指定的超算上进行使用,如超算中心A3分区的sc0001账号(即为源目标)转移至超算分区A6的sc0001(即为目的目标),具体操作如下:</span> </p> <p style="line-height:normal"> <span style="font-family:'arial' , 'helvetica' , sans-serif;font-size:16px"> 1. 单击【一键转区】中的【申请一键转区】</span> </p> <p> <span style="font-size:14px"><img src="https://kbs.paratera.com/uploads/1/image/public/202208/20220809100845_a3269g5dky.png" title="" alt="image.png" ></span> </p> <p> <span style="font-size:14px"><img src="https://kbs.paratera.com/uploads/1/image/public/202208/20220809100855_atmxepobl8.png" title="" alt="image.png" ></span> </p> <p><span style="font-size:14px"> </span></p> <p style="line-height:normal"> <span style="font-family:'arial' , 'helvetica' , sans-serif;font-size:14px"> <span style="font-family:'arial' , 'helvetica' , sans-serif;font-size:16px"> 2. 在【源目标】处选择可用的超算分区及超算账号,例如A3超算分区的sc0001;</span></span> </p> <p style="line-height:normal"> <span style="font-family:'arial' , 'helvetica' , sans-serif;font-size:16px"> 在【目的目标】处选择匹配的超算分区,不用选择转区后的超算账号,例如A6超算分区;</span> </p> <p style="line-height:normal"> <span style="font-family:'arial' , 'helvetica' , sans-serif;font-size:16px"> 在【源目标账号环境信息】处填软件使用时的环境要求。</span> </p> <p style="line-height:normal"> <span style="font-family:'arial' , 'helvetica' , sans-serif;font-size:16px"> 3. 单击【确定】,会有提示框告知源目标超算账号保存3个月,3个月后超算账号回收的提醒。</span> </p> <p style="line-height:normal"> <span style="font-family:'arial' , 'helvetica' , sans-serif;font-size:16px"> 4. 在【一键转区】的页面查看工单进展。</span> </p> </div> </div> <div class="outer-link-wrap"> <el-button type="primary" class="btn-open-in-window" @click="openDocLink"> 新窗口查看文档 </el-button> </div> </div> </el-dialog> </template> <script> import { setStyle, on, off, addClass } from 'element-ui/lib/utils/dom' export default { name: 'HelpModal', directives: { dragAndResize: { bind(el, binding, vnode, oldVnode) { addClass(el, 'dlg-wrap-help') // 弹框可拉伸最小宽高 const minWidth = 300 const minHeight = 300 // 获取弹框头部(这部分可双击全屏) const dialogHeaderEl = el.querySelector('.el-dialog__header') // 弹窗 const dragDom = el.querySelector('.el-dialog') dragDom.className = dragDom.className + ' dialog-can-resize' // // 给弹窗加上overflow auto;不然缩小时框内的标签可能超出dialog; // dragDom.style.overflow = 'auto' // 清除选择头部文字效果 dialogHeaderEl.onselectstart = new Function('return false') // 头部加上可拖动cursor dialogHeaderEl.style.cursor = 'move' // 获取原有属性 ie dom元素.currentStyle 火狐谷歌 window.getComputedStyle(dom元素, null); const sty = dragDom.currentStyle || window.getComputedStyle(dragDom, null) // 初始设置在页面左下角 const initialHeight = window.innerHeight * 2 / 3 setStyle(dragDom, 'height', initialHeight + 'px') setStyle(dragDom, 'top', window.innerHeight * 1 / 3 - 10 + 'px') const moveDown = (e) => { e.preventDefault() // 鼠标按下,计算当前元素距离可视区的距离 const disX = e.clientX - dialogHeaderEl.offsetLeft const disY = e.clientY - dialogHeaderEl.offsetTop // 获取到的值带px 正则匹配替换 let styL, styT // 注意在ie中 第一次获取到的值为组件自带50% 移动之后赋值为px if (sty.left.includes('%')) { styL = +document.body.clientWidth * (+sty.left.replace(/%/g, '') / 100) styT = +document.body.clientHeight * (+sty.top.replace(/%/g, '') / 100) } else { styL = +sty.left.replace(/px/g, '') styT = +sty.top.replace(/px/g, '') } document.onmousemove = function(e) { e.preventDefault() // 通过事件委托,计算移动的距离 const l = e.clientX - disX const t = e.clientY - disY // 移动当前元素 dragDom.style.left = `${l + styL}px` dragDom.style.top = `${t + styT}px` // 将此时的位置传出去 // binding.value({x:e.pageX,y:e.pageY}) } document.onmouseup = function(e) { document.onmousemove = null document.onmouseup = null } } on(dialogHeaderEl, 'mousedown', moveDown) // 添加位置调整锚点 const buildResizeElements = (dragDom) => { const directions = ['top', 'right', 'bottom', 'left', 'top-right', 'top-left', 'bottom-right', 'bottom-left'] for (const direction of directions) { const resizeEl = document.createElement('div') resizeEl.className = `resize-handle resize-${direction}` resizeEl.setAttribute('resize', direction) dragDom.appendChild(resizeEl) } } buildResizeElements(dragDom) let moveOpts = null on(document, 'mousedown', (event) => { if (event.which !== 1 && event.type === 'mousedown') { return } const { target } = event if (!target.className.includes('resize-handle')) { return } const resizeDirection = target.getAttribute('resize') moveOpts = { resizeDirection: resizeDirection, clientX: event.clientX, clientY: event.clientY, startWidth: dragDom.offsetWidth, startHeight: dragDom.offsetHeight, offsetLeft: dragDom.offsetLeft, offsetTop: dragDom.offsetTop } const screenWidth = window.innerWidth const screenHeight = window.innerHeight const mouseMoveHandler = (event) => { event.preventDefault() const style = dragDom.style const resizeDirection = moveOpts.resizeDirection const { startWidth, startHeight } = moveOpts let left = moveOpts.offsetLeft let top = moveOpts.offsetTop let width = moveOpts.startWidth let height = moveOpts.startHeight let clientX = event.clientX let clientY = event.clientY let x = (clientX = screenWidth <= clientX ? screenWidth : clientX) <= 0 ? 0 : clientX let y = (clientY = screenHeight <= clientY ? screenHeight : clientY) <= 0 ? 0 : clientY x = event.clientX - moveOpts.clientX y = event.clientY - moveOpts.clientY switch (resizeDirection) { case 'top': top = y + top height = -y + height break case 'right': width = x + width break case 'bottom': height = y + height break case 'left': left = x + left width = -x + width break case 'top-left': left = x + left top = y + top width = -x + width height = -y + height break case 'top-right': top = y + top width = x + width height = -y + height break case 'bottom-right': width = x + startWidth height = y + startHeight break case 'bottom-left': left = x + left width = -x + startWidth height = y + startHeight } left = left <= 0 ? 0 : left top = top <= 0 ? 0 : top style.left = left + 'px' style.top = top + 'px' style.width = Math.max(minWidth, width) + 'px' style.height = Math.max(minHeight, height) + 'px' } const mouseUpHandler = (event) => { console.error('mouseUp', event) off(document, 'mousemove', mouseMoveHandler) off(document, 'mouseup', mouseUpHandler) } on(document, 'mousemove', mouseMoveHandler) on(document, 'mouseup', mouseUpHandler) }) } } }, props: { show: { type: Boolean, required: true, default: false }, docId: { type: Number, required: true } }, data() { return { originStyle: null, verticalMaxed: false } }, computed: { isShow: { get() { return this.show }, set(val) { this.$emit('update:show', val) } } }, created() { this.init() }, methods: { async init() { }, setDlgMaxHeight() { const $dlg = this.$refs['dlg'] console.log('dlg', $dlg) const dlgEl = $dlg.$el.childNodes[0] const dlgStyle = dlgEl.style if (this.verticalMaxed) { setStyle(dlgEl, 'top', this.originStyle.top) setStyle(dlgEl, 'height', this.originStyle.height) } else { this.originStyle = { top: dlgStyle.top, height: dlgStyle.height } setStyle(dlgEl, 'top', 0) setStyle(dlgEl, 'height', window.innerHeight + 'px') } this.verticalMaxed = !this.verticalMaxed }, openDocLink() { window.open('https://kbs.paratera.com/info/1272') } } } </script> <style lang="scss"> @import "./modal-resize.scss"; .help-modal-wrap { border-radius: 0; pointer-events: all; margin: 0; left: 10px; top: calc(100vh - 100% - 10px); // transition: all ease 0.2s; .el-dialog__header { padding: 10px; background-image: url(./header-bg-2.jpg); background-repeat: no-repeat; background-position: 100% 0%; background-size:cover; background-color: $theme-light-1; color:#fff; position: relative; em.console-svg { font-size: 16px; cursor: pointer } .el-dialog__headerbtn { top: 15px; right: 15px; } .el-dialog__close { color:#fff; } } .el-dialog__body { flex: auto; overflow: auto; height: 100%; padding: 0; } .dlg-body-inner { box-sizing: border-box; height: 100%; // position: relative; // overflow: auto; // min-height: 352px; // max-height: calc(100vh - 60px); } } </style> <style lang="scss" scoped> @import "../../dashboard/modals/ArticleContent.scss"; :deep(.dlg-wrap-help) { pointer-events: none; } .dialog-title { h3 { font-size: 16px; font-weight:normal; } } .panel-actions { position: absolute; right: 30px; top: 10px; } .panel-action { width: 24px; height:24px; line-height: 24px; text-align: center; display:inline-block; margin: 0 3px; cursor: pointer; &:hover { background-color:rgba(0,0,0,0.15) } } .content-wrap { height: calc(100% - 95px); overflow: auto; padding: 10px; } article { padding-top: 20px; } .article-title { text-align: left; color: #333333; padding-top: 10px; font-size: 22px; font-weight: normal; } .article-subtitle { position: relative; width: 100%; padding-top: 10px; text-align: left; color: #999; font-size: 14px; } .article-content { padding: 10px; } .outer-link-wrap { padding: 10px; position: static; background-color: #fff; width: 100%; bottom: 0; } .btn-open-in-window { display: block; width: 100%; box-sizing: border-box; margin: 0 auto; } </style> <style> .dlg-wrap-help { pointer-events: none; z-index: 9000!important; } </style> modal-resize.scss.dialog-can-resize .resize-handle { position: absolute; z-index: 100; display: block } .dialog-can-resize .resize-top { cursor: n-resize; top: -3px; left: 0px; height: 7px; width: 100% } .dialog-can-resize .resize-bottom { cursor: s-resize; bottom: -3px; left: 0px; height: 7px; width: 100% } .dialog-can-resize .resize-right { cursor: e-resize; right: -3px; top: 0px; width: 7px; height: 100% } .dialog-can-resize .resize-left { cursor: w-resize; left: -3px; top: 0px; width: 7px; height: 100% } .dialog-can-resize .resize-bottom-right { cursor: se-resize; width: 20px; height: 20px; right: -8px; bottom: -8px; background: url('./resize_corner.png') no-repeat; opacity: .4; filter: alpha(opacity=40) } .dialog-can-resize .resize-bottom-left { cursor: sw-resize; width: 16px; height: 16px; left: -8px; bottom: -8px } .dialog-can-resize .resize-top-left { cursor: nw-resize; width: 16px; height: 16px; left: -8px; top: -8px } .dialog-can-resize .resize-top-right { cursor: ne-resize; width: 16px; height: 16px; right: -8px; top: -8px } .dialog-can-resize .aui-min,.dialog-can-resize .aui-max { display: block } .dialog-can-resize .resize-top-right:before,.dialog-can-resize .resize-bottom-right:before,.dialog-can-resize .resize-bottom-left:before,.dialog-can-resize .resize-top-left:before { position: absolute; content: ""; -ms-transition: all .2s; -webkit-transition: all .2s; -moz-transition: all .2s; -o-transition: all .2s; transition: all .2s; width: 15px; height: 15px; opacity: 0; pointer-events: none; border: 1px solid #1890ff; border-radius: 0 } .dialog-can-resize .resize-top-right:hover:before,.dialog-can-resize .resize-bottom-right:hover:before,.dialog-can-resize .resize-bottom-left:hover:before,.dialog-can-resize .resize-top-left:hover:before,.dialog-can-resize .resize-top-right:active:before,.dialog-can-resize .resize-bottom-right:active:before,.dialog-can-resize .resize-bottom-left:active:before,.dialog-can-resize .resize-top-left:active:before { opacity: 1 } .dialog-can-resize .resize-top-right:before { border-left: none; border-bottom: none; border-radius: 0 4px 0 0; left: -8px; top: 8px } .dialog-can-resize .resize-bottom-right:before { border-left: none; border-top: none; border-radius: 0 0 4px 0; left: -4px; top: -4px } .dialog-can-resize .resize-bottom-left:before { border-right: none; border-top: none; border-radius: 0 0 0 4px; left: 8px; top: -8px } .dialog-can-resize .resize-top-left:before { border-right: none; border-bottom: none; border-radius: 4px 0 0 0; left: 8px; top: 8px } 实现文档入口组件<template> <div v-if="docList.length > 0" class="fn-doc-wrap"> <el-popover position="bottom" trigger="hover" > <el-button slot="reference" type="text" class="padding-xs"> <em class="console-svg svg-fn-help" /> </el-button> <ul class="doc-list"> <li v-for="docItem in docList" :key="docItem.id" @click="showHelpModal(docItem.id)"> <p v-text="docItem.title" /><svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg" class="icon-tool-arrow" data-v-68bcb17a=""><path d="M13.7031 11.5259L13.7031 4.77588L6.95314 4.77589M4.30093 14.177L13.3949 5.08301" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg> </li> </ul> </el-popover> </div> </template> <script> export default { name: 'ParaFnDoc', desc: '随动帮助文档入口组件,使用过程中只需要传入关键词即可', props: { keyword: { type: String, required: true } }, data() { return { docList: [] } }, created() { this.init() }, methods: { init() { // TODO 根据关键词获取文档列表 this.docList = [ { id: 1, title: '一键转区功能说明' }, { id: 2, title: '为什么一键转区功能不可用?' }, { id: 3, title: '为什么一键转区提交后没有及时生效?' } ] }, showHelpModal(docId) { window.ParaCommon.showHelpModal(docId) } } } </script> <style lang="scss" scoped> .fn-doc-wrap { display: inline-block; vertical-align: middle; } .doc-list li { line-height: 22px; margin-bottom: 5px; padding: 7px 10px; list-style:none; display: flex; align-items: center; justify-content: center; &:hover { background-color: #f7f8fa; cursor: pointer; .icon-tool-arrow { stroke: #1e80ff; } } p { flex: 1 } .link-arrow { color:blue } } </style> 实现后的效果{dplayer src="/usr/uploads/2023/06/592593977.mp4"/}
2023年06月10日
14 阅读
0 评论
0 点赞
2023-05-24
记一次使用v-charts绘制图表出现重影的处理经历分享
背景介绍目前维护的项目属于多年积累的老项目了,使用的 vue2 + vue-router + vuex + element-ui + webpack 的技术架构。涉及图表相关的库主要使用 echarts@4.9.0 和 饿了么前端团队基于 echarts 4.x 封装的 v-charts 1.19.0。近期接运营要求,需要增加一些图表效果,基本界面效果实现如下:问题但在实现过程中,在区域缩放组件拖动调整数据显示范围时,发现x轴会出现重影的现象,如下图所示:排查过程初期怀疑跟图表配置项的 xAxis 相关配置信息有关,随将 position: 'bottom' 配置项注释掉,发现重影现象没有了,但是上下出现了重复的x 轴label如下图所示:将echarts 图表配置信息拷贝到 echarts 实例页面(https://echarts.apache.org/v4/examples/zh/editor.html?c=bar-large)进行验证,发现在该页面不存在重影现象:配置信息如下:var option = { "color": ["#004CA1", "#32D4E3", "#03A2DC", "#B581E3", "#0F7DFA", "#c4b4e4"], "tooltip": { "trigger": "axis", "axisPointer": { "type": "shadow" } }, "legend": { "show": true, "top": -5, "data": ["CPU", "GPU"] }, "grid": { "right": "5%", "top": "10%", "bottom": "20%", "containLabel": true }, "dataZoom": [ { "type": "inside" }, { "type": "slider", "left": 77, "right": 77 } ], "xAxis": { "data": [ "2022-11-26", "2022-11-27", "2022-11-28", "2022-11-29", "2022-11-30", "2022-12-01", "2022-12-02", "2022-12-03", "2022-12-04", "2022-12-05", "2022-12-06", "2022-12-07", "2022-12-08", "2022-12-09", "2022-12-10", "2022-12-11", "2022-12-12", "2022-12-13", "2022-12-14", "2022-12-15", "2022-12-16", "2022-12-17", "2022-12-18", "2022-12-19", "2022-12-20", "2022-12-21", "2022-12-22", "2022-12-23", "2022-12-24", "2022-12-25", "2022-12-26", "2022-12-27", "2022-12-28", "2022-12-29", "2022-12-30", "2022-12-31", "2023-01-01", "2023-01-02", "2023-01-03", "2023-01-04", "2023-01-05", "2023-01-06", "2023-01-07", "2023-01-08", "2023-01-09", "2023-01-10", "2023-01-11", "2023-01-12", "2023-01-13", "2023-01-14", "2023-01-15", "2023-01-16", "2023-01-17", "2023-01-18", "2023-01-19", "2023-01-20", "2023-01-21", "2023-01-22", "2023-01-23", "2023-01-24", "2023-01-25", "2023-01-26", "2023-01-27", "2023-01-28", "2023-01-29", "2023-01-30", "2023-01-31", "2023-02-01", "2023-02-02", "2023-02-03", "2023-02-04", "2023-02-05", "2023-02-06", "2023-02-07", "2023-02-08", "2023-02-09", "2023-02-10", "2023-02-11", "2023-02-12", "2023-02-13", "2023-02-14", "2023-02-15", "2023-02-16", "2023-02-17", "2023-02-18", "2023-02-19", "2023-02-20", "2023-02-21", "2023-02-22", "2023-02-23", "2023-02-24", "2023-02-25", "2023-02-26", "2023-02-27", "2023-02-28", "2023-03-01", "2023-03-02", "2023-03-03", "2023-03-04", "2023-03-05", "2023-03-06", "2023-03-07", "2023-03-08", "2023-03-09", "2023-03-10", "2023-03-11", "2023-03-12", "2023-03-13", "2023-03-14", "2023-03-15", "2023-03-16", "2023-03-17", "2023-03-18", "2023-03-19", "2023-03-20", "2023-03-21", "2023-03-22", "2023-03-23", "2023-03-24", "2023-03-25", "2023-03-26", "2023-03-27", "2023-03-28", "2023-03-29", "2023-03-30", "2023-03-31", "2023-04-01", "2023-04-02", "2023-04-03", "2023-04-04", "2023-04-05", "2023-04-06", "2023-04-07", "2023-04-08", "2023-04-09", "2023-04-10", "2023-04-11", "2023-04-12", "2023-04-13", "2023-04-14", "2023-04-15", "2023-04-16", "2023-04-17", "2023-04-18", "2023-04-19", "2023-04-20", "2023-04-21", "2023-04-22", "2023-04-23", "2023-04-24", "2023-04-25", "2023-04-26", "2023-04-27", "2023-04-28", "2023-04-29", "2023-04-30", "2023-05-01", "2023-05-02", "2023-05-03", "2023-05-04", "2023-05-05", "2023-05-06", "2023-05-07", "2023-05-08", "2023-05-09", "2023-05-10", "2023-05-11", "2023-05-12", "2023-05-13", "2023-05-14", "2023-05-15", "2023-05-16", "2023-05-17", "2023-05-18", "2023-05-19", "2023-05-20", "2023-05-21", "2023-05-22", "2023-05-23", "2023-05-24" ], "type": "category", "silent": false, "axisLine": { "show": true }, "axisLabel": { "show": true, "interval": "auto", "showMinLabel": true, "showMaxLabel": true }, "scale": true, "splitLine": { "show": false }, "splitArea": { "show": true } }, "yAxis": { "type": "value", "axisLine": { "show": true } }, "series": [ { "type": "bar", "stack": "总量", "name": "CPU", "barMaxWidth": 40, "label": { "show": false, "position": "insideRight" }, "large": true, "data": [ 41284.35, 47697.4, 8413.61, 9252.11, 31770.33, 35508.78, 46398.67, 58060.04, 18176.99, 36094.06, 9143.29, 29802.65, 3537.02, 47931.77, 45087.27, 16314.9, 56488.59, 34108.47, 32244.21, 13635.22, 58377.44, 462.67, 56279.27, 2491.55, 13690.14, 29416.47, 35530.21, 18119.02, 23853.13, 33763.83, 7201.61, 26889.05, 51124.77, 53054.58, 48729.74, 37691.94, 46411.41, 25579.66, 30987.66, 18372.65, 45733.25, 56040.3, 13739.55, 16133.89, 53678.09, 1180.71, 4276.45, 11854.02, 46574.02, 6939.56, 18079.92, 25200.51, 47832.78, 25691.29, 14248.72, 4032.22, 39072.21, 12722.16, 42049.24, 27181.52, 29552.25, 51946, 48863.94, 22122, 39071.33, 50454.55, 59850.52, 23004.45, 51904.63, 28075.69, 38243.83, 32189.07, 1613.23, 40114.69, 30133.03, 28253.31, 38028.55, 25430.73, 23515.73, 9958.14, 38543.8, 36322.66, 57980.19, 57456.79, 27707.03, 269.52, 7980.29, 17138.2, 432.99, 48010.12, 1241.83, 39925.79, 41015.29, 38714.92, 18084.32, 28493.39, 51526.46, 22794.2, 32825.65, 53792.55, 18065.35, 4739.43, 39413.07, 37827.19, 9056.71, 49265.35, 35348.52, 10957.42, 30322.44, 22973.19, 47500.12, 48545.9, 946.19, 19273.15, 22761.02, 40868.44, 775.85, 49775.87, 28345, 52012.99, 30051.59, 45289.68, 36900.8, 31460.81, 51110.19, 12397.24, 34578.01, 47720.53, 15712.89, 6756.28, 25762.61, 54078.42, 50010.54, 16609.55, 13129.16, 32402.43, 26270.31, 5681.48, 31295.89, 5824.84, 12550.59, 50847.39, 32752.45, 18003.45, 53884.61, 32547.36, 47514.46, 5961.97, 24958.12, 51028.81, 26937.62, 31497.45, 16485.2, 13467.6, 35387.98, 14217.41, 18260.78, 45292.45, 15902.32, 15645.63, 48198.75, 26858.24, 28383.47, 5838.99, 35127.11, 26490.36, 21835.39, 3394.33, 55933.54, 55142.88, 39673.89, 4071.1, 44240.5, 39103.1, 45423, 15899.4, 372.21, 4810.24, 15848.72, 10573.71 ] }, { "type": "bar", "stack": "总量", "name": "GPU", "barMaxWidth": 40, "label": { "show": false, "position": "inside" }, "large": true, "data": [ 45656.8, 48919.63, 30686.44, 39105.29, 6583.58, 16579.54, 28757.36, 48880.32, 34710.97, 52267.01, 35244.55, 29465.79, 18699.38, 26708.27, 48610.16, 41065.65, 11168.56, 52782.18, 50920.27, 8510.37, 1054.53, 15473.91, 52598.26, 20234.36, 54710.49, 37693.7, 25479.18, 15601.02, 715.52, 47497.22, 30241.03, 15098.62, 36223.61, 39782.68, 31656.7, 52831.1, 4355.73, 13741.26, 2690.1, 8953.15, 40778.42, 40186.61, 29139.39, 34951.79, 29297.1, 13981.23, 50465.4, 52068.09, 24882.09, 11735.73, 24719.71, 26538.39, 54061.69, 17362.82, 27552.54, 39175.74, 35403, 45847.45, 39241.42, 21423.24, 28252.3, 15936.17, 51460.45, 38450.2, 36549.7, 8833.54, 49552.81, 11116.06, 7629.68, 23024.14, 46621.6, 48414.22, 34057.44, 16508.14, 8695.67, 9935.44, 23205.87, 459.17, 6058.89, 11538.15, 1691.72, 5674.09, 43659.65, 42399.6, 23885.54, 16819.74, 18714.28, 51726.61, 12924.85, 9741.92, 47726.78, 52780.24, 19201.93, 17926.32, 42666.5, 8034.12, 1942.08, 47110.64, 46218.2, 44514.71, 31623.2, 42708.9, 26215.12, 28139.43, 32497.47, 45742.64, 38990.36, 4446.69, 44520.51, 20765.67, 18069.18, 6911.16, 5579.52, 29354.69, 17215.26, 26342.73, 11537.4, 16678.75, 37653.87, 3944.92, 15016.5, 31703.34, 46996.72, 4255.04, 36405.66, 15890.87, 8085.44, 8727.37, 20420, 11387.02, 50816.76, 38010.23, 44139.63, 1437.55, 19776.06, 5491.32, 1402.23, 18269.63, 31190.68, 9546.1, 52578.27, 26100.02, 8729.68, 37343.21, 2299.31, 28782.83, 31684.85, 16185.26, 15100.92, 29155.27, 38192.58, 16588.44, 18377.32, 54143.88, 51351.67, 43105.14, 46469.82, 22805.7, 3730.43, 29585.85, 21114.32, 26529.31, 15564.97, 615.24, 32499.96, 43994.11, 36856.41, 19624.48, 7703.24, 17755.76, 47110.48, 54746.48, 23024.8, 3153.74, 39398.46, 31583.36, 9440.84, 10548.38, 8578.17, 17212.49 ] } ] } 那问题肯定基本上就能定位是 v-charts在封装的时候对相关的配置信息做了些变更。于是,使用 v-charts 组件的 afterSetOption 获取最终的配置信息:控制台打印的最终 echarts 配置信息如下:简单清理一些data数据后,使用 Beyond 进行数据对比,还真发现使用 v-charts 转换后的数据存在很大问题:然后进一步跟踪相关源代码,发现在图表组件初始化的时候,不知为何将 xAxis 初始化为2个对象的数组:然后在 optionsHandler 方法中将 extend 中的数据附加到option上就改变了原有数据然后 setExtend 方法会把xAxis对象数据复制到目标数组内的每一个对象值里:解决方案问题分析过程中,认真阅读了下相关的核心源码,发现 v-charts 组件在执行echarts.setOption()前提供了 afterConfig的扩展点,可以基于该扩展点对前期生成的 option 进行修改:为解决该问题,修改后的核心代码如下:修复后的效果总结项目开发过程中出现问题往往不可怕,最重要的是需要静下心来定位问题,当知道问题的产生缘由后,往往解决问题的方法也就水到渠成了。
2023年05月24日
16 阅读
0 评论
0 点赞
2022-06-06
vue-cli工程打包生成gzip相关压缩文件
一、添加依赖yarn add vue-cli-plugin-compression -D二、更新vue.config.js文件添加zlib引用const zlib = require('zlib')module.exports 中添加如下配置信息:pluginOptions: { compression:{ brotli: { filename: '[file].br[query]', algorithm: 'brotliCompress', include: /\.(js|css|html|svg|json)(\?.*)?$/i, compressionOptions: { params: { [zlib.constants.BROTLI_PARAM_QUALITY]: 11, }, }, minRatio: 0.8, }, gzip: { filename: '[file].gz[query]', algorithm: 'gzip', include: /\.(js|css|html|svg|json)(\?.*)?$/i, minRatio: 0.8, } } }效果打包后生成相应的.br和.gz结尾的文件
2022年06月06日
14 阅读
0 评论
0 点赞
2022-04-11
使用「Relation Graph」优化图表展现效果
背景说明近期检查前端工程的打包情况,发现@antv/g6占用了很大的空间:经过前端团队内部了解,这个主要是用于项目内超算中心的关系图展示,关系图这一块也随着业务的需求使用了不同的组件来实现:echarts > Mindelixir > @antv/G6实际使用@antv/G6实现完的效果,我的直观感觉也不是很理想,数据量大的情况下,显示太小,而且看了下相关的代码,感觉实现过程也不够优雅。所以秉承公司精益求精的企业文化,为了减少打包体积及优化使用体验等目的,准备使用「Relation Graph」对这一块进行大刀阔斧的改造。经过三四天的打磨,整体效果得到了运营同事的认可,先看看实现后的效果:本次调整后,打包体积上有较大的缩减:优化前:(原始大小:13.69M;压缩后:4.71M;开启gzip:1.42M)优化后:(原始大小:8.97M;压缩后:3.14M;开启gzip:957.42K)代码实现下面将项目中关系图相关的一些核心代码进行讲解视图代码<relation-graph ref="relationGraph" :options="graphOptions"> <div slot="node" slot-scope="{node}"> <el-popover v-if="node.data.status === 'user'" trigger="hover" placement="right" popper-class="account-graph-popver" > <div slot="reference" class="node-item flex align-center"> <div class="node-icon" :style="{color:node.data.iconColor}"> <em class="node-icon-inner" :class="node.data.nodeIconClassName" /> </div> <span class="node-name margin-left-sm" v-text="node.text" /> </div> <div class="popover-tips"> <p v-if="node.data.realname !== node.data.email && node.data.realname !== ''"> <span>{{ $t('userCenter.name') }}</span>: {{ node.data.realname }} </p> <p><span>{{ $t('common.email') }}</span>: {{ node.data.email }}</p> <p v-if="node.data.phone"> <span>{{ $t('common.mobile') }}</span>: {{ node.data.phone }} </p> <div v-if="belongingType==='use' && stopClick('accountMgmt')"> <el-divider /> <p class="text-center"> <el-button slot="reference" type="text" class="padding-lr-xs" @click="unBind(node)"> <para-lang code="selfService.group.accountUnbinding" /> </el-button> </p> </div> </div> </el-popover> <el-popover v-else-if="node.data.status !== 'accountMessage' && stopClick('accountMgmt')" trigger="hover" placement="right" popper-class="account-graph-popver" > <div slot="reference" class="node-item flex align-center"> <div class="node-icon" :style="{color:node.data.iconColor}"> <em class="node-icon-inner" :class="node.data.nodeIconClassName" /> </div> <span class="node-name margin-left-sm" v-text="node.text" /> </div> <div v-if="node.data.status === 'account'" class="popover-tips"> <p><span>{{ $t('common.SCAccount') }}</span>: {{ node.text }}</p> <div> <el-divider /> <p class="text-center"> <el-button v-if="belongingType==='belonging'" type="text" @click="modifyAccountRelation(node)"> 修改所属关系 </el-button> <el-button v-if="belongingType==='use'" type="text" @click="bindAccount(node)"> <para-lang code="selfService.group.accountBinding" /> </el-button> </p> </div> </div> <div v-if="node.data.status === 'ssc'" class="popover-tips"> <p><span>{{ $t('common.SCCode') }}</span>: {{ node.text }}</p> <el-divider /> <p class="text-center"> <el-button type="text" @click="showAccountApply(node.text)"> <para-lang code="selfService.group.applicationAccount" /> </el-button> </p> </div> </el-popover> <div v-else class="node-item flex align-center"> <div class="node-icon" :style="{color:node.data.iconColor}"> <em class="node-icon-inner" :class="node.data.nodeIconClassName" /> </div> <span class="node-name margin-left-sm" v-text="node.text" /> </div> </div> </relation-graph>图标配置项在Vue 组件的 data 里添加如下信息graphOptions: { 'debug': envName === 'development', // 仅开发或Istio环境下为调试模式 'allowShowMiniView': true, 'allowSwitchLineShape': true, 'defaultFocusRootNode': false, 'layouts': [ { 'label': '树状-横向', 'layoutName': 'tree', 'useLayoutStyleOptions': true, 'layoutClassName': 'member-layout-tree-v', 'defaultLineColor': '#DDDDDD', 'levelDistance': '300,300,300,500', 'min_per_width': 100, 'max_per_width': 300, 'min_per_height': 40, 'max_per_height': 100, 'defaultNodeShape': 1, 'defaultLineShape': 6, 'defaultJunctionPoint': 'lr', 'defaultExpandHolderPosition': 'right', 'from': 'left', 'defaultNodeHeight': '36', 'defaultNodeColor': '#C6E5FF', 'defaultNodeBorderWidth': 1, 'defaultNodeBorderColor': '#CED4D9', 'defaultNodeFontColor': '#666666' }, { 'label': '树状-纵向', 'layoutName': 'tree', 'useLayoutStyleOptions': true, // 'defaultFocusRootNode': false, 'layoutClassName': 'member-layout-tree-h', 'defaultLineColor': '#DDDDDD', // 'levelDistance': '120', 'min_per_width': 100, 'max_per_width': 400, 'min_per_height': 100, 'max_per_height': 500, 'defaultNodeShape': 1, 'defaultLineShape': 6, 'defaultJunctionPoint': 'tb', 'defaultExpandHolderPosition': 'bottom', 'from': 'top', 'defaultNodeHeight': '36', 'defaultNodeColor': '#C6E5FF', 'defaultNodeBorderWidth': 1, 'defaultNodeBorderColor': '#CED4D9', 'defaultNodeFontColor': '#666666' }, { 'label': '中心布局', 'layoutName': 'center', 'useLayoutStyleOptions': true, 'layoutClassName': 'member-layout-center', 'levelDistance': '80', 'defaultNodeShape': 1, 'defaultLineShape': 5, // 'defaultFocusRootNode': true, 'defaultLineColor': '#DDDDDD', 'defaultJunctionPoint': 'border', 'defaultNodeHeight': '36', 'defaultNodeColor': '#C6E5FF', 'defaultNodeBorderWidth': 1, 'defaultNodeBorderColor': '#CED4D9', 'defaultNodeFontColor': '#666666' } ] }将后端响应的数据组织成 Relation Graph 组件所需的数据在 methods 中添加如下方法 // 根据 RelationGraph 图表的数据组织形式,梳理内容 buildGraphData() { const _this = this const graphData = { rootId: '', nodes: [], links: [] } const nodes = graphData.nodes const links = graphData.links // 生成随机ID,针对数据无id属性的情况可生成一个随机ID const buildRandomId = function() { return 'mock-' + Date.now().toString(36) + '-' + Math.random().toString(36).substr(4) } var buildNodeData = function(node) { const nodeData = { 'id': node.id, 'name': node.name, 'data': { ...node } } const nodeClassMap = { 'ssc': { name: '超算中心', color: node.online === 1 ? '#9de173' : '#ccc', icon: 'console-svg svg-sc-center' }, 'user': { name: '用户', color: node.groupId === _this.user.userInfo.groupId ? '#C6E5FF' : '#8aabc1', iconColor: node.userId === _this.user.userInfo.group.payUserId ? '#f7c934' : '', icon: node.userId === _this.user.userInfo.group.payUserId ? 'console-svg svg-main-account' : 'console-svg svg-sub-account' }, 'account': { name: '超算账号', color: node.inSameGroup ? '#da97f9' : '#c7ceff', icon: 'console-svg svg-sc-account-2' }, 'accountMessage': { name: '账号信息', color: '#0f7dfa', icon: 'console-icon icon-sc-account' } } // nodeData.width = node.name.length * 12 nodeData.data.nodeIconClassName = nodeClassMap[node.status].icon nodeData.data.iconColor = nodeClassMap[node.status].iconColor || '#666' nodeData.color = nodeClassMap[node.status].color || '#C6E5FF' if (node.status === 'accountMessage') { nodeData.fontColor = '#fff' } return nodeData } const buildData = function(node) { let parentNodeId = node.id if (!parentNodeId) { node.id = parentNodeId = buildRandomId() } if (nodes.length === 0) { graphData.rootId = parentNodeId const nodeData = buildNodeData(node) nodes.push(nodeData) } if (node.children && node.children.length > 0) { for (const childNode of node.children) { // 若节点没有id,则给一个随机id值 if (!childNode.id) { childNode.id = buildRandomId() } const childNodeData = buildNodeData(childNode) const nodeLink = { from: parentNodeId, to: childNode.id, text: '' } nodes.push(childNodeData) links.push(nodeLink) // 递归添加子节点 if (childNode.children && childNode.children.length > 0) { buildData(childNode) } } } } if (this.drawData && this.drawData.length > 0) { buildData(this.drawData[0]) } return graphData }获取数据并初始化组件在 methods 中添加如下代码initGraph() { this.graphInited = false const graphData = this.buildGraphData() this.$nextTick(_ => { this.$refs.relationGraph.setJsonData(graphData, () => { this.graphInited = true }) }) }其他说明:layoutName 为 force 时存在一些问题,自动时会动态调整各节点的位置,导致一些交互受到影响,所以本工程没有启用自动布局使用纵向树状布局时节点较多的情况,后面的节点容易覆盖在上一个节点上,找了下github里的issue,说可以对各节点进行绝对布局实现,但是工作量会比较大,运营没在这个点上纠结,咱也就不考虑优化了。鼠标悬停在节点上时无法触发el-popover组件的悬停事件,经排查跟节点的 + - 折叠样式有关,该节点的宽度太宽,导致底部的节点内容无法响应鼠标事件,使用如下样式覆盖默认样式完美解决(以下样式代码包含图标组件其他元素的样式调整):.graph-container { ::v-deep .c-mini-graph { position: absolute; top:0; right:0; margin-top:0 } ::v-deep .c-mini-toolbar { margin-left:0; margin-top:0; right:0; bottom:10px; } /* 调整样式,避免出现账号悬停不显示用户信息的情况 */ ::v-deep .c-expand-positon-right{ width: auto; right:0; } ::v-deep .c-expand-positon-bottom { width: auto; left: 50%; margin-left: -10px; } ::v-deep .rel-node-shape-1 { overflow:hidden; padding:0; border-radius: 6px; } ::v-deep .node-item { padding: 0 10px 0 0; line-height:34px; .node-icon { background-color:#fff; width: 36px; text-align: center; line-height: 34px; vertical-align: middle; em { font-size:22px; } } .node-name { white-space:nowrap; display: inline-block; } } // 布局文字定位调整 ::v-deep .c-mb-child-panel .c-mb-button .c-mb-text{ padding-top:3px; display: block; width: 100%; position: relative; margin: 0; } }链接Relation Graph 官网Relation Graph 在Github的开源地址
2022年04月11日
386 阅读
0 评论
1 点赞
2021-07-14
在vue-cli3使用sass(scss)定义的全局样式及变量
为统一风格及便于后续维护,在项目中常用的主色调、圆角、阴影相关的样式定义到一个全局变量文件中,常规的使用需要每个Vue组件中进行import,该操作会比较繁琐,经过了解后发现可以基于webpack的loader参数引入全局变量的方式来实现,相关实现过程如下所示:vue.config.jsmodule.exports = { // ... css: { // 为便于在Vue组件中使用,全局引入variables.scss中定义的变量 loaderOptions: { sass: { prependData: `@import "@/styles/variables.scss";` } } } variables.css// base color $blue:#324157; $light-blue:#3A71A8; $red:#C03639; $pink: #E65D6E; $green: #30B08F; $tiffany: #4AB7BD; $yellow:#FEC171; $panGreen: #30B08F; $theme: #004ca1; // ...vue 组件<style lang="scss" scoped> .btn-cursor { cursor:pointer; color: $theme } </style>
2021年07月14日
403 阅读
0 评论
0 点赞
2021-06-30
一个简单的自定义工作流设计器实现
先瞅瞅产品提供的丑陋至极的原型效果再看看经过本人精雕细琢之后的惊艳效果Show me the code流程设计弹出层<template> <el-dialog width="760px" class="hkt-dlg-darkblue" :title="`流程设计:${formData.name}`" :visible.sync="isShow" destroy-on-close :close-on-click-modal="false"> <div class="dialog-wrap" v-loading="loading"> <div style="min-height:50vh"> <div class="info-block"> <el-row class="hkt-block-title"> <el-col :span="12"><div><span class="title-name">流程配置</span></div></el-col> <el-col :span="12" class="text-right"> <el-button type="text" @click="formModalChange('New', null)"><i class="el-icon-plus"></i> 添加审核节点</el-button> </el-col> </el-row> <div class="info-content"> <el-steps direction="vertical" :active="nodes.length"> <el-step v-for="(step, stepIndex) in nodes" class="step-item-wrap" :class="'step-item-signmodel-' + step.signModel" :key="step + stepIndex"> <div slot="title"> <div style="line-height:32px;heihgt:38px;"> <el-row> <el-col :span="12"> <span class="step-name" v-text="step.nodeName"></span> </el-col> <el-col :span="12"> <div class="step-item-oprations text-right"> <el-button size="mini" type="text" @click="stepSort(stepIndex, 'up')" v-if="stepIndex > 0"><i class="el-icon-top"></i> 上移</el-button> <el-button size="mini" type="text"@click="stepSort(stepIndex, 'down')" v-if="stepIndex < nodes.length - 1"><i class="el-icon-bottom"></i> 下移</el-button> <el-button size="mini" type="text" @click="formModalChange('Edit', step, stepIndex)"><i class="el-icon-edit"></i> 修改</el-button> <el-button size="mini" type="text" @click="removeStep(step, stepIndex)"><i class="el-icon-close"></i> 删除</el-button> <el-button size="mini" type="text" @click="addPersons(step, stepIndex)"><i class="el-icon-user"></i> 配置审批人</el-button> </div> </el-col> </el-row> </div> </div> <div slot="description"> <div v-if="step.approveNames.length === 0"> <span class="color-gray">暂未配置审批人员</span> </div> <div v-else class="step-item-content-wrap"> <div class="person-item"> <el-tag v-for="(person, personIndex) in step.approveNames" :key="person" disable-transitions @close="removePerson(stepIndex, personIndex)" closable>{{person}}</el-tag> </div> </div> </div> </el-step> </el-steps> <div v-if="nodes.length === 0"> <h3 style="padding-top:40px;font-weight:normal;color:#dedede;font-size: 22px; text-align:center"><p>当前流程暂未配置审核节点信息,</p><p>请单击右上角的“添加审核节点”按钮进行配置</p></h3> </div> </div> </div> </div> </div> <div slot="footer" class="dialog-footer-wrap text-center"> <el-button @click="isShow = false">取 消</el-button> <el-button type="primary" :loading="submitting" @click="save">{{submitting ? '保存中': '确 定'}}</el-button> </div> <step-form-modal :show.sync="formModalVisible" v-if="formModalVisible" :formData="currentRow" :mode="formMode" @callback="formCallback" /> </el-dialog> </template> <script> import stepFormModal from './stepFormModal.vue' export default { name: 'flowDesignModal', desc: '流程设计器', components: { stepFormModal }, props: { show: { type: Boolean, required: true, default: false }, formData: { // 适用于编辑 type: Object, required: false }, mode: { type: String, required: false } }, data () { return { loading: true, form: { }, nodes: [], formModalVisible: false, currentRow: null, currentStepIndex: null, formMode:'New', // New || Edit submitting: false } }, computed: { isShow: { get () { return this.show }, set (val) { this.$emit('update:show', val) } } }, created() { this.init() }, methods: { init () { if (this.formData) { this.getFlowNodes() // this.form = this.$hktUtils.deepClone(this.formData) } }, stepSort (index, type) { const steps = this.nodes if (type === 'up') { ;[steps[index], steps[index - 1]] = [steps[index - 1], steps[index]] } else { ;[steps[index], steps[index + 1]] = [steps[index + 1], steps[index]] } // 触发页面更新 this.nodes = steps.concat() }, async getFlowNodes () { this.loading = true const res = await this.$request.get(`/v1/manager/flow/node/listByFlowId?flowId=${this.formData.id}`) if (res.success) { this.nodes = res.data } this.loading = false }, // 添加或编辑流程节点 formModalChange (mode, formData, stepIndex) { this.formMode = mode this.currentRow = formData this.currentStepIndex = stepIndex this.formModalVisible = true }, formCallback (item) { // TODO 判断步骤名称是否已存在 if (this.formMode === 'New') { item.flowId = this.formData.id item.approveArray = [] item.approveNames = [] this.nodes.push(item) } else if (this.currentStepIndex >= 0) { this.nodes[this.currentStepIndex] = item this.currentStepIndex = null } }, addPersons (row, stepIndex) { const dialogProps = { multipleFlag: 'Y' } if (row.approveArray && row.approveArray.length > 0) { dialogProps.initRightUsers = row.approveArray.join(',') } window.hktCommon.selectUser(`配置 ${row.nodeName} 节点的审批人`, dialogProps, (res) => { row.approveArray = res.map(item => item.id) row.approveNames = res.map(item => item.name) this.nodes.splice(stepIndex, 1, this.$hktUtils.deepClone(row)) }) }, removePerson(stepIndex, personIndex) { this.nodes[stepIndex].approveArray.splice(personIndex, 1) this.nodes[stepIndex].approveNames.splice(personIndex, 1) }, removeStep(stepItem, stepIndex) { this.$confirm(`本操作不可恢复,确定删除该步骤(${stepItem.nodeName})?`, '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning', }).then(() => { this.nodes.splice(stepIndex, 1) }).catch(() => {}) }, // 保存 async save () { const flowNodes = this.$hktUtils.deepClone(this.nodes) const existNodeNames = []// 用于校验名称是否重复 for (let i = 0; i < flowNodes.length; i++) { const flowNode = flowNodes[i] if (flowNode.approveArray.length === 0) { this.$message({ message: `审核步骤(${flowNode.nodeName})至少需选择一个的审批人`, type: 'warning' }) return } if (existNodeNames.includes(flowNode.nodeName)) { this.$message({ message: `存在相同的审核节点名称:${flowNode.nodeName}`, type: 'warning' }) return } existNodeNames.push(flowNode.nodeName) flowNode.orders = i +1 flowNode.approveIds = flowNode.approveArray.join(',') } this.submitting = true // 执行保存入库相关操作 const res = await this.$request.post(`/v1/manager/flow/node/add`, flowNodes).catch(err => { this.submitting = false }) this.submitting = false if (res && res.success) { this.$emit('callback', flowNodes) this.isShow = false } } } } </script> <style lang="scss" scoped> .form-body-item { background-color:#fff; padding:10px; border-radius: 4px; margin-bottom: 10px; } .form-item-zones { border-bottom: solid 1px #DCDFE6 ; margin-bottom:20px; .block-title { font-weight:bold; display: inline-block; line-height: 36px; font-size:14px; } } .step-item-oprations { display: none; } .step-item-wrap { /deep/ .el-step__main { padding: 0 10px 10px; } } .step-item-wrap:hover { /deep/ .el-step__main { background-color:#F5F7FA; border-radius: 5px; } .step-item-oprations { display: block; } } .step-item-content-wrap { padding: 10px; } .btn-choose { margin-left:10px; vertical-align: middle; } .person-item /deep/ { display: inline-block; .el-tag { vertical-align: middle; position: relative; overflow: initial; margin-bottom:10px; .el-tag__close { padding: 2px; box-sizing: content-box; position:absolute; background-color: #D1E9FF; // display: none; top:-10px; right: -10px; &:hover { background-color: #1890ff; } } &:hover { .el-tag__close { display:block; } } } .el-tag + .el-tag { margin-left: 40px; &:before { content: '或'; position: absolute; left: -26px; color: #666666; } } } // 会签模式步骤的审批人之间是用和 .info-content /deep/ .step-item-signmodel-1 .person-item { .el-tag + .el-tag { &:before { content: '和' } } } .step-name { color:#101010; font-weight: bold; } </style> 步骤编辑弹出层<template> <el-dialog width="460px" class="hkt-dlg-darkblue" :title="(form.id ? '编辑' : '新建') + '步骤'" :visible.sync="isShow" append-to-body destroy-on-close :close-on-click-modal="false"> <div class="dialog-wrap"> <el-form ref="form" :model="form"> <el-form-item label="步骤名称" prop="nodeName" :rules="[{required: true, message: '请填写步骤名称', trigger: 'change'}]"> <el-input v-model="form.nodeName" style="width:250px"></el-input> </el-form-item> <el-form-item label="是否会签模式"> <el-switch v-model="form.signModel" :active-value="1" :inactive-value="2" active-text="是" inactive-text="否"></el-switch> </el-form-item> <el-form-item label="是否可办结"> <el-switch v-model="form.isOver" :active-value="1" :inactive-value="0" active-text="是" inactive-text="否"></el-switch> </el-form-item> </el-form> </div> <div slot="footer" class="dialog-footer-wrap text-center"> <el-button @click="isShow = false">取 消</el-button> <el-button type="primary" @click="save">确 定</el-button> </div> </el-dialog> </template> <script> export default { name: 'stepFormModal', desc: '流程步骤表单', props: { show: { type: Boolean, required: true, default: false }, formData: { // 适用于编辑 type: Object, required: false }, mode: { type: String, required: false } }, data () { return { form: { nodeName: '', signModel: 2, isOver: 0 } } }, computed: { isShow: { get () { return this.show }, set (val) { this.$emit('update:show', val) } } }, created() { this.init() }, methods: { init () { if (this.formData) { this.form = this.$hktUtils.deepClone(this.formData) } }, save () { this.$refs['form'].validate(async (valid) => { if (valid) { this.$emit('callback', this.form) this.isShow = false } }) } } } </script> <style lang="scss" scoped> .form-body-item { background-color:#fff; padding:10px; border-radius: 4px; margin-bottom: 10px; } .form-item-zones { border-bottom: solid 1px #DCDFE6 ; margin-bottom:20px; .block-title { font-weight:bold; display: inline-block; line-height: 36px; font-size:14px; } } .step-item-oprations { display: none; } .step-item-wrap:hover { background-color:#D1E9FF; .step-item-oprations { display: block; } } .step-item-content-wrap { padding: 10px; } .btn-choose { margin-left:10px; vertical-align: middle; } .person-item /deep/ { display: inline-block; .el-tag { vertical-align: middle; position: relative; overflow: initial; .el-tag__close { padding: 2px; box-sizing: content-box; position:absolute; background-color: #D1E9FF; // display: none; top:-10px; right: -10px; &:hover { background-color: #1890ff; } } &:hover { .el-tag__close { display:block; } } } .el-tag + .el-tag { margin-left: 40px; &:before { content: '或'; position: absolute; left: -26px; color: #666666; } } } .step-name { color:#101010; font-weight: bold; } </style> 该组件涉及人员选择相关公共组件的封装,详细见 分享一个在管理系统中一些公共组件的调用方式
2021年06月30日
161 阅读
0 评论
0 点赞
2021-06-30
分享一个在管理系统中一些公共组件的调用方式
公共组件效果查看合同:地图描点:公共资源文件上传:人员选择-多选:人员选择-单选:增加公共组件目录核心代码如下:<template> <div class="common-dialogs-wrap"> <!-- 公共资源上传 --> <resource-modal :title="resourcePickerDlg.dialogTitle" :resourceDialogProps.sync="resourcePickerDlg.resourceDialogProps" v-if="resourcePickerDlg.show" :show.sync="resourcePickerDlg.show" @callback="resourceCallback"></resource-modal> <!-- 公共人员选择 --> <select-user-modal :title="userSelectDlg.dialogTitle" :dialogProps.sync="userSelectDlg.dialogProps" v-if="userSelectDlg.show" :show.sync="userSelectDlg.show" @callback="userSelectedCallback" ></select-user-modal> <!-- 合同详情弹出层 --> <contract-detail-modal :show.sync="contractDetailDlg.show" v-if="contractDetailDlg.show" :detail-id="contractDetailDlg.dialogProps.contractId"></contract-detail-modal> <!-- 地图描点 --> <picker-q-map-position-modal :show.sync="qMaplDlg.show" v-if="qMaplDlg.show" @callback="qMapCallback"></picker-q-map-position-modal> <!-- 预留后续扩展其他公共组件 --> </div> </template> <script> const ResourceModal = () => import('./ResourceModal.vue') const SelectUserModal = () => import('./SelectUserModal.vue') const ContractDetailModal = () => import('@/views/contract-manage/contractList/DetailContractModel.vue') // 合同详情弹出层 const PickerQMapPositionModal = () => import('./PickerQMapPositionModal.vue') // 腾讯地图描点 var hktCommon = {} var uploadCallbackFn = null // 资源上传回调 var userSelectCallback = null // 用户选择回调 var qMapCallbackFn = null // 腾讯地图描点回调 export default { name: 'HktCommon', desc: '华宽通公共组件封装', components: { ResourceModal, SelectUserModal, ContractDetailModal, PickerQMapPositionModal }, data () { return { // 资源上传对话框默认参数 resourceDialogProps: { businessCode: 'resources', // 业务代码,默认为resources resourceType: 'file', // 资源类型,file||image||document||video||audio accept: '', // 接受上传的文件类型 multipleFlag: 'Y' // 是否允许多个 }, resourcePickerDlg: { show: false, dialogTitle: '选择资源', resourceDialogProps: {} }, userSelectDlg: { show: false, dialogTitle: '选择人员', dialogProps: { multipleFlag: 'N', // 是否支持多选 organizationId: '', // 机构ID ignoreUsernames: '', // 不能选择的人员用户名 initRightUsers: '' // 复选上的用户,多个使用逗号分隔 } }, // 合同详情弹出层 contractDetailDlg: { show: false, dialogTitle: '合同信息', dialogProps: { contractId: '' // 合同ID } }, // 合同详情弹出层 qMaplDlg: { show: false, dialogTitle: '地图描点', dialogProps: { } } } }, created () { window.hktCommon = hktCommon this.init() }, methods: { init () { // 初始化公共上传组件 this.initUpload() // 初始化用户选择组件 this.initSelectUser() // 初始化查看合同详情组件 this.initContractDetailModal() // 初始化查看合同详情组件 this.initQMapModal() // TODO:后续可继续扩展其他公共组件 }, initUpload () { // 上传图片 hktCommon.uploadImage = (title, props, callback) => { if (arguments.length === 1 && typeof arguments[0] === 'function') { title = '上传图片' props = { resourceType: 'image' } callback = arguments[0] } if (arguments.length === 2 && typeof arguments[0] === 'string' && typeof arguments[1] === 'function') { props = { resourceType: 'image' } callback = arguments[1] } if (arguments.length === 2 && typeof arguments[0] === 'object' && typeof arguments[1] === 'function') { title = '上传图片' props = arguments[0] callback = arguments[1] } if (!props) { props = { resourceType: 'image' } } if (!props.accept) { props.accept = '.jpg,.png,.gif' } upload('image', title, props, callback) } // 上传视频 hktCommon.uploadVideo = (title, props, callback) => { if (arguments.length === 2 && typeof arguments[0] === 'object' && typeof arguments[1] === 'function') { title = '视频上传' props = arguments[0] callback = arguments[1] } upload('video', title, props, callback) } // 上传音频 hktCommon.uploadAudio = (title, props, callback) => { if (arguments.length === 2 && typeof arguments[0] === 'object' && typeof arguments[1] === 'function') { title = '音频上传' props = arguments[0] callback = arguments[1] } upload('audio', title, props, callback) } // 上传资源 hktCommon.uploadResource = (title, props, callback) => { if (arguments.length === 2 && typeof arguments[0] === 'object' && typeof arguments[1] === 'function') { title = '文件上传' props = arguments[0] callback = arguments[1] } let resourceType = 'file' if (props.resourceType) { resourceType = props.resourceType } upload(resourceType, title, props, callback) } // 定义公共上传方法 const upload = (type, title, props, callback) => { this.resourcePickerDlg.dialogTitle = title const resourceDialogProps = Object.assign({}, this.resourceDialogProps, props) resourceDialogProps.resourceType = type this.resourcePickerDlg.resourceDialogProps = resourceDialogProps this.resourcePickerDlg.show = true if (callback) { uploadCallbackFn = callback } else { uploadCallbackFn = null } } // 完全自主控制上传方式 hktCommon.upload = upload }, initSelectUser() { const self = this /** * 公共选择用户方法 * @param title 对话框标题 * @param props 人员选择配置信息,配置属性见:userSelectDlg.dialogProps * @param callback 选择人员后的回调 */ hktCommon.selectUser = function(title, props, callback) { if (arguments.length === 2 && typeof arguments[0] === 'object' && typeof arguments[1] === 'function') { title = '选择用户' props = arguments[0] callback = arguments[1] } else if (arguments.length === 2 && typeof arguments[0] === 'string' && typeof arguments[1] === 'function') { callback = arguments[1] props = { multipleFlag: 'N', // 是否支持多选 organizationId: '', // 机构ID ignoreUsernames: '' // 不能选择的人员用户名 } } self.userSelectDlg.dialogTitle = title self.userSelectDlg.dialogProps = props self.userSelectDlg.show = true if (callback) { userSelectCallback = callback } else { userSelectCallback = null } } }, // 初始化合同详情界面 initContractDetailModal () { const self = this hktCommon.showContract = function (contractId) { self.contractDetailDlg.dialogProps.contractId = contractId self.contractDetailDlg.show = true } }, // 初始化腾讯地图选择弹出层 initQMapModal () { const self = this hktCommon.pickQMap = function (callback) { self.qMaplDlg.show = true if (callback) { qMapCallbackFn = callback } else { qMapCallbackFn = null } } }, // 选择资源后的回调 resourceCallback (resourceData) { // 可以通过事件机制获取选择后的资源 if (window.eventHub && typeof window.eventHub.$emit === 'function') { window.eventHub.$emit('resourcePicked', resourceData) } if (typeof uploadCallbackFn === 'function') { uploadCallbackFn(resourceData) uploadCallbackFn = null } }, // 选择人员回调 userSelectedCallback(userData) { // 可以通过事件机制获取选择后的资源 if (window.eventHub && typeof window.eventHub.$emit === 'function') { window.eventHub.$emit('userSelected', userData) } if (typeof userSelectCallback === 'function') { userSelectCallback(userData) userSelectCallback = null } }, // 地图描点后的回调 qMapCallback (latLng) { // 可以通过事件机制获取选择后的资源 if (window.eventHub && typeof window.eventHub.$emit === 'function') { window.eventHub.$emit('qMapCallPicked', latLng) } if (typeof qMapCallbackFn === 'function') { qMapCallbackFn(latLng) qMapCallbackFn = null } }, } } </script> main.js主方法中引入公共组件:import HktCommon from './views/hktcommon/HktCommon.vue' // 增加公共组件对象 const commonEl = document.createElement('div') document.body.appendChild(commonEl) new Vue({ name: 'AppCommonRoot', el: commonEl, render: h => h(HktCommon) })在其他组件中调用全局方法:// 上传资源 hktCommon.uploadResource(this.dialogTitle, dialogProps, (resources) => { this.uploadCallback(resources) }) // 选择用户: window.hktCommon.selectUser(`配置 ${row.nodeName} 节点的审批人`, dialogProps, (res) => { row.approveArray = res.map(item => item.id) row.approveNames = res.map(item => item.name) this.nodes.splice(stepIndex, 1, this.$hktUtils.deepClone(row)) }) // 显示合同信息 hktCommon.showContract = function (contractId) { self.contractDetailDlg.dialogProps.contractId = contractId self.contractDetailDlg.show = true } // 地图描点 window.hktCommon.pickQMap((res) => { self.projectForm.areaCode = `${res.latitude},${res.longitude}` })
2021年06月30日
68 阅读
0 评论
0 点赞
2020-03-25
浅谈vue中axios的封装
在Vue项目中我们一般都会使用 axios 作为 Ajax 请求库。它是基于promise的http库,他有很多优秀的特性,例如拦截请求和响应、取消请求、转换json、客户端防御cSRF等。所以我们的尤大大也是果断放弃了对其官方库vue-resource的维护,直接推荐我们使用axios库。在实际开发过程我们一般都会将常用的方法做一些封装以便于在项目中调用。工程添加axios依赖yarn add axios -S建立axios封装文件htttp.jsimport axios from 'axios'; import { Message } from 'element-ui'; axios.defaults.timeout = 5000; axios.defaults.baseURL =''; //http request 拦截器 axios.interceptors.request.use( config => { // const token = getCookie('名称');注意使用的时候需要引入cookie方法,推荐js-cookie config.data = JSON.stringify(config.data); config.headers = { 'Content-Type':'application/x-www-form-urlencoded' } // if(token){ // config.params = {'token':token} // } return config; }, error => { return Promise.reject(err); } ); //http response 拦截器 axios.interceptors.response.use( response => { if(response.data.errCode ==2){ router.push({ path:"/login", query:{redirect:router.currentRoute.fullPath}//从哪个页面跳转 }) } return response; }, error => { return Promise.reject(error) } ) /** * 封装get方法 * @param url * @param data * @returns {Promise} */ export function fetch(url,params={}){ return new Promise((resolve,reject) => { axios.get(url,{ params:params }) .then(response => { resolve(response.data); }) .catch(err => { reject(err) }) }) } /** * 封装post请求 * @param url * @param data * @returns {Promise} */ export function post(url,data = {}){ return new Promise((resolve,reject) => { axios.post(url,data) .then(response => { resolve(response.data); },err => { reject(err) }) }) } /** * 封装patch请求 * @param url * @param data * @returns {Promise} */ export function patch(url,data = {}){ return new Promise((resolve,reject) => { axios.patch(url,data) .then(response => { resolve(response.data); },err => { reject(err) }) }) } /** * 封装put请求 * @param url * @param data * @returns {Promise} */ export function put(url,data = {}){ return new Promise((resolve,reject) => { axios.put(url,data) .then(response => { resolve(response.data); },err => { reject(err) }) }) }在入口文件main.js中引入并注册为vue原型方法import axios from 'axios' import {post,fetch,patch,put} from './utils/http.js' //定义全局变量 Vue.prototype.$post=post; Vue.prototype.$fetch=fetch; Vue.prototype.$patch=patch; Vue.prototype.$put=put;在vue组件里直接使用mounted(){ this.$fetch('/api/v2/contents') .then((response) => { console.log(response) }) }, //其余的方法一样
2020年03月25日
103 阅读
0 评论
0 点赞