日常记录

378 阅读6分钟

Vue

1. 自定义组件 .sync 和 v-model 实现父子组件数据同步的区别

.sync

  • 子组件使用 this.$emit('updata:更新属性名',属性值)且子组件接收对应属性的props属性
  • 父组件使用 :更新的属性名.sync="接收的属性值"
// 子组件
<template>
  <div style="border:1px solid red">
    <h4>子组件的值{{userId}}</h4>
    <el-button @click="update">点击更新子组件值</el-button>
  </div>
</template>

<script>
export default {
  props: ['userId'],
  data() {
    return {
    };
  },
  methods: {
    update() {
      this.$emit("update:userId", 111111);
    },
  },
};
</script>

// 父组件
<template>
  <div id="app">
    <h1>父组件值{{userId}}</h1>
    <hello-world :userId.sync="userId"></hello-world>
  </div>
</template>

<script>
import HelloWorld from "./components/HelloWorld";
export default {
  name: "App",
  components: {
    HelloWorld,
  },
  data() {
    return {
      userId:1000
    };
  },
};
</script>

前端下载

  • 后端接口返回的是一个文件下载的url地址
// 无需改文件名
window.open(url)
// 更改文件名
    let link = document.createElement('a')
    link.style.display = 'none'
    link.href = url
    let tempFileName = '容器信息模板'
    link.setAttribute('download', tempFileName + ".xlsx");
    document.body.appendChild(link)
    link.click()
  • 后端返回是二进制流文件

{responseType: 'blob'}

Blob文件type类型

参考博客

  • 一定要冲response中取到Blob对象,否则下载完为损坏文件或乱码!!!!!
  • blob是特殊的二进制,真正需要转成什么格式取决于 new Bolb 的type类型
  • 生成的文件路径,只能使用创建a链接形式的方式下载
// 一定要冲response中取到Blob对象,否则下载完为损坏文件或乱码!!!!!
// {responseType: 'blob'} 必须配置
this.axios.post(
    '/download',{templateName:"xxx"},
    {responseType: 'blob'}
   ).then(res => {
       const url = window.URL.createObjectURL(new Blob([res.data],{ type: '.csv,  application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel' }))
       let link = document.createElement('a')
       link.style.display = 'none'
       link.href = url
       let tempFileName = '模板'
       link.setAttribute('download', tempFileName + ".xlsx");
       document.body.appendChild(link)
       link.click()
 })

image.png

前端用blob实现上传

// 获取文件类型
        getFileType(fileName = '') {
            if (!fileName) return ''
            const index = fileName.lastIndexOf('.')
            let fileType = ''
            if (index) {
                fileType = fileName.substring(index + 1)
            }
            return fileType
        },
        // 附件上次前判断
        beforeUpload(file) {
            const { name, size } = file
            const typeList = ['pdf', 'xlsx', 'doc', 'docx']
            const fileType = this.getFileType(name)
            // 判断文件类型
            if (!typeList.includes(fileType)) {
                this.$message.error('请上传Excel/PDF/Word格式文件')
                return false
            }
            // 判断文件大小
            const maxSize = 100 * 1024 * 1024
            if (size > maxSize) {
                this.$message.error('文件大小不能超过100M')
                return
            }
            const attachFile = new Blob([file], { type: file.type })
            const title = this.formData.title
            this.uploadFileInfo = { attachFile, title }
        },
        // 更新附件
        uploadAttachFile() {
            const formData = new FormData()
            const keys = Object.keys(this.uploadFileInfo)
            keys.forEach(key => {
                formData.append(key, this.uploadFileInfo[key])
            })
            this.loading = true
            this.axios
                .post('xxxx', formData)
                .then(str => {
                    const { data } = str
                    if (data.result) {
                        this.$message.success('文件更新成功')
                        this.fileName = data.message
                    }
                })
                .catch(err => {
                    this.$message.error(err)
                })
                .finally(() => {
                    this.loading = false
                    this.$refs.upload && this.$refs.upload.clearFiles()
                })
        },

动态隐藏超出文本和气泡提示

<template>
  <div ref="textOverflow" class="text-overflow" :style="boxStyle">
    <el-tooltip v-bind="tooltipAttrs" :disabled="!(type === 'tooltip' || type === 'tooltipExpandable') || expanded || !isCutOut" :content="text">
      <span ref="overEllipsis" :title="!(type === 'tooltip' || type === 'tooltipExpandable') && text">{{ realText }}</span>
    </el-tooltip>
    <span ref="slotRef" class="slot-box">
      <span v-if="showSlotNode && (type === 'expandable' || type === 'tooltipExpandable')" @click="toggle">
        <span v-if="!expanded">{{ unfoldText }}</span>
        <span v-else>{{ foldText }}</span>
      </span>
    </span>
  </div>
