将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: ` `, } } } ] }), 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 ↓
背景介绍我们项目提供了在线的项目文档，便于用户在使用过程中有什么问题可以通过直接查文档解决，也在一定程度上减少了公司运营及客服成本。在实际使用过程中，用户反馈造作文档的使用不是很方便，基本上要打开新Tab页签，不能方便的一遍对照文档，一边操作。解决方案为解决以上问题，在参考阿里云及腾讯云相关产品的文档模式后，引入了随动帮助文档机制。该机制主要实现模式如下：1、在相关功能指定位置添加文档入口2、单击文档名称后，在页面左下角显示文档弹出层(以下简称「文档查看器」)，主要是系统的大部分功能使用右侧展开的抽屉组件 (Drawer)进行显示。3、打开的文档查看器，可垂直方向最大，可上下左右四边及四个角落通过拖拽调整大小4、可拖拽到页面任意位置5、文档查看器不影响页面中其他组件的交互该功能是系统每个功能模块都有可能会用到的，所以将起提取为一个公共组件。组件只需传入功能标识，然后调用内容管理系统的接口获取数据，如果有数据则显示文档入口，由于一个功能极有可能会对应有多篇帮助文档，用户从文档入口单击将展开文档列表，用户单击需要查看的文档，则在左下角显示文档。实现过程示例中均使用模拟的数据，实际数据在生产环境调用内容管理系统的接口获取。实现文档查看器组件HelpModal.vue文档查看器的拖拽及大小调整均通过指令控制 <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 > 功能指南 如何使用一键转区 更新时间：2022-08-09 10:10 可使用【一键转区】功能，将超算账号的数据转移至指定的超算上进行使用，如超算中心A3分区的sc0001账号（即为源目标）转移至超算分区A6的sc0001（即为目的目标），具体操作如下: 1. 单击【一键转区】中的【申请一键转区】 2. 在【源目标】处选择可用的超算分区及超算账号，例如A3超算分区的sc0001； 在【目的目标】处选择匹配的超算分区，不用选择转区后的超算账号，例如A6超算分区； 在【源目标账号环境信息】处填软件使用时的环境要求。 3. 单击【确定】，会有提示框告知源目标超算账号保存3个月，3个月后超算账号回收的提醒。 4. 在【一键转区】的页面查看工单进展。 <el-button type="primary" class="btn-open-in-window" @click="openDocLink"> 新窗口查看文档 </el-button> </el-dialog> 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 } 实现文档入口组件 0" class="fn-doc-wrap"> <el-popover position="bottom" trigger="hover" > <el-button slot="reference" type="text" class="padding-xs"> </el-button> <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> </el-popover> 实现后的效果{dplayer src="/usr/uploads/2023/06/592593977.mp4"/}
记一次使用v-charts绘制图表出现重影的处理经历分享
一、添加依赖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结尾的文件
背景说明近期检查前端工程的打包情况，发现@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"> <el-popover v-if="node.data.status === 'user'" trigger="hover" placement="right" popper-class="account-graph-popver" > {{ $t('userCenter.name') }}: {{ node.data.realname }} {{ $t('common.email') }}: {{ node.data.email }} {{ $t('common.mobile') }}: {{ node.data.phone }} <el-divider /> <el-button slot="reference" type="text" class="padding-lr-xs" @click="unBind(node)"> <para-lang code="selfService.group.accountUnbinding" /> </el-button> </el-popover> <el-popover v-else-if="node.data.status !== 'accountMessage' && stopClick('accountMgmt')" trigger="hover" placement="right" popper-class="account-graph-popver" > {{ $t('common.SCAccount') }}: {{ node.text }} <el-divider /> <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> {{ $t('common.SCCode') }}: {{ node.text }} <el-divider /> <el-button type="text" @click="showAccountApply(node.text)"> <para-lang code="selfService.group.applicationAccount" /> </el-button> </el-popover> </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的开源地址
为统一风格及便于后续维护，在项目中常用的主色调、圆角、阴影相关的样式定义到一个全局变量文件中，常规的使用需要每个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 组件
一个简单的自定义工作流设计器实现
先瞅瞅产品提供的丑陋至极的原型效果再看看经过本人精雕细琢之后的惊艳效果Show me the code流程设计弹出层 <el-dialog width="760px" class="hkt-dlg-darkblue" :title="`流程设计：${formData.name}`" :visible.sync="isShow" destroy-on-close :close-on-click-modal="false"> <el-row class="hkt-block-title"> <el-col :span="12"> 流程配置 </el-col> <el-col :span="12" class="text-right"> <el-button type="text" @click="formModalChange('New', null)"> 添加审核节点</el-button> </el-col> </el-row> <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"> <el-row> <el-col :span="12"> </el-col> <el-col :span="12"> <el-button size="mini" type="text" @click="stepSort(stepIndex, 'up')" v-if="stepIndex > 0"> 上移</el-button> <el-button size="mini" type="text"@click="stepSort(stepIndex, 'down')" v-if="stepIndex < nodes.length - 1"> 下移</el-button> <el-button size="mini" type="text" @click="formModalChange('Edit', step, stepIndex)"> 修改</el-button> <el-button size="mini" type="text" @click="removeStep(step, stepIndex)"> 删除</el-button> <el-button size="mini" type="text" @click="addPersons(step, stepIndex)"> 配置审批人</el-button> </el-col> </el-row> 暂未配置审批人员 <el-tag v-for="(person, personIndex) in step.approveNames" :key="person" disable-transitions @close="removePerson(stepIndex, personIndex)" closable>{{person}}</el-tag> </el-step> </el-steps> 当前流程暂未配置审核节点信息， 请单击右上角的“添加审核节点”按钮进行配置 <el-button @click="isShow = false">取 消</el-button> <el-button type="primary" :loading="submitting" @click="save">{{submitting ? '保存中': '确 定'}}</el-button> <step-form-modal :show.sync="formModalVisible" v-if="formModalVisible" :formData="currentRow" :mode="formMode" @callback="formCallback" /> </el-dialog> 步骤编辑弹出层 <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"> <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> <el-button @click="isShow = false">取 消</el-button> <el-button type="primary" @click="save">确 定</el-button> </el-dialog> 该组件涉及人员选择相关公共组件的封装，详细见 分享一个在管理系统中一些公共组件的调用方式
分享一个在管理系统中一些公共组件的调用方式
公共组件效果查看合同：地图描点：公共资源文件上传：人员选择-多选：人员选择-单选：增加公共组件目录核心代码如下： <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> 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}` })
在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) }) }, //其余的方法一样