美文网首页
个人项目记录

个人项目记录

作者: 羽晞yose | 来源:发表于2021-02-02 16:12 被阅读0次

因公司业务需要,去往远方开发项目,大半年来除了加班还是加班,开发了三个后台系统,使用的vue-element-admin框架,该文章如标题,为个人记录。

vue-element-admin

该框架作者有写了两篇webpack教学文章,其中详细分析了为何该框架为何这么配置,两篇文章建议仔细阅读。得益于webpack 4+vue/cli3+作者做的配置,基本已经达到开箱即用了,不过多介绍
手摸手,带你用合理的姿势使用webpack4(上)
手摸手,带你用合理的姿势使用webpack4(下)

vue-cli不支持 eject 来弹出默认配置

当初刚开始看的时候想要看一下目前vue-cli究竟做了哪些默认配置(官网写的一般不会完全覆盖,所以想自己查看默认配置),可惜并没有类似 react 的 eject来弹出
关于为何没有可查看以下链接,比较赞同官方,为了一些特定场景而去加大他们的工作量,并且并不能带来实质收益,对到框架开发来讲不合理
Eject from Vue CLI / Export Webpack Config

查看vue/cli默认配置

如果不清楚vue/cli做的默认配置,那么遇到问题的时候其实只靠网上搜索很难找到自己遇到的问题,因此先了解vue/cli的默认配置,方便出现问题的时候查看是否有某些配置没更改,进入项目,在项目根目录下

  • 运行命令,在终端输出:
    开发环境:npx vue-cli-service inspect --mode development
    生产环境:npx vue-cli-service inspect --mode production
  • 运行命令,将输出导入到 js 文件:
    开发环境:npx vue-cli-service inspect --mode development >> webpack.config.development.js
    生产环境:npx vue-cli-service inspect --mode production >> webpack.config.production.js

参考地址:
vue-cli-service-inspect
修改插件选项

动态路由无法分包

该问题直到项目后期才发现,因此已经没法追溯究竟什么时候开始构建的时候分包失败