</template>

<script>
/**
 使用示例:https://blog.csdn.net/qq_41887214/article/details/116663975
 <Paragraph
    :text="text"
 />
 */
export default {
  props: {
    // 显示的文本
    text: {
      type: String,
      default: ''
    },
    // 最多展示的行数
    maxLines: {
      type: Number,
      default: 3
    },
    // 组件宽
    width: {
      type: Number,
      default: 0
    },
    // 展开
    unfoldText: {
      type: String,
      default: '展开'
    },
    // 收起
    foldText: {
      type: String,
      default: '收起'
    },
    // 是否使用Tooltip
    tooltip: {
      type: Boolean,
      default: false
    },
    // 组件类型,expandable、tooltip、tooltipExpandable
    type: {
      type: String,
      default: 'expandable'
    },

    // el-tooltip Attributes
    tooltipAttrs: {
      type: Object,
      default: () => {
        return {
          effect: 'dark',
          placement: 'top'
        }
      }
    }
  },
  data() {
    return {
      offset: this.text.length, // 原始文本length
      expanded: false, // 是否已展开
      slotBoxWidth: 0, // 展开收起按钮宽度
      textBoxWidth: this.width, // 展示的文本宽度
      showSlotNode: false // 是否展示slot节点
    }
  },
  computed: {
    // 设置展示文本宽度
    boxStyle() {
      if (this.width) {
        return {
          width: this.width + 'px'
        }
      } else {
        return { width: 'auto' }
      }
    },

    // 是否被截取
    isCutOut() {
      const isCutOut = this.offset !== this.text.length
      return isCutOut && !this.expanded
    },

    // 获取展示文本
    realText() {
      let realText = this.text
      if (this.isCutOut) {
        realText = this.text.slice(0, this.offset) + '...'
      }
      return realText
    }
  },
  mounted() {
    const { len } = this.getLines()
    if (len > this.maxLines) {
      this.showSlotNode = true
      this.$nextTick(() => {
        this.slotBoxWidth = this.$refs.slotRef.clientWidth
        this.textBoxWidth = this.$refs.textOverflow.clientWidth
        this.calculateOffset(0, this.text.length)
      })
    }
  },
  methods: {
    // 计算offset 核心代码
    calculateOffset(from, to) {
      this.$nextTick(() => {
        if (Math.abs(from - to) <= 1) return
        if (this.isOverflow()) {
          to = this.offset
        } else {
          from = this.offset
        }
        this.offset = Math.floor((from + to) / 2)
        this.calculateOffset(from, to)
      })
    },

    // 内容是否溢出
    isOverflow() {
      const { len, lastWidth } = this.getLines()
      if (len < this.maxLines) {
        return false
      }
      if (this.maxLines) {
        // 超出部分 行数 > 最大行数 或者  已经是最大行数但最后一行宽度 + 后面内容超出正常宽度
        const lastLineOver = !!(
          len === this.maxLines &&
          lastWidth + this.slotBoxWidth > this.textBoxWidth
        )
        if (len > this.maxLines || lastLineOver) {
          return true
        }
      }
      return false
    },

    // 获取元素占据页面的所有矩形区域的行数和最后一行宽度
    getLines() {
      // getClientRects():是获取元素占据页面的所有矩形区域:
      const clientRects = this.$refs.overEllipsis.getClientRects()
      return {
        len: clientRects.length,
        lastWidth: clientRects[clientRects.length - 1].width
      }
    },

    // 切换展开收起
    toggle() {
      this.expanded = !this.expanded
    }
  }
}
</script>

<style lang="less" scoped>

</style>

实现二

<template>
  <div class="tooltip-wrap">
    <el-tooltip
      ref="tlp"
      :content="text"
      effect="dark"
      :disabled="!tooltipFlag"
      :placement="placement"
      class="tooltip">
      <span :class="className" @mouseenter="visibilityChange($event)">{{text}}</span>
    </el-tooltip>
  </div>
</template>
 
