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'}
- 一定要冲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()
})
前端用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、水波纹效果
2、带有圆角的边框设置渐变色
- 问题:带有圆角的边框设置渐变色圆角会失效(border-image 和 border-radius 是冲突的)
- 解决:利用嵌套盒子,外出使用padding + 背景渐变 + radius ,内层设置一个跟外层一样的背景为白色即可盒子即可。
参考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;
}
3、css3绘制不规则图形
更改滚动条样式
@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属性可以动态的为声明的类的最后一个元素的后面添加唯一标识,并设置为属性选则器
- 造成的问题:使其当前声明的类中的根标签带着一个属性选择器,造成无法将后代元素写正常选择器无法选中
- 解决:使用样式穿透【样式穿透写在哪,就会加上 data-v-xxxx】
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; // 内线粗细
}
}
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'}
}
)
- 传递数组显示异常
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]}`,
}
)
- 传递数组格式JSON串【数组是字符串格式需后端解析】
this.$axios(
{
method:'GET',
url:`/api/value`,
params:{list:JSON.stringify([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>
13、JSON.paras()报错
在element-UI校验中报错会抛出到message上
14、webStock 地址为undefined 问题
前端没有配置webStock 地址
element-plus 自定义样式
npm i sass-loader@9 sass@1.5
"sass-loader": "^9.0.0"
"sass": "^1.53.0"
- 创建文件夹
- 自定义样式【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自定义字体
- 下载字体到项目中
- 利用scss方法自定义字体
@font-face {
font-family: "Alibaba Medium";
src: url("~@/assets/font/medium.ttf");
}
- main中引入自定义字体
Vue3 进行i18n
- 安装插件
npm i vue-i18n
- 进行语言配置
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')},