由于项目中的菜单需要根据权限进行管理,角色权限没有的菜单栏不会显示到侧边栏上,所以由后台返回对应的页面级组件地址(字符串,例如'./view/order'),前端使用require()来获取正式的component(其结果类似于route中component: './view/order'


参考地址:
Webpack-Vue 分片优化——为什么使用懒加载() => import() 里面的组件没有分片打包
后改为映射表,通过路径来映射前端component,再使用import来导入对应的component(并非真正原因,因为vue/cli3默认就是可以对动态路由进行分包的,改完之后依旧无效。自行配置spliteChunks后虽然成功分包了,但是动态路由也失效了,没法按需加载,直接一进首页全部请求下来了),该记录仅为让自己以后如果出现问题还能往这方面考虑


真实预发布环境增加了一个pre环境(也就是多了一个.env.preprod文件),虽然里面已经指定了NODE_ENV=production,然而发现一个奇怪的bug,就是不管怎样都无法分包(代码压缩等都正常),并且该bug表现为:

  1. 如果先执行build:prod,则正常分包,此时再执行build:pre,结果也会分包
  2. 如果先执行build:pre,则无法分包,此时再执行build:prod,结果也无法分包
  3. 如果更改一下chunkName,再执行build:prod,又会正常分包
  4. 删除node_modules中的缓存,执行build:prod,也会恢复正常分包

这里涉及知识盲区,不继续深究,以后技术深度有所突破能知道原因再来更新此文,后来与后台协定,预发布与正式均使用build:prod,不再区分环境(因为对到前端来讲其实预发布与正式环境代码都是同一份)

打包后的app.css巨大

构建后发现app.css包居然达到15M,饿了么样式被重复打包
优化:去除入口文件引入的饿了么UI默认样式(官网有说自定义样式不需要引入默认样式,仅作提示),将vue-element-admin中主题配置文件拆分(因为项目中可能会有很多需要引用主题色变量的,直接从该配置文件获取主题相关配置样式,会导致饿了么样式被重复打包)
参考链接:
自定义主题样式文件多次打包 ,解决方法为Tofandel这名用户所说的,不要将变量与自定义导入主题放在一块

icon-font打包后乱码,浏览器能正常显示

该问题表现为打包结果乱码(为一个空芯方块),但代码在浏览器中可正常显示iconf-font。打开控制台再刷新,则第一次icon-font必定展示乱码(也就是为什么Unicode明文在F12的时候会有一次无法识别)
百度出来的肯定是将dart-sass换成node-sass,但根据自行查看issue,dart-sass确实会导致编译为Unicode 明文,但貌似并非底层真正原因。由于时间也是换成node-sass来解决,先做记录,后续再看issue
用最新的框架,打包出来element的字体图标乱码了? #3526
页面刷新有时候elementui 的字体图标会乱码 #19247

现代浏览器type="password"自动填充密码

查看了很多文章,全体方案阵亡,包括:

  1. 设置autocomplete
  2. 通过动态设置readonly
  3. 动态更改type为password
  4. 增加两个autocomplete="off"的隐藏密码输入框

不管哪一个方案,在切换成password的时候,自动填充又会出现,通过stackoverflow了解到之前这些方案都行不通了,原本打算自行实行模态密码框来模拟密码输入框(本质还是text)
但通过stackoverflow了解到可以将type设置为text,通过css设置text-security: disc;来模拟密码输入框,但这个时候下面也有人说了复制粘贴会复制到全都是小圆点,但密码本来就不该让用户复制粘贴,所以再将密码框的粘贴事件禁用即可onpaste="return false"
如何阻止浏览器自动填充账号密码
下面这篇文章我没尝试,稍微看了一下也是动态设置type和readonly这些方案,但是多管齐下,不确定是否有用,有兴趣的可以自行尝试
完美解决 element-ui input=password 在浏览器会自动填充密码的问题

封装后的v-password组件

<template>
    <el-input
        type="text"
        :class="['model-password', {'is-show-password': isShowPassword}]"
        v-bind="$attrs"
        onpaste="return false"
        v-on="$listeners"
    >
        <slot slot="prefix" name="prefix" />
        <slot slot="suffix" name="suffix">
            <span class="el-input__icon el-icon-circle-close el-input__clear" @click="clearValue" />
            <span class="el-input__icon el-icon-view el-input__clear" @click="toggleType" />
        </slot>
        <slot slot="prepend" name="prepend" />
        <slot slot="append" name="append" />
    </el-input>
</template>

<script>
/**
 * 现代浏览器type="password"自动填充密码,查看了很多文章,全体方案阵亡,方案包括:
 * 设置autocomplete
 * 通过动态设置readonly
 * 动态更改type为password
 * 增加两个autocomplete="off"的隐藏密码输入框
 * -------------------------------------------------------------------
 * 自用的模态密码输入框,组件特点:
 * 1. 不出现自动填充账号密码弹出层
 * 2. 密码不允许粘贴
 * 3. 使用方式完全同 el-input
 * 4. type固定为text,不可变更,因为password会导致自动填充
 */
export default {
    name: 'VPassword',
    data () {
        return {
            isShowPassword: true
        }
    },
    computed: {

    },
    methods: {
        toggleType () {
            this.isShowPassword = !this.isShowPassword
        },
        clearValue () {
            this.$emit('input', '')
        }
    }
}
</script>

<style lang="scss" scoped>
    .model-password {
        .el-icon-circle-close {
            display: none;
        }
        &:hover {
            .el-icon-circle-close {
                display: inline-block;
            }
        }
    }
    .is-show-password {
        ::v-deep .el-input__inner {
            text-security: disc;
            -webkit-text-security:disc;
        }
    }
</style>

前端文件下载

前端文件下载最简单的方式是<a></a>使用download属性
<a download href="文件下载地址">文件名</a>
但是这种下载方法不能跨域,非同域download会无效。而使用新开页面下载的方式会有一个弹窗闪一下影响体验。由于项目中有需要下载第三方文件的需求,还有私有桶提供的blob文件流,所以更改为使用文件id请求模拟点击下载,代码如下

// 工具函数
const Tool = {
    _getBlob (ret, fileName, file) {
        const type = ret.type
        const blob = new Blob([ret], { type }) // type必须指定,即使时流文件,否则火狐下载无后缀
        const ie = navigator.userAgent.match(/MSIE\s([\d.]+)/)
        const ie11 = navigator.userAgent.match(/Trident\/7.0/) && navigator.userAgent.match(/rv:11/)
        const ieEDGE = navigator.userAgent.match(/Edge/g)
        const ieVer = (ie ? ie[1] : (ie11 ? 11 : (ieEDGE ? 12 : -1)))

        if (ie && ieVer < 10) {
            Message.error('您的浏览器版本过低,请切换到IE EDGE模式或更换浏览器')
        }
        if (ieVer > -1) {
            window.navigator.msSaveBlob(blob, fileName)
        } else {
            const url = window.URL.createObjectURL(blob)
            file.href = url
        }
    },
    /**
     * @description 文件下载,非流文件(如视频音频等)
     * @param {String, Array} id 文件id,如果是批量下载必须传入数组id集合
     * @param {String} fileName 保存时候的文件名
     */
    async downLoad (id, fileName = '') {
        const file = document.createElement('a')
        const body = document.querySelector('body')

        let ret = {}
        if (!Array.isArray(id) || !id.length) {
            Message('下载参数异常')
            return
        }
        // 业务接口
        ret = await fileModel.zipFile(fileName, id)
        if (!ret.size || ret.size < 1024) {
            Message.error('文件异常,该文件无法下载')
        } else {
            fileName = fileName || ret.fileName // 实现前端自定义文件名或使用后台返回的文件名,需要在request.js补充
            this._getBlob(ret, fileName, file)
        }

        // IE和火狐必须制定下载的格式,否则下载后丢失文件后缀,目前来看会有类型,因为之前没有content-type?
        file.download = fileName
        file.style.display = 'none'
        body.appendChild(file)
        file.click()
        body.removeChild(file)
        window.URL.revokeObjectURL(file)
    }
}

request.js

service.interceptors.response.use(
    response => {
        const contentType = response.headers['content-type'].toLowerCase()
        // 返回请求体是流文件
        if (contentType.includes('octet-stream') || contentType.includes('vnd.ms-excel') || contentType.includes('zip')) {
            if (response.status === 200) {
                // 将后台返回的晴天球头文件名填充到响应体中
                response.data.fileName = window.decodeURI(response.headers['content-disposition'].split('=')[1])
                return Promise.resolve(response.data)
            } else {
                Message({
                    message: '文件下载失败,请稍后尝试',
                    type: 'error',
                    duration: 2 * 1000
                })
            }
        }
    }
)

vue-echarts

项目中使用了百度可视化插件ECharts,本来想直接使用vue-echarts,但用vue-charts一直偶现实例化时必要数据无数据(options中的数据),导致一直报错,github有同样的issuse但作者并无理会,所以直接放弃,自行封装了一个简易版的v-charts,options为echarts实例所需的对象

<template>
    <div ref="echarts" class="echarts" />
</template>

<script>
/**
 * 简易版 vue-echarts,用vue-charts一直偶现实例化时必要数据无数据(options中的数据),导致一直报错,github有同样的issuse但作者并无理会,所以直接放弃
 * 该组件不支持响应式数据,响应式数据实现后发现性能损耗很大(触发极为频繁),不想跟vue-charts一样提供props来让用户决定,因为大部分使用者将会忽略文档提示
 * 所以直接不支持响应式数据,重新渲染的时机需用户自行调用render方法
 * echart整体包过大,使用按需引入,否则整体echarts就九百多K,优化后可减少五百多K,自行根据需要自行选用按需导入或全量导入
 */
import echarts from 'echarts/lib/echarts'
import 'echarts/lib/chart/bar'
import 'echarts/lib/chart/pie'
import 'echarts/lib/chart/line'
import 'echarts/lib/component/tooltip'
import 'echarts/lib/component/title'
import 'echarts/lib/component/legend'
import 'echarts/lib/component/grid'

import resize from './resize'

export default {
    mixins: [resize],
    props: {
        options: {
            type: Object,
            default: () => ({})
        }
    },
    data () {
        return {
            echarts: null
        }
    },
    mounted () {
        this.initEcharts()
    },
    // 暂时留着,不是很确定vue销毁是否能让ECherts也销毁
    destroyed () {
        this.echart.dispose()
        this.echart = null
    },
    methods: {
        initEcharts () {
            const echartsDom = this.$refs.echarts
            this.echart = echarts.init(echartsDom)
            this.echart.setOption(this.options)
        },
        // 重新绘制图表
        render () {
            this.$nextTick(() => {
                this.echart.setOption(this.options)
            })
        }
    }
}
</script>

<style lang="scss" scoped>
    .echarts {
        width: 100%;
        height: 100%;
    }
</style>

resize.js

import { debounce } from '@/utils'

export default {
    data () {
        return {
            $_sidebarElm: null,
            $_resizeHandler: null
        }
    },
    mounted () {
        this.initListener()
    },
    // 假如页面走缓存,则离开页面销毁
    activated () {
        if (!this.$_resizeHandler) {
            this.initListener()
        }
        this.resize()
    },
    deactivated () {
        this.destroyListener()
    },
    beforeDestroy () {
        this.destroyListener()
    },
    methods: {
        $_sidebarResizeHandler (e) {
            if (e.propertyName === 'width') {
                this.$_resizeHandler()
            }
        },
        initListener () {
            this.$_resizeHandler = debounce(() => {
                this.resize()
            }, 100)
            window.addEventListener('resize', this.$_resizeHandler)

            // 侧边导航条因为不会触发浏览器resize,所以需要进行事件监听
            this.$_sidebarElm = document.getElementsByClassName('sidebar-container')[0]
            this.$_sidebarElm && this.$_sidebarElm.addEventListener('transitionend', this.$_sidebarResizeHandler)
        },
        destroyListener () {
            window.removeEventListener('resize', this.$_resizeHandler)
            this.$_resizeHandler = null

            this.$_sidebarElm && this.$_sidebarElm.removeEventListener('transitionend', this.$_sidebarResizeHandler)
        },
        resize () {
            const { echart } = this
            echart && echart.resize()
        }
    }
}

富文本编辑器Tinymce

网上评测文章很多,不再赘述。主要强调的是版本,网上有一篇比较通用的文章,但版本是4x的,下载的时候已经是5x,并且tinymce-vue也已经到2x,因此不能跟着4x的方式来做,本人使用的版本如下:
"tinymce": "5.0.3", "@tinymce/tinymce-vue": "2.0.0"
vue-element-admin原作者里面也有封装了一个tinymce插件,但使用的CDN的方式通过script插入。由于为真实项目,使用这种方式不太合适,因此自行封装了一版tinymceEditor组件,图片自动上传(可截图)
完整步骤:

  1. 下载合适的版本(大部分报错都是因为tinymce与tinymce-vue不匹配,如果有问题可固定为上述版本)
  2. public文件夹中新增tinymce文件夹,将node_modules中tinymce里的skins复制进去,可删除多余的文件,仅剩所需的css文件即可
-| public
  -| tinymce
    -| skins
      -| ui
        -| oxide
          -| content.min.css
          -| skin.min.css
  1. 下载中文包(看个人需要),下载链接tinymce语言包
  2. 导入tinymce、tinymce-vue、zh_CN.js中文包与所需的插件,语言包我是放在资源目录中的,具体根据项目规范自行放置导入即可
  3. 如果项目中tinymce是在弹层当中,下拉菜单会因为层级过低导致不可见,需要自行调整z-index,没有什么好办法,本人解决方案是通过!important来调整层级的
.tox-tinymce-aux {
    z-index: 2021 !important;
}

完整代码:

<template>
    <div class="custom-tinymce">
        <tinymceEditor ref="tinymce_editor" v-model="tinyContent" :init="initParams" />
    </div>
</template>

<script>
import fileModel from '@/api/file'
import 'tinymce/tinymce'
import tinymceEditor from '@tinymce/tinymce-vue'
// 语言包需要自行下载,https://liubing.me/goto/https://www.tiny.cloud/get-tiny/language-packages/
import CN from '@/assets/tinymceLangs/zh_CN.js'
import 'tinymce/themes/silver'
import 'tinymce/plugins/lists'// 列表
import 'tinymce/plugins/image'// 插入上传图片
import 'tinymce/plugins/media'// 插入视频
import 'tinymce/plugins/table'// 表格
import 'tinymce/plugins/wordcount'// 字数统计
import 'tinymce/plugins/quickbars' // 快捷工具栏
import 'tinymce/plugins/fullscreen' // 全屏编辑
import 'tinymce/plugins/preview' // 预览
import 'tinymce/plugins/autolink' // 自动识别链接
import 'tinymce/plugins/charmap' // 特殊字符
import 'tinymce/plugins/searchreplace' // 搜索替换
import 'tinymce/plugins/link'
import 'tinymce/icons/default'
// import 'tinymce/skins/ui/oxide/skin.css' // 虽然能正常运行,但插件底层还会去请求静态目录下的css文件,因此不能采用该做法
import './custom.scss' // 调整层级的css

export default {
    components: {
        tinymceEditor
    },
    props: {
        plugins: {
            type: [String, Array],
            default: 'wordcount quickbars fullscreen preview lists autolink link charmap searchreplace table'
        },
        toolbar: {
            type: [String, Array],
            default: 'undo redo | alignleft aligncenter alignright| bullist numlist | charmap bold italic underline strikethrough forecolor backcolor | quickimage link | fullscreen preview table | fontsizeselect | formatselect | fontselect | lineheight'
        },
        value: {
            type: String,
            default: ''
        }
    },
    data () {
        return {
            tinyContent: this.value
        }
    },
    computed: {
        // 不直接用tinymce,防止后续多个实例的场景出现bug
        editor () {
            return this.$refs.tinymce_editor.editor
        },
        initParams () {
            return {
                language_url: CN,
                language: 'zh_CN',
                height: 500,
                plugins: this.plugins, // 菜单栏
                toolbar: this.toolbar, // 工具栏
                branding: false,
                object_resizing: false,
                quickbars_insert_toolbar: '', // 这个不配为空,每次换行都会出现快捷工具栏,影响操作
                elementpath: false,
                // 解决粘贴图片后,不自动上传,而是使用base64编码。
                urlconverter_callback: (url, node) => {
                    if (node === 'img' && url.startsWith('blob:')) {
                        this.editor.uploadImages()
                        console.log(url, node)
                    }
                    return url
                },
                // 按照文档 issue中的说法,但是怎么设置都没法阻止默认图片变成bolb或base64
                // images_upload_url: fileModel.uploadToPublicStatic,
                // automatic_uploads: false,
                // 插件会将bolb的文件替换未上传成功的文件,但是失败的时候会遗留bolb图片文件,导致文章请求体过大,所以需要删除
                images_upload_handler: async (blobInfo, succFun) => {
                    const file = blobInfo.blob()
                    const formData = new FormData()
                    formData.append('file', file)

                    const { flag, data } = await fileModel.uploadToPublicStaticMethod(formData)
                    if (flag) {
                        succFun(data)
                    } else {
                        // 这里如果用Undo返回上一步不太合理,考虑到网络延迟,上传图片后继续打字什么的,可能无法将图片回退,所以还是走遍历删除
                        const imgElms = Array.from(this.editor.dom.doc.getElementsByTagName('img'))
                        imgElms.forEach(item => {
                            if (item.getAttribute('src').startsWith('blob:')) {
                                this.editor.execCommand('delete', false, item)
                            }
                        })
                    }
                },
                // 走import或require,线上依然会去请求静态目录中的css文件,导致有404请求(虽然已经打包成bundle,功能样式都正常,找不到该插件底层代码时哪里写了)
                // 静态文件均放在public中,并且content_css不指定,线上还会再去请求一个content.css,只能写死
                skin_url: '/tinymce/skins/ui/oxide',
                content_css: '/tinymce/skins/ui/oxide/content.min.css',
                menu: {
                    file: { title: 'File', items: ' preview ' },
                    view: { title: 'View', items: 'preview fullscreen' },
                    insert: { title: 'Insert', items: 'image link media template codesample inserttable | charmap emoticons hr | pagebreak nonbreaking anchor toc | insertdatetime' },
                    format: { title: 'Format', items: 'bold italic underline delectline strikethrough superscript subscript codeformat | formats blockformats fontformats fontsizes align lineheight | forecolor backcolor | removeformat' },
                    tools: { title: 'Tools', items: 'spellchecker spellcheckerlanguage | code wordcount' },
                    table: { title: 'Table', items: 'inserttable | cell row column | tableprops deletetable' }
                },
                default_link_target: '_blank',
                font_formats: "微软雅黑='微软雅黑';宋体='宋体';黑体='黑体';仿宋='仿宋';楷体='楷体';隶书='隶书';幼圆='幼圆';Andale Mono=andale mono,times;Arial=arial,helvetica,sans-serif;Arial Black=arial black,avant garde;Book Antiqua=book antiqua,palatino;Comic Sans MS=comic sans ms,sans-serif;Courier New=courier new,courier;Georgia=georgia,palatino;Helvetica=helvetica;Impact=impact,chicago;Symbol=symbol;Tahoma=tahoma,arial,helvetica,sans-serif;Terminal=terminal,monaco;Times New Roman=times new roman,times;Trebuchet MS=trebuchet ms,geneva;Verdana=verdana,geneva;Webdings=webdings;Wingdings=wingdings",
                fontsize_formats: '12px 14px 16px 18px 24px 36px 48px',
                visual: false,
                toolbar_mode: 'sliding'
            }
        }
    },
    watch: {
        tinyContent () {
            this.$emit('input', this.tinyContent)
        }
    },
    mounted () {
        // tinymce.init({})
    }
}
</script>

使用方法

<tinymce v-model="formData.content" />

跨组件,将按钮插入到头部navbar中

由于后期设计导致的交互变更
如上图所示,一开始原型所有按钮都在下方红框当中,根据逻辑展示对应按钮(因此开发的时候都写在业务组件之中)。
后来设计出视觉的时候将所有区块按钮转移到头部navbar当中(出现该问题的原因很多,从流程到相关人员专业度等等,这里就不吐槽了)。此时有一个致命的问题:超级多页面都有该功能,且都有不同的逻辑在其中,如果从框架上进行更改,工作量极大
这里跨越的组件层级已经无法通过常规的传输方式来解决(不仅是跨组件,而且是跨了N多个组件文件)。要解决这个问题的前提是必须保证现有逻辑的运行,原本的v-if或v-show也能正常判断
因此开发了一个指令与一个组件,用于该场景,解决思路是将通过指令,将原有的节点插入到navbar中去。由于navbar在系统中仅有一个,所以写了一个带ID的空白节点作为落脚点,具有该指令的节点初始化则直接不可见。
/**
 * @author yose
 * @description 该指令仅适用于页面没走缓存(非keep-alive),否则需要使用组件sysbtn
 */
const install = function (Vue) {
    Vue.directive('sysbtn', {
        bind (el) {
            el.style.display = 'none'
            appendToSysbtn(el, Vue)
        },
        update (el) {
            appendToSysbtn(el, Vue)
        },
        unbind (el) {
            const sysbtnDom = document.getElementById('sys_btn')
            if (!sysbtnDom) { return } // 路由路径为退出/404等非layout界面,无法获取到该dom节点
            const childrenNodes = Array.from(sysbtnDom.childNodes)
            childrenNodes.forEach(item => el.appendChild(item))
        }
    })
}

function appendToSysbtn (elm, vue) {
    vue.prototype.$nextTick().then(() => {
        const childrenNodes = Array.from(elm.children)
        const sysBtnNode = document.getElementById('sys_btn') // navbar中被插入的节点,上图最后插入的位置
        childrenNodes.forEach(item => sysBtnNode.appendChild(item))
    })
}

export default install

如果页面走缓存(也就是keep-alive),该指令在离开页面后也无法释放按钮(因为不会触发unbind),所以需要一个功能一致的组件来监听页面的进入activated与离开deactivated

<template>
    <div v-if="!isLeavePage" v-sysbtn>
        <slot />
    </div>
</template>

<script>
/**
 * @author yose
 * @description 由于页面走缓存,因此不会执行unbind,指令中无法得知是否离开当前页面,如果是没有走缓存的页面级组件,则直接使用v-sysbtn指令即可
 */
export default {
    name: 'SysBtn',
    data () {
        return {
            isLeavePage: false
        }
    },
    activated () {
        this.isLeavePage = false
    },
    deactivated () {
        this.isLeavePage = true
    }
}
</script>

使用方式示例(主要是想说明原来的代码除了增加v-sysbtn指令,其余都无需更改就能完成新的视觉需求):

<div v-sysbtn>
    <el-button
        v-if="isShowBalance"
        id="btn_add_deduct"
        v-permission="'DrawingOrder-2-orderBaseInformation-addDeductOrAddMoney'"
        class="btn-w120 main-plain-btn"
        plain
        @click="handleAddDeduct"
    >订单加/扣款</el-button>
    <el-button
        v-if="isWaiting"
        id="btn_submit_drawing"
        v-permission="'DrawingOrder-2-quotationProcurementPlanDetailList-submitCompleted'"
        :loading="submitLoading"
        class="btn-w84"
        type="primary"
        @click="handleSubmit"
    >提交完成</el-button>
</div>

组件上

<template slot="buttons">
    <el-button type="primary" class="btn-w120 check-btn" @click="getUsers">查询</el-button>
    <sys-btn>
        <el-button type="primary" class="btn-w120" @click="handleUser({})">新增</el-button>
    </sys-btn>
</template>

主要为提供一个思路,不要仅限于组件间传值,遇到部分场景不剑走偏锋,会导致需求难度极度复杂,并且耗费大量的无意义时间

相关文章

  • 个人项目记录

    因公司业务需要,去往远方开发项目,大半年来除了加班还是加班,开发了三个后台系统,使用的vue-element-ad...

  • js导出json文件

    参照个人项目: Tool_and_dome/spanish/index.html (仅作个人记录)

  • js实现截图功能(可实现带滚动条的长截图)

    参照个人项目: Tool_and_dome/spanish/index.html (仅作为个人记录)支持长截图,滚...

  • 记录历史的重要性

    记录从古代到现代的大历史固然重要,但是记录个人历史也很重要。什么叫个人历史呢?个人历史就是自己做的项目、干的事,或...

  • iOS 运行报错笔记

    纯个人记录运行项目报错笔记记录 一、问题1 解决办法:问题原因:导入库引入问题解决的办法就是在项目的target里...

  • 高效总结

    新项目 项目相关的readme-项目资料中保存把项目的细节记录下来,方便其他同事参考获取信息 项目总结-个人笔记项...

  • github actions 部署个人Vue项目记录

    首先说明gitpages放的静态页面,路由刷新会报404项目准备工作:在vue.config.js里边的publi...

  • github构建个人网站

    github构建个人网站 个人学习记录 1.github帐号 github.com注册/登录 2.github项目...

  • 项目记录

  • 项目记录

    ``` // 子 与 父联动 // let temp1 = scopeParent.children.eve...

网友评论

      本文标题:个人项目记录

      本文链接:https://www.haomeiwen.com/subject/jygxaktx.html