<script>
export default {
  name: 'ellipsisTooltip',
  props: {
    text: { // 文本内容
      type: String,
      default: () => ''
    },
    className: {
      type: String,
      default: () => 'text'
    },
    placement: {
      type: String,
      default: () => 'top-start'
    }
  },
  data() {
    return {
      disabledTip: false,
      tooltipFlag: false
    }
  },
  mounted() {
  },
  methods: {
    visibilityChange(e) {
      const ev = e.target
      const evWidth = ev.offsetWidth
      const contentWidth = this.$refs.tlp.$el.parentNode.clientWidth
      this.tooltipFlag = contentWidth < evWidth
    }
  }
}
</script>

封装input+button组件

    <el-input
        v-bind="$attrs"
        v-on="$listeners"
        :class="[{'is-focus':isFocus},{'is-error':isError}]"
        @focus="focus"
        @blur="blur"
    >
        <div slot="append">
            <el-button :disabled="item.disabled" @click="click">{{
                item.__extend__.append.text || '选 择'
            }}</el-button>
        </div>
    </el-input>
</template>

<script>
import $ from 'jquery'
export default {
    name: '',
    components: {},
    props: {
        item: {
            type: Object,
            default: () => ({})
        }
    },
    data() {
        return {
            isFocus: false,
            isError:false,
        }
    },
    watch: {},
    computed: {},
    created() {},
    mounted() {
        this.$nextTick(() => {
            this.initWatch()
        })
    },
    destroyed(){
      this.nodeObserver.disconnect()
    },
    methods: {
        // 通过监视form-item节点类名变化动态添加错误边框
        initWatch() {
            const node = $(this.$el).parents('.el-form-item')[0]
            this.nodeObserver = new MutationObserver((mutations) => {
                mutations.forEach(mutation => {
                  if($(mutation.target).hasClass('is-error')) {
                    this.isError = true
                  }else {
                    this.isError = false
                  }
                });
            });
            this.nodeObserver.observe(node, {
                  attributeFilter: ['class', 'style'], // 标签所带的属性都可增加,包含自定义属性
            })
            
        },
        focus() {
            this.isFocus = true
        },
        blur() {
            this.isFocus = false
        },
        click() {
            this.item.__extend__.append.onClick && this.item.__extend__.append.onClick()
        }
    }
}
</script>

<style lang="scss" scoped>
$--border-radius-small: 2px;
.el-input {
    width: calc(100% - 2px);
    border: 1px solid $--color-line;
    border-radius: $--border-radius-small;
    &::v-deep > input {
        border: none;
    }
    &::v-deep .el-input-group__append {
        position: relative;
        background: white;
        border: none;
        overflow: hidden !important;
        &::before {
            content: '';
            position: absolute;
            left: 0px;
            top: 50%;
            width: 1px;
            height: 22px;
            transform: translateY(-50%);
            background: $--color-line;
        }
        button {
            border: none;
            margin-left: -27px;
            margin-right: -27px;
        }
        .is-disabled {
            background-color: $input-disabled-color-fill;
        }
    }
    &:hover {
        border-color: $base-color;
    }
    &:has(.is-disabled):hover {
        border-color: $--color-line;
    }
}
.is-focus {
    border-color: $base-color;
}
.is-error {
    border-color: #fc292b !important;
}
</style>

webpack【vue-cli > 3 】

配置引入路径别名

const {defineConfig} = require('@vue/cli-service')
const path = require('path')
function resolve(dir) {
	return path.join(__dirname, '.', dir)
}
module.exports = defineConfig({
    chainWebpack: config => {
      config.resolve.alias
          .set('@',resolve('src'))
          .set('_UT', resolve('src/utils'))
    
    }

})

将插件设置为全局变量减少组件内import的引入

const {defineConfig} = require('@vue/cli-service')
const webpack = require('webpack');
module.exports = defineConfig({
  chainWebpack: config => {
    config.plugin('provide').use(webpack.ProvidePlugin, [
      {
        _: ['lodash'], // []中为插件名称,key 为全局变量
      }])
  }
})

配置scss全局变量

const path = require("path");
function addStyleResource(rule) {
  rule.use("style-resource")
      .loader("style-resources-loader")
      .options({
          patterns: [path.resolve(__dirname, "./src/theme/var.scss")],
      });
}
module.exports = {
  chainWebpack: (config) => {
    const types = ["vue-modules", "vue", "normal-modules", "normal"];
    types.forEach((type) =>
      addStyleResource(config.module.rule("scss").oneOf(type))
    );
  },
};

css

1、水波纹效果

image.png 参考

2、带有圆角的边框设置渐变色

  • 问题:带有圆角的边框设置渐变色圆角会失效(border-image 和 border-radius 是冲突的)
  • 解决:利用嵌套盒子,外出使用padding + 背景渐变 + radius ,内层设置一个跟外层一样的背景为白色即可盒子即可。 image.png 参考1 参考2
<template>
  <div class="wrap">
    <div class="box"></div>
  </div>
</template>
<style lang="less" scoped>
.wrap {
  width: 100px;
  height: 100px;
  box-sizing: border-box;
  padding: 1px; // 为边框的宽度
  border-radius: 50%;
  background-image: linear-gradient(180deg, rgba(159,187,255,1) 0%,rgba(77,130,255,1) 100%);
}
.box{
  width: 100%;
  height: 100%;
  border-radius: 50%;
  background: #fff;
}

image.png

3、css3绘制不规则图形

网站

image.png

更改滚动条样式

@mixin scrollbar {
    /*定义滚动条高宽及背景高宽分别对应横竖滚动条的尺寸*/
    &::-webkit-scrollbar {
        width: 16px; // 纵向滚动条生效
    }
    /*定义滚动条轨道内阴影+圆角*/
    &::-webkit-scrollbar-track {
        background-color: rgba(250, 250, 250, 1);
        box-shadow: inset 1px 0 0 0 rgba(221, 221, 221, 1), inset -1px 0 0 0 rgba(221, 221, 221, 1);
    }
    /*定义滑块内阴影+圆角*/
    &::-webkit-scrollbar-thumb {
        background: rgba(204, 204, 204, 1);
        border-radius: 1px;
    }
}

更改input/textarea中placeholder样式

 .input-textarea {
     // placeholder字体
     ::-webkit-input-placeholder {
        font-family: AlibabaPuHuiTi_2_55_Regular;
     }
     ::-moz-placeholder{
        font-family: AlibabaPuHuiTi_2_55_Regular;
     }
     ::-ms-input-placeholder{
         font-family: AlibabaPuHuiTi_2_55_Regular;
     }
     // value字体
     &::v-deep .el-textarea__inner{
         font-family: Arial;
     }
}

更改placeholder样式

@mixin placeholder { 
            &::-webkit-input-placeholder { 
                @content; 
            } 
            &::-moz-placeholder {
                @content; 
            } 
            &:-ms-input-placeholder {
                @content; 
            } 
        }
        
 @include placeholder {
        color: #999;
        font-size: 14px;
        font-family: $font-family-PHT55;
  }

scoped 为什么要用穿透

  • scoped解决了单页面应用样式混乱的问题,通过其scoped属性可以动态的为声明的类的最后一个元素的后面添加唯一标识,并设置为属性选则器

image.png

  • 造成的问题:使其当前声明的类中的根标签带着一个属性选择器,造成无法将后代元素写正常选择器无法选中
  • 解决:使用样式穿透【样式穿透写在哪,就会加上 data-v-xxxx】

image.png

image.png

image.png

js

保留两位小数【toFixed(2)】

100.toFixed(2)

数字按照3位进行逗号分隔【toLocaleString()】

10000.toLocaleString()

递归生成树形结构

    const source = [{
      id: 0,
      parent: null,
      text: '1'
    }, {
      id: 1,
      parent: null,
      text: '菜单1'
    }, {
      id: 2,
      parent: null,
      text: '菜单2'
    }, {
      id: 3,
      parent: 1,
      text: '菜单1-1'
    }, {
      id: 4,
      parent: 2,
      text: '菜单2-1',
    }, {
      id: 5,
      parent: 4,
      text: '菜单3-1',
    }, {
      id: 6,
      parent: 5,
      text: '菜单3-1-1',
    }]
// 方法1
    const getThree = function (source, id, list){
      for (let i of source) {
        if (i.parent === id) {
          list.push(i)
        }
      }
      for (let i of list) {
        i.children = []
        arguments.callee(source, i.id, i.children)
      }
      return list
    }
    getThree(source, null,[])
 // 方法2 
    const getThree2 = function (source, id) {

      // 分组
      const chrldrenList = source.filter(i => i.parent === id)
      // 生成
      return chrldrenList.map(item => {
        return {
          ...item,
          children:arguments.callee(source,item.id)
        }

      })
    }
    getThree2(source, null)

element-ui 下拉搜索或远程搜索动态展示放大镜

          <el-select
                        v-if="model !== 'check'"
                        v-model="formData.emailTemplateTitle"
                        class="selectSearch-1"
                        placeholder="请选择"
                        style="width: 302px"
                        filterable
                        :filter-method="remoteMethod"
                        @change="changeTemplate"
                        @visible-change="(flag) => {handlevisiblechange(flag,1)}" // 动态获取唯一的class
                    >
                        <el-option
                            v-for="(item, index) in templateList"
                            :value="item.value"
                            :key="item.value"
                            :label="item.label"
                        >
                        </el-option>
          </el-select>
handlevisiblechange(flag,id) {
            if (flag) {
                this.$nextTick(() => {
                    const selectSearchIcon = document.querySelector(
                        `.selectSearch-${id} .el-icon-arrow-up`
                    )
                    selectSearchIcon && selectSearchIcon.classList.replace('el-icon-arrow-up', 'el-icon-search') 
                })
            } else {
                this.$nextTick(() => {
                    const selectSearchIcon = document.querySelector(
                        `.selectSearch-${id} .el-icon-arrow-up`
                    )
                    selectSearchIcon &&
                        selectSearchIcon.classList.replace('el-icon-search', 'el-icon-arrow-up')
                })
            }
        },

vxe-table

修改vxe-table 表格边框的颜色

   .custom-table {

  // 表格外边框颜色
  &::v-deep .vxe-table--border-line {
    border-color: yellow;
  }

  // 头部底部边框
  &::v-deep .vxe-table--header-border-line {
    border-color: blueviolet;
  }

  // 表格内填充的线条
  &::v-deep .vxe-body--column,
  &::v-deep .vxe-footer--column,
  &::v-deep .vxe-header--column {
    background-image: linear-gradient(#d1141e, #d1141e), linear-gradient(#d1141e, #d1141e);
    background-size: 1.5px 100%, 100% 1.5px; // 内线粗细
  }
}

image.png

bug记录

1、element-ui 校验规则返回的promise为pidding状态的undefined

  • 原因:使用了自定义校验,但在某些判断中没有调用cb()回调

2、await判定是否完成取决于当前函数或表达式的返回值

3、el-dialog 中数量过大可以使用异步操作【定时器、请求】让其先弹窗后渲染内容。

4、表格中根据某些下拉框的值进行当前行的某一个值校验时,要使用自定义校验。rule中的校验时针对某一列进行校验的。

5、vue中动态添加类型,实则是先写好类样式,再动态控制是否加上改类样式。

6、使用v-model进行数据动态收集的是数据是响应式,通过非包装的数组方式和对象的添加删除都是非响应式的。[www.jb51.net/article/241…]

7、使用el-form + el-input,在el-input上绑定@keyup.enter.native事件引起的页面刷新问题

参考博客

  • 原因:form表单默认使用键盘提交会刷新页面,所有要阻止表单默认行为
<el-form @submit.native.prevent> </el-form>

8、弹窗内容,超出弹窗高度一点点造成滚动条,通过修改role="dialog"所在标签的 max-height 属性即可

9、watch无法监视到非响应式数据(props)

export default {
  name: "Home",
  data() {
    return {
      setObj:{
        a: {
        }
      }
    };
  },
  methods: {
    testSet(){
        //watch监测不到
        tis.setObj.a.age  = 6666
        //watch可以监测到
        this.$set(this.setObj.a,'age',11111)
    }
  },
  watch:{
    setObj:{
      handler(){
          console.log(this.setObj)
      },
      deep: true
    }
  },
};

10、switch 中case使用逻辑运算符产生的bug

        let a = 0;
        switch (a) {
            case 1 || 0:
                console.log("aaa");
                break;
            default:
                console.log("else");
        }
        // 输出的为default的值

在js中,case后面的语句是一个整体的表达式,不能拆分开,就是说这里的case 1 || 0其实相当于case (1 || 0)。(1 || 0)的值为1,当a等于1时,两者才相等。

        switch (a) {
            case 1:
            case 0:
                console.log("aaa");
                break;
            default:
                console.log("else");
        }

11、get请求发送数组或对象

  • 非数组一切正常
this.$axios(
        {
          method:'GET',
          url:`/api/value`,
          params:{age:10,name:'xxx'}
        }
)

http://localhost:8080/api/value?age=10&name=xxx

  • 传递数组显示异常
this.$axios(
        {
          method:'GET',
          url:`/api/value`,
          params:{list:[1,2,4,5]}
        }
      )

http://localhost:8080/api/value?list[]=1&list[]=2&list[]=4&list[]=5

  • 解决传递数组异常【将数组转成以逗号连接】
      this.$axios(
        {
          method:'GET',
          url:`/api/value`,
          params:{list:[1,2,4,5].toString()}
        }
      )
      this.$axios(
        {
          method:'GET',
          url:`/api/value/?list=${[1,2,4,5]}`,
        }
      )

http://localhost:8080/api/value?list=1,2,4,5

  • 传递数组格式JSON串【数组是字符串格式需后端解析】
      this.$axios(
        {
          method:'GET',
          url:`/api/value`,
          params:{list:JSON.stringify([1,2,4,5])}
        }
      )

http://localhost:8080/api/value?list=[1,2,4,5]

12、el-form 中使用自定义组件实现校验成功清空

                <el-form-item
                    class="form-address"
                    label-width="74px"
                    label="收件人:"
                    prop="addresses"
                    ref="inputValueRef"
                >
                    <InputTags
                        v-model="formData.addresses"
                        @change="$refs.inputValueRef.$emit('el.form.change', $event)"
                    ></InputTags>
                </el-form-item>

参考博客1 参考博客2

13、JSON.paras()报错

image.png

在element-UI校验中报错会抛出到message上

14、webStock 地址为undefined 问题

image.png

前端没有配置webStock 地址

element-plus 自定义样式

npm i sass-loader@9 sass@1.5

"sass-loader": "^9.0.0"

"sass": "^1.53.0"

  • 创建文件夹

image.png

  • 自定义样式【index.scss】
@forward "element-plus/theme-chalk/src/common/var.scss" with (
  $border-radius: ("base": 0px, "small": 0px, "round": 20px, "circle": 100%),
  $colors: (
    'primary': (
      'base': green,
    ),
  ),
);
@use "element-plus/theme-chalk/src/index.scss" as *;
  • main.js引入自定义样式无需引入element-plus默认样式

import './theme/styles/element/index.scss'

Vue中利用scss自定义字体

  • 下载字体到项目中

image.png

  • 利用scss方法自定义字体
@font-face {
  font-family: "Alibaba Medium";
  src: url("~@/assets/font/medium.ttf");
}
  • main中引入自定义字体

image.png

Vue3 进行i18n

  • 安装插件

npm i vue-i18n

  • 进行语言配置

image.png

index.ts

import { createI18n } from 'vue-i18n'
import zh from './language-CN.ts'
import en from './language-EN.ts'
const messages = {
  en,
  zh,
}
const language = (navigator.language || 'en').toLocaleLowerCase() // 这是获取浏览器的语言
const i18n = createI18n({
  locale: localStorage.getItem('lang') || language.split('-')[0] || 'en', // 首先从缓存里拿,没有的话就用浏览器语言,
  fallbackLocale: 'en', // 设置备用语言
  messages, 
})

export default i18n

language-CN

export default {
  login: {
      login: '登录',
      userName: '用户名',
      password: '密码'
  }
}
export default {
  login: {
      login: 'loginsfsdfsdfsdfsdfsfsdfsdfdsfsd',
      userName: 'userName',
      password: 'password'
  }
}

引入并使用i18n

import i18n from './assets/language/index.ts'
use(i18n)
  • 动态切换语言
import { useI18n } from "vue-i18n";
export default {
  setup(props, { attrs, slots, emit }) {
    const { locale } = useI18n(); // 必须写在setup方法中
    const change = (val = "zh") => {
      locale.value = val
      localStorage.setItem('lang',val)
      emit('change',val)
    };
    return {
      change,
    };
  },
};
  • 关联地方组件库
<template>
  <el-config-provider :locale="localeMap[currentLag]">
    <HelloWorld @change="change" msg="Welcome to Your Vue.js App" />
  </el-config-provider>
</template>

import { ElConfigProvider } from "element-plus";
import zhCn from "element-plus/lib/locale/lang/zh-cn";
import en from "element-plus/lib/locale/lang/en";
import {ref,reactive,toRefs} from 'vue'
export default {
  name: "App",
  components: {
    HelloWorld,
    ElConfigProvider,
  },
  setup() {
    // 在setUp方法中操作
    const change = (val) => {
       data['currentLag'] = val
    }
    const data = reactive(
      {
        localeMap:{
          'zh':zhCn,
          'en':en,
        },
        currentLag:localStorage.getItem('lang')
      }
    )
    return {
      ...toRefs(data),
      change,
    };
  },
};
  • 在JS文件中使用
// 将Vue实例绑定在window上 【主意W是小写】
window.__VM__ = new Vue({
    router,
    store,
    i18n,
    render: (h) => h(App),
}).$mount('#app')
 {value: 404,label:window.__VM__.$i18n.t('inventory.move.cancel')},