原理:生成的vNode,通过 $mount 方法,会直接替换挂载的 DOM 元素
缺点每个都一样
//在index.html文件中
<div id="app">
<div>
写入骨架屏
<img src="./img/icons/android-chrome-512x512.png">
</div>
</div>
vue-skeleton-webpack-plugin 这个插件跟自己手写差不多,虽然可以不同路由匹配不同skeleton,但是只能是初始页面刷新生效。
//mian.ts
import "@/styles/normalize.css";
normalize.css
/*! normalize.css v5.0.0 | MIT License | github.com/necolas/normalize.css */
/**
* 1. Change the default font family in all browsers (opinionated).
* 2. Correct the line height in all browsers.
* 3. Prevent adjustments of font size after orientation changes in
* IE on Windows Phone and in iOS.
*/
/* Document
========================================================================== */
html {
font-family: sans-serif;
/* 1 */
line-height: 1.15;
/* 2 */
-ms-text-size-adjust: 100%;
/* 3 */
-webkit-text-size-adjust: 100%;
/* 3 */
}
a{
display: block;
}
/* Sections
========================================================================== */
/**
* Remove the margin in all browsers (opinionated).
*/
body {
margin: 0;
}
/**
* Add the correct display in IE 9-.
*/
article,
aside,
footer,
header,
nav,
section {
display: block;
}
/**
* Correct the font size and margin on `h1` elements within `section` and
* `article` contexts in Chrome, Firefox, and Safari.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/* Grouping content
========================================================================== */
/**
* Add the correct display in IE 9-.
* 1. Add the correct display in IE.
*/
figcaption,
figure,
main {
/* 1 */
display: block;
}
/**
* Add the correct margin in IE 8.
*/
figure {
margin: 1em 40px;
}
/**
* 1. Add the correct box sizing in Firefox.
* 2. Show the overflow in Edge and IE.
*/
hr {
box-sizing: content-box;
/* 1 */
height: 0;
/* 1 */
overflow: visible;
/* 2 */
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
pre {
font-family: monospace, monospace;
/* 1 */
font-size: 1em;
/* 2 */
}
/* Text-level semantics
========================================================================== */
/**
* 1. Remove the gray background on active links in IE 10.
* 2. Remove gaps in links underline in iOS 8+ and Safari 8+.
*/
a {
background-color: transparent;
/* 1 */
-webkit-text-decoration-skip: objects;
/* 2 */
}
/**
* Remove the outline on focused links when they are also active or hovered
* in all browsers (opinionated).
*/
a:active,
a:hover {
outline-width: 0;
}
/**
* 1. Remove the bottom border in Firefox 39-.
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
*/
abbr[title] {
border-bottom: none;
/* 1 */
text-decoration: underline;
/* 2 */
text-decoration: underline dotted;
/* 2 */
}
/**
* Prevent the duplicate application of `bolder` by the next rule in Safari 6.
*/
b,
strong {
font-weight: inherit;
}
/**
* Add the correct font weight in Chrome, Edge, and Safari.
*/
b,
strong {
font-weight: bolder;
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp {
font-family: monospace, monospace;
/* 1 */
font-size: 1em;
/* 2 */
}
/**
* Add the correct font style in Android 4.3-.
*/
dfn {
font-style: italic;
}
/**
* Add the correct background and color in IE 9-.
*/
mark {
background-color: #ff0;
color: #000;
}
/**
* Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/* Embedded content
========================================================================== */
/**
* Add the correct display in IE 9-.
*/
audio,
video {
display: inline-block;
}
/**
* Add the correct display in iOS 4-7.
*/
audio:not([controls]) {
display: none;
height: 0;
}
/**
* Remove the border on images inside links in IE 10-.
*/
img {
border-style: none;
}
/**
* Hide the overflow in IE.
*/
svg:not(:root) {
overflow: hidden;
}
/* Forms
========================================================================== */
/**
* 1. Change the font styles in all browsers (opinionated).
* 2. Remove the margin in Firefox and Safari.
*/
button,
input,
optgroup,
select,
textarea {
font-family: sans-serif;
/* 1 */
font-size: 100%;
/* 1 */
line-height: 1.15;
/* 1 */
margin: 0;
/* 2 */
}
/**
* Show the overflow in IE.
* 1. Show the overflow in Edge.
*/
button,
input {
/* 1 */
overflow: visible;
}
/**
* Remove the inheritance of text transform in Edge, Firefox, and IE.
* 1. Remove the inheritance of text transform in Firefox.
*/
button,
select {
/* 1 */
text-transform: none;
}
/**
* 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`
* controls in Android 4.
* 2. Correct the inability to style clickable types in iOS and Safari.
*/
button,
html [type="button"],
/* 1 */
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
/* 2 */
}
/**
* Remove the inner border and padding in Firefox.
*/
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
}
/**
* Restore the focus styles unset by the previous rule.
*/
button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
}
/**
* Change the border, margin, and padding in all browsers (opinionated).
*/
fieldset {
border: 1px solid #c0c0c0;
margin: 0 2px;
padding: 0.35em 0.625em 0.75em;
}
/**
* 1. Correct the text wrapping in Edge and IE.
* 2. Correct the color inheritance from `fieldset` elements in IE.
* 3. Remove the padding so developers are not caught out when they zero out
* `fieldset` elements in all browsers.
*/
legend {
box-sizing: border-box;
/* 1 */
color: inherit;
/* 2 */
display: table;
/* 1 */
max-width: 100%;
/* 1 */
padding: 0;
/* 3 */
white-space: normal;
/* 1 */
}
/**
* 1. Add the correct display in IE 9-.
* 2. Add the correct vertical alignment in Chrome, Firefox, and Opera.
*/
progress {
display: inline-block;
/* 1 */
vertical-align: baseline;
/* 2 */
}
/**
* Remove the default vertical scrollbar in IE.
*/
textarea {
overflow: auto;
}
/**
* 1. Add the correct box sizing in IE 10-.
* 2. Remove the padding in IE 10-.
*/
[type="checkbox"],
[type="radio"] {
box-sizing: border-box;
/* 1 */
padding: 0;
/* 2 */
}
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
*/
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
*/
[type="search"] {
-webkit-appearance: textfield;
/* 1 */
outline-offset: -2px;
/* 2 */
}
/**
* Remove the inner padding and cancel buttons in Chrome and Safari on macOS.
*/
[type="search"]::-webkit-search-cancel-button,
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button;
/* 1 */
font: inherit;
/* 2 */
}
/* Interactive
========================================================================== */
/*
* Add the correct display in IE 9-.
* 1. Add the correct display in Edge, IE, and Firefox.
*/
details,
/* 1 */
menu {
display: block;
}
/*
* Add the correct display in all browsers.
*/
summary {
display: list-item;
}
/* Scripting
========================================================================== */
/**
* Add the correct display in IE 9-.
*/
canvas {
display: inline-block;
}
/**
* Add the correct display in IE.
*/
template {
display: none;
}
/* Hidden
========================================================================== */
/**
* Add the correct display in IE 10-.
*/
[hidden] {
display: none;
}
a {
text-decoration: none;
color: #333;
}
html {
color: #000;
background: #fff;
overflow-y: scroll;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%
}
html * {
outline: 0;
-webkit-text-size-adjust: none;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0)
}
html,
body {
font-family: sans-serif
}
body,
div,
dl,
dt,
dd,
ul,
ol,
li,
h1,
h2,
h3,
h4,
h5,
h6,
pre,
code,
form,
fieldset,
legend,
input,
textarea,
p,
blockquote,
th,
td,
hr,
button,
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
menu,
nav,
section {
margin: 0;
padding: 0
}
input,
select,
textarea {
font-size: 100%
}
table {
border-collapse: collapse;
border-spacing: 0
}
fieldset,
img {
border: 0
}
abbr,
acronym {
border: 0;
font-variant: normal
}
del {
text-decoration: line-through
}
address,
caption,
cite,
code,
dfn,
em,
th,
var {
font-style: normal;
font-weight: 500
}
ol,
ul {
list-style: none
}
caption,
th {
text-align: left
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: 100%;
font-weight: 500
}
input{
display:block;
border: none;
outline: none;
}
原文连接1
原文连接2
1.登录,从后台获取到token(鉴权令牌),refresh_token(刷新token的令牌),expire_time(token的时效)
2.用户操作中,向后台发送请求,每次请求时,将当前请求时间与expire_time对比,使用refresh_token去重新获取token。(refresh_token也是有时效的,但是比token长)
3,使用refresh_token去重新获取token ,并覆盖上一次存储信息
由后端设置redis
前端设置axios
首先token过期就会重新获取token,如果并发多条数据那就先搞一个闸门,一旦有一个去刷新接口就 把闸门关闭。什么时候刷新完成 什么时候关闭,并把后面的以函数的形式都存到数组里面,等token 刷新完成之后 还得重新执行下 这些请求
let isFreshToken = true // 默认是打开
let casheRequests = []; // 请求队列
// 响应拦截
request.interceptors.response.use(res => {
const { data, status, config } = res;
if(status == 200) { // 请求状态成功
if(data.Code == 4441) { // token过期
if(isFreshToken) {//token可以刷新 一次并发只能有一个刷新
isFreshToken = false; // 把开关关闭
// 刷新token 是一个 返回promise的请求接口的方法 这个是自定义的 看你们自己的项目来定,要用原生的axios
reFreshToken().then(result => {
isFreshToken = true // 方法重新打开
let token = result.token; // 拿到新的token
// 重新赋值token
localStorage.setItem('token', token);
// 重新执行 之前缓存的方法数组 使用最新的token
casheRequests.forEach(cb => cb(token))
// 重置为空 降缓存的请求方法
casheRequests = []
// 然后重新执行本次的方法
config.headers['token']= token // 使用最新的token
return request(config);
})
//
} else {
// token 正在刷新中 其他的请求先放在队列中
return new Promise(resolve => {
// 将resolve放进队列,用一个函数形式来保存,等token刷新后直接执行
casheRequests.push((token) => {
config.headers['token']= token
resolve(request(config))
})
})
}
}
return data
}
}
用于控制那些页面是需要登录后查看的
给路由添加meta 属性在beforeEach中判断,是否用于meta属性,有就代表需要鉴权,然后对登录状态进行查看,查看是否有token
1.前端讲订单商品传给后端
2.后端调用微信服务器发起统一支付
3.得到微信返回二维码,返给前端
4.手机扫码
5.微信客户端讲扫码链接提交微信支付系统
6.微信支付系统验证链接有效返回支付授权
7.支付成功后,微信支付系统,异步通知商家支付结果
1、携带手机号向后端发送请求,后端会给手机发送验证码
2、携带手机号和验证码发送请求,后端进行判断
腾讯云短信验证,创建正文模板,创建密匙
网页
1.pc请求二维码
2.服务器返回二维码id,过期时间
3.pc获取二维码id生成二维码
4.手机扫码获得二维码id
5.手机将手机token+二维码id发送到服务器
6.服务器校验手机端token,根据手机端token+二维码id生成PCtoken
7.PC通过轮询更新二维码状态,返回PCtoken
1.先进行手机号的正则校验,
2.调用接口,后端返回一个码,和用户输入的码进行对比
qrcode插件
npm install --save qrcode
import QRCode from 'qrcode'
// With promises
QRCode.toDataURL('I am a pony!')//生成的图片路径
.then(url => {
console.log(url)
})
.catch(err => {
console.error(err)
})
// 或async/await
const generateQR = async text => {
try {
console.log(await QRCode.toDataURL(text))
} catch (err) {
console.error(err)
}
}
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
indent_style = space
indent_size = 4
end_of_line = crlf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
function diffDay(lastDate,earlyDate){
return (Date.parse(lastDate) - Date.parse(earlyDate))/1000/60/60/24;
}
diffDay('2021/04/22','2021/04/20'); //return 2
//添加key
<template v-if="show">
<input>
<button key="1">点我</button>
</template>
<template v-else>
<input>
<button key="2">点我</button>
</template>
修改headers[‘Content-Type’] = ‘application/json’
或者headers[‘Content-Type’] = ‘application/x-www-form-urlencoded;charset=UTF-8’
先用axios试一试能不能发,如果不能就改header,如果还不能就qs
1.下载 cnpm install qs
import qs from 'qs'
post传值后台收不到 需要用qs转换一下数据
post('http://localhost:8080/api', qs.stringify(this.addCategoryName))
axios({
method:'post',//post,get
url:'/api/song-search',//地址
data:{keyword: keyword1},//数据
headers: {'X-Requested-With': 'XMLHttpRequest'},//请求头
timeout: 1000,//超过多少秒终止
}).then((res) => {
// 将请求的结果赋值给personData全局变量,用于展示搜索结果
if (res.data.code === '0') {
this.personData = res.data.data || [];
} else {
this.personData = [];
}
})
vue.config.js
当我们在一个页面上,页面上有不同的分类,点击不同的分类需要传不同的参数来请求接口,当接口返回数据后,需要将页面的数据重新渲染,而不是之前的数据。
1.created改为activated
activated() {
this.getList();
},
2.watch监听
watch: {
'$route' (to, from) {
// 路由发生变化页面刷新
this.$router.go(0);
}
},
3.使用VUE的v-if控制DOM
4.在router-view上添加 :key=“$route.fullPath”
- this.$router.push 的query 添加new Date().getTime()
this.$router.push({
path: "/homePage/searchResult",
query: {
keywords: this.input,
type: this.type,
date:new Date().getTime()
}
})
当我们在一个页面上,页面上有不同的分类,点击不同的分类需要传不同的参数来请求接口,当接口返回数据后,需要将页面的数据重新渲染,而不是之前的数据。
比如:
path只是路径部分,fullPath是完整地址
fullPath: “/movie/2?name=zs%20age%3D28”
path: “/movie/2”
query: {name: ‘zs age=28’}
我们每次只改变url后面的id,同时将页面重新渲染
<router-view :key='$route.fullPath'>
或者
this.$router.push(`/login?redirect=${this.$route.fullPath}`);
通过绑定一个fullPath,可以识别当前页面路由的完整地址,当地址发生改变(包括参数改变)则重新渲染页面(例如动态路由参数的变化)
<template>
<my-component v-if="renderComponent"/>
</template>
<script>
export default {
data() {
return {
renderComponent: true,
};
},
methods: {
forceRerender() {
// Remove my-component from the DOM
this.renderComponent = false;
this.$nextTick(() => {
// Add the component back in
this.renderComponent = true;
});
}
}
};
</script>
this.$forceUpdate()
<component-to-re-render :key="componentKey" />
export default {
data() {
return {
componentKey: 0,
};
},
methods: {
forceRerender() {
this.componentKey += 1;
}
}
}
也可以刷新 缺点出现空白页
1.this.$router.go(0)
2.location.reload()
3.if+provide+inject
//优点没有白屏
<!-- App.vue -->
<template>
<router-view v-if="isRouterAlive"></router-view>
<!-- 在router-view使用isRouterAlive或者是下面这种在组件中使用 -->
<!-- <BLank v-if="isRouterAlive"></BLank> -->
</template>
<script>
// 局部组件刷新
const isRouterAlive = ref(true);
const reload = () => {
isRouterAlive.value = false;
nextTick(() => {
isRouterAlive.value = true;
});
};
provide("reload", reload);
</script>
//组件使用test.vue
<template>
<input type="text">
<button @click="ceshi">测试按钮</button>
</template>
<script>
import {inject} from "vue";
const ceshi = inject('reload', Function, true)
</script>
this.$toast("提示内容");
ts+setup
import { Toast } from "vant";
import "vant/es/toast/style";
Toast("结束时间不能比开始时间大");
https://blog.csdn.net/weixin_45768768/article/details/122199323
https://blog.csdn.net/weixin_53072519/article/details/120658248
解决办法:数组里面图片的路径要写成如下:
image:require('../assets/img/login.png')
渲染的时候要写
<img :src="item.image" />
具体事例
<a-menu
v-for="(v, i) in navData"
:key="i"
:default-selected-keys="['/account/home']"
:selected-keys="[v.key]"
mode="inline"
type="inner"
class="accout_menu"
@openChange="onOpenChange"
>
<a-menu-item key="v.key">
<router-link :to="{ name: v.name }">
<img :src="v.src" class="menu_account_image" />
<span class="menu_account_title">{{ v.title }}</span>
</router-link>
</a-menu-item
:src=" "
8080端口下找的是public下的文件,所以找不到
方法一
v-for
<img
style="width:100%;height:100%;"
:src="require('../'+item.imgSrc)"
/>
图片存放在assets/banner/banner.jpg
imgs: [{
id: '01',
imgSrc: 'assets/banner/banner1.jpg',
}, {
id: '02',
imgSrc: 'assets/banner/banner2.png'
}, {
id: '03',
imgSrc: 'assets/banner/banner3.jpg'
}]
方法2
<img
style="width:100%;height:100%;"
:src="item.imgSrc"
/>
import img1 from '../assets/banner/banner1.jpg'
imgs: [{
id: '01',
imgSrc: img1,
},]
import Vue from 'vue';
let vm = new Vue();
vm.$message({
showClose: true,
message: data.msg,
type: 'warning'
});
<body>
<div id="app">
<ul>
<li v-for="d in currentList" :key="d">{{d}}</li>
</ul>
<button :disabled="page===1" @click="page--">上一页</button>//如果第一页再往前就按钮禁用
<button :disabled="page===Math.ceil(mapList.length/pageSize)" @click="page++">下一页</button>//判断最后一页禁用按钮
</div>
</body>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js"></script>
<script>
new Vue({
el: '#app',
data() {
return {
mapList: [1, 2, 3, 4, 5, 6, 7, 8, 9],
page: 1, //页数,最大页数:数组长度/2向上取整
pageSize: 2 //每页2页
}
},
computed: {
currentList() {
return this.mapList.slice((this.page - 1) * this.pageSize, this.page * this.pageSize)//截取对应数据
}
},
})
</script>
Vue不能检测通过数组索引直接修改一个数组项,由于JavaScript的限制,Vue不能检测数组和对象的变化
this.$set(arr,index,newVal)
checkClick (item) {
item.check =! item.check;
this.$forceUpdate()
},
watch: {
'$route': function() {
}
}
原因:Vue在更新DOM时是异步执行。只要侦听到数据变化,Vue将开启一个队列,并缓冲在同一个事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“nextTick”中,Vue 刷新队列并执行实际 (已去重的) 工作。
nextTick:在下一次dom更新循环结束之后,执行回调函数
解决办法:
this.$nextTick(function(){ })
原因:显示的元素不会动
解决办法:
使用v-if先隐藏元素,更新的数组排序处理好了,才显示元素
const arr=reactive([])
arr=resault.data//X这样不会响应式
const arrNew=reactive({arr1:[]})
arrNew.arr1=resault.data//这样可以响应式
<div id="app">
<button @click="a" @dblclick="b">点击</button>
</div>
<script>
let vm = new Vue({
el: "#app",
data: {
name: "清欢"
},
methods: {
a() {
this.flag = true
setTimeout(() => {
if (this.flag) {
console.log(1);
}
}, 300);
},
b() {
this.flag = false
console.log(5);
}
}
})
</script>
<input v-show="!show" :ref="value.id" />
this.$nextTick(() => {
this.$refs[this.value.id].focus();
})
autofocus="true"
<table>
<thead>
<tr>
<th>标题1</th>
<th>标题2</th>
<th>标题3</th>
</tr>
</thead>
<tbody>
<tr v-for="(items,index) in dataList" :key="index"
draggable="true"
@dragstart="handleDragStart($event, items)"
@dragover.prevent="handleDragOver($event, items)"
@dragenter="handleDragEnter($event, items)"
@dragend="handleDragEnd($event, items)"
>
<td>{{items.content}}</td>
</tr>
</tbody>
</table>
<script>
var VM = new Vue({
el:'#app',
data:function(){
return {
dataList:[{
content:'内容'
},{
content:'内容'
},{
content:'内容'
}],
dragging: null
}
},
methods:{
handleDragStart(e,items){
this.dragging = items;//开始拖动时,暂时保存当前拖动的数据。
},
handleDragEnd(e,items){
this.dragging = null;//拖动结束后,清除数据
},
handleDragOver(e) {
e.dataTransfer.dropEffect = 'move';//在dragenter中针对放置目标来设置!
},
handleDragEnter(e,items){
e.dataTransfer.effectAllowed = "move";//为需要移动的元素设置dragstart事件
if(items == this.dragging) return;
var newItems = [...this.dataList];//拷贝一份数据进行交换操作。
var src = newItems.indexOf(this.dragging);//获取数组下标
var dst = newItems.indexOf(items);
newItems.splice(dst, 0, ...newItems.splice(src, 1));//交换位置
this.dataList = newItems;
}
}
})
</script>
官网
1.下载
npm i -S vuedraggable
2.使用
里面必须是循环,v-for element-ui的列表就不可以,需要自己创个表格
<draggable v-model="myArray" >
<div v-for="element in myArray" :key="element.id">{{element.name}}</div>
</draggable>
import draggable from 'vuedraggable'
...
export default {
data() {
return {
myArray:[]
};
},
components: {
draggable,
},
...
3.配合vuex使用
会存在一个问题,如果直接v-model去绑定vuex的数据,会直接修改数据,这里不允许直接修改vuex和props的数据
//方法一:v-model的另一种写法
<draggable :value="$store.state.vuexArr" @input="update($event)"> </draggable>
update($event){
//向后端发送请求,提交修改的数据
//axios.post('updata',{}).then((res)=>{commit修改数据})
this.$store.commit('updateVuexArr',$event)
}
//方法二:使用官方使用的计算属性
<draggable v-model="vuexArr"></draggable>
computed: {
vuexArr: {
get() {
return this.$store.state.vuexArr
},
set(value) {
this.$store.commit('updateVuexArr', value)
}
}
}
因为用来v3的插件 禁用就行或者
在jsconfig.json里添加"jsx":“preserve”
注意版本,太高的版本会报错
npm install swiper@5 vue-awesome-swiper@3 -S
import Swiper from "swiper";
import 'swiper/css/swiper.min.css';
<div class="swiper-container">//最外层包裹的不可以变
<div class="swiper-wrapper">
<div class="swiper-slide">Slide 1</div>
<div class="swiper-slide">Slide 2</div>
<div class="swiper-slide">Slide 3</div>
</div>
<div class="swiper-button-next"></div>
<div class="swiper-button-prev"></div>
</div>
//使用
mounted(){
new Swiper ('.swiper-container', {
loop: true,
navigation: {
nextEl: ".swiper-button-next",
prevEl: ".swiper-button-prev",
},
autoplay: {
delay: 1500,
disableOnInteraction: false
}
})
},
vue swiper 封装slot插槽
英文官网
npm install swiper
import Swiper from 'swiper/js/swiper';//这个要根据自己的情况找路径
import 'swiper/css/swiper.css';
swiper:null, // 存放swiper实例
mounted(){
new Swiper('.swiper',{
loop:true,
direction:"horizontal"
});
},
beforeDestroy(){//卸载,防止内存泄漏
this.swiper.destroy();
}
<div class="swiper">
<div class="swiper-wrapper">
<div class="swiper-slide" v-for="d in swiperList" :key="d.image">
<a :href="d.link">
<img :src="d.image">
</a>
</div>
</div>
</div>
watch: {
cities: function () {
this.$nextTick(() => {
this.scroll.refresh()
})
}
}
1.按需引入
npm i element-ui -S
npm install babel-plugin-component -D
2.babel.config.js
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset',
[
"@babel/preset-env", { modules: false }
]
],
"plugins": [
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}
]
]
}
3.main.js
import { Button } from 'element-ui';
Vue.component(Button.name, Button);
4.使用
<el-button type="primary">按钮</el-button>
main。js
import { Button,Message } from 'element-ui';
Vue.prototype.$message = Message
<el-button type="primary" @click=" $message('这是一条消息提示');">按钮</el-button>
import {Loading} from 'element-ui';
Vue.use(Loading);
//局部使用
loading: true
<div class="box" v-loading="loading">123</div>
prop+对象+校验
<el-form-item prop="name">//prop
<el-input v-model="ruleForm.name" />
</el-form-item>
const ruleForm = reactive({ name: 'Hello',})//名字
实现:下一个表单验证取决于上一个表单输入,提交按钮的防止多次点击
重要点:
this. r e f s . r u l e F o r m . v a l i d a t e F i e l d ( " n a m e " ) ; / / 用自己的校验规则的时候,也制定另外一个校验规则进行校验 t h i s . refs.ruleForm.validateField("name");//用自己的校验规则的时候,也制定另外一个校验规则进行校验 this. refs.ruleForm.validateField("name");//用自己的校验规则的时候,也制定另外一个校验规则进行校验this.refs.form.validate((pass) => {})//表单整体验证,通过后发送请求
//使用自定义表单验证
<el-form
v-loading="loading"
:model="formData"//
:rules="rules"//
:ref="ruleForm"//
>
<el-form-item
label="设备名称"
prop="name"//
>
<el-input v-model="formData.name"></el-input>
</el-form-item>
<el-form-item
label="设备分类"
prop="category"//
>
<el-select v-model="formData.category">
<el-option
v-for="d in $store.state.statusList1"
:value="d.id"
:key="d.id"
:label="d.name"
>
</el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button
@click="submit"
v-loading="submiting"
>提交</el-button>
</el-form-item>
</el-form>
export default {
data() {
const nameValidator = (rule, value, callback) => {//需要在data下面写
if (value !=='1') {
callback(new Error('value不能为1'));
} else {
callback();
}
};
const categoryValidator = (rule, value, callback) => {
this.$refs.ruleForm.validateField("name");//这个校验的时候,使用指定校验来校验
callback();
};
return {
submiting: false,
formData: {
name: "",
category: "", },
loading: true,
rules: {//定义规则
name: [
{ required: true, message: "请输入活动名称", trigger: "blur" },
{ validator: nameValidator, trigger: "blur" },//validator使用自定义的规则
],
category: [
{ required: true, message: "请输入活动名称", trigger: "change" },
{ validator: categoryValidator, trigger: "blur" },
]
},
};
},
methods: {
submit() {
this.$refs.form.validate((pass) => {//全部校验成功后提交
if (!pass) return;
this.submiting = true;//按钮的loading效果,实现防抖,防止多次点击
axios.post("/pre-edit", this.formData).then((res) => {//发送请求
if (res.code) {
this.submiting = false;
this.$router.back();
}
});
});
},
},
};
</script>
new Date(year,month,0) 0 代表最后一天,1代表第一天
1.先获取当前年:let year=new Date().getFullYear()
当前月:let month=new Date().getMonth()+1,
2.获取这个月有多少天 :new Date(year,month,0).getDate(),利用push 添加到数组里面
3.获取到这个月第一天是周几:new Date(year,month-1,1).getDay()
再获取上个月最后一天:new Date(year,month-1,0).getDate(),利用unshife补全前面的天数
4.数组长度%7看余数 ,获取下月是那月new Date(year,month,1).getMonth(),push补全
<template>
<div class="week-wrapper">
<div>
<button @click="onChange(-1)">上个月</button>
<p>{{ new Date(year, month - 1, 1).getFullYear() }} - {{ new Date(year, month - 1, 1).getMonth() + 1 }}</p>
<button @click="onChange(1)">下个月</button>
</div>
<ul>
<li>周一</li>
<li>周二</li>
<li>周三</li>
<li>周四</li>
<li>周五</li>
<li>周六</li>
<li>周日</li>
</ul>
<ul>
<li
v-for="d in list"
:key="d.date"
>
{{ d.label }}
<span v-if="arr.includes(d.date)">✅</span>
</li>
</ul>
</div>
</template>
<script>
export default {
data() {
return {
list: [],
year: new Date().getFullYear(),
month: new Date().getMonth() + 1,
arr: ['8-5','8-6','8-7','8-10','9-2','9-5']
};
},
methods: {
setList(year, month) {
this.list = [];
// 这个月总共有多少天
const lastDay = new Date(year, month, 0);
const total = lastDay.getDate();
for (let i = 1; i <= total; i++) {
this.list.push({
label: i,
date: `${lastDay.getMonth() + 1}-${i}`
});
}
/* 0周日,1-6周1-周6
year年month月的第一天是周几
第一天是周1,week === 1,不用补
第一天是周2,week === 2,补1天
第一天是周3,week === 3,补2天
第一天是周4,week === 4,补3天
第一天是周5,week === 5,补4天
第一天是周6,week === 6,补5天
第一天是周日,week === 0,补6天
*/
const firstDay = new Date(year, month - 1, 1);
const week = firstDay.getDay();
const fillLeft = !week ? 6 : week - 1;
// 上个月的最后一天
const lastMonthLastDay = new Date(year, month - 1, 0);
let lastMonthLastDayCount = lastMonthLastDay.getDate();
for (let i = 0; i < fillLeft; i++) {
this.list.unshift({
label: lastMonthLastDayCount - i,
date: `${lastMonthLastDay.getMonth() + 1}-${lastMonthLastDayCount - i}`
});
}
// 下个月需要补几天
const n = this.list.length % 7;
if (n > 0) {
// 下个月是几月?
const nextMonth = new Date(year, month, 1);
for (let i = 0; i < 7 - n; i++) {
this.list.push({
label: i + 1,
date: `${nextMonth.getMonth() + 1}-${i + 1}`
});
}
}
},
onChange(n) {
this.month += n;
this.setList(this.year, this.month);
}
},
created() {
this.setList(this.year, this.month);
}
}
</script>
<style>
.week-wrapper ul {
display: flex;
flex-wrap: wrap;
width: 500px;
margin: 0 auto;
list-style: none;
}
.week-wrapper li {
width: 14.285%; /*七分之一*/
}
</style>
//bookshop页面
<template>
<div class="book-shop">
<ul>
<li v-for="(shop,index) in list" :key="shop.name">
<h2>
<div :class="{'checkbox':true,'checked':shop.books.every(i=>i.checked)}" @click="toggleShop(index)"></div>
{{shop.name}}
</h2>
<ul>
<li v-for="book in shop.books" :key="book.name">
<div :class="{'checkbox':true,'checked':book.checked}" @click="book.checked=!book.checked"> </div>
{{book.name}}-¥{{book.price}}
<step-input v-model="book.num"></step-input>
</li>
</ul>
</li>
</ul>
<div :class="{'checkbox':true,'checked':isAllChecked}" @click="toggleAll"></div>全选
<div>总数:{{total.num}}总价:{{total.price}}</div>
</div>
</template>
<script>
//@代表src
import StepInput from '@/components/StepInput.vue';
export default {
components: { StepInput },
data() {
return {
list: [
{
name:'书店1',
books:[
{
name:'小王子和白玫瑰',
price:10,
num:1,
checked:false
},
{
name:'十宗罪',
price:20,
num:1,
checked:false
},
]
},
{
name:'书店2',
books:[
{
name:'小公主',
price:50,
num:1,
checked:false
},
{
name:'白雪公主',
price:20,
num:1,
checked:false
},
]
},
]
}
},
methods: {
toggleShop(index) {
const checked1=this.list[index].books.every(i=>i.checked)//判断该书店下是不是都勾上了,也就是书店的勾选状态
this.list[index].books.forEach(item => {//当前书店状态取反
item.checked=!checked1
});
},
toggleAll(){
const checked=this.isAllChecked
this.list.forEach(shop=>shop.books.forEach(book=>book.checked=!checked))
}
},
computed: {
isAllChecked() {
return this.list.every(shop=>shop.books.every(book=>book.checked))
},
total(){
let num=0;let price=0;
this.list.forEach(shop=>{
shop.books.filter(book=>book.checked).forEach(book=>{
num+=book.num
price+=book.num*book.price
})
})
return {num,price}
}
},
}
</script>
<style lang="scss" scoped>
.book-shop{
.checkbox{
display: inline-block;
width:20px;
height: 20px;
border-radius: 50%;
border: 2px solid black;
}
& .checked{
background: red;
position: relative;
}
& .checked::after{
position: absolute;
content: '>';
left: -3px;
top: -2px;
width: 20px;
font-size: 16px;
color: white;
height: 20px;
transform: rotate(90deg);
}
}
</style>
//组件
//- 10 + 效果
<template>
<div class="step-input">
<button @click="$emit('input',value-1)" :disabled="value==0">-</button>
{{value}}
<button @click="$emit('input',value+1)">+</button>
</div>
</template>
<script>
export default {
props: {
value: {
type: Number,
default:0,
},
},
}
</script>
<style lang="scss" scoped>
</style>
function throttling(fun,delay =300){//节流throttle
let canRun = true; // 通过闭包保存一个标记
return function () {
if (!canRun) return;//在delay时间内,直接返回,不执行fn
canRun = false;
setTimeout(() => {
fn.apply(this, arguments);
canRun = true;//直到执行完fn,也就是delay时间后,打开开关,可以执行下一个fn
}, delay );
};
}
function debounce(func, delay = 300, immediate = false) {//防抖debounce
let timer = null
return function() {
if (timer) {
clearTimeout(timer)
}
if (immediate && !timer) {
func.apply(this, arguments)
}
timer = setTimeout(() => {
func.apply(this, arguments)
}, delay)
}
}
export {debounce,throttling}
<div @click="appSearch">
import {debounce,throttling} from 'assets/utils/debThro.js'
methods: {
appSearch:debounce(function(value){
this.handleSearch(value)
}, 1000),
handleSearch(value) {
console.log(value)
}
//或者
appSearch:throttling(function(){
let itemlength=this.value.list.length
this.$emit('nextItem',[itemlength,this.value.id])
}),
}
<input placeholder="请输入" @input="inputChange($event.target.value)"/>
timer:null,//用于防抖
personData:[],//请求结果
inputChange:debounce(function(val){
this.remoteSearch(val)
},300)
remoteSearch(keyword1) {
if(keyword1==''){
return
}
axios({
method:'post',
url:'/api/song-search',
data:{keyword: keyword1},
}).then((res) => {
// 将请求的结果赋值给personData全局变量,用于展示搜索结果
if (res.data.code === '0') {
this.personData = res.data.data || [];
} else {
this.personData = [];
}
})
//html 页面上配置script 动态给html赋值
(function(doc,win){
//获取html
var docEl=doc.documentElement,
//用于获取事件
//in window 用于查询window的一些事件和方法是否存在orientationchange屏幕翻转
resizeEvt="orientationchange" in window?"orientationchange":"resize";
function fun(){
var w=docEl.clientWidth;
if(!w) return
if(w>=750){
docEl.style.fontSize='100px'
}else{
docEl.style.fontSize=100 /750* w + 'px'
}
}
//如果文档没有addEventListener这方法,就停止执行
if(!doc.addEventListener)return
//通过二级事件绑定为window添加resize事件
win.addEventListener(resizeEvt,fun,false)
//页面初始化加载的时候也需要获取当前设备宽度,改变字体大小
//DOMContentLoaded w3c支持的一个事件,因为load事件会在页面所以元素渲染结束后才执行,DOMContentLoaded是在HTML 文档被完全加载和解析完成之后,不用等待图片
doc.addEventListener('DOMContentLoaded',fun,false)
})(document,window)
npm install postcss-pxtorem --save-dev
npm install amfe-flexible --save-dev
1、在mian.js中 引入amfe-flexible
import ‘amfe-flexible’
vue -version看版本
2.在vite.config.js中配置postcss-pxtorem
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import postCssPxToRem from "postcss-pxtorem"
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
css: {
postcss: {
plugins: [
postCssPxToRem({
rootValue: 112.5, // 设计图最大宽度除以10 //比如750的宽就写成75 我这边是1125的宽
propList: ['*'], // 需要转换的属性,这里选择全部都进行转换
})
]
},
}
})
2.(vue2)在根目录创建.postcssrc.js文件夹中进行配置
module.exports = {
plugins: {
// to edit target browsers: use "browserslist" field in package.json
autoprefixer: {
browsers: ['Android >= 4.0', 'iOS >= 7']
},
'postcss-pxtorem': {
rootValue: 37.5,
propList: ['*', '!border*']
}
}
}
我还是想使用px来表达的话,那么我们可以把1px写成 1Px 或 1PX来解决
https://better-scroll.github.io/docs/zh-CN/plugins/slide.html#%E4%BB%8B%E7%BB%8D
npm install better-scroll --save //具备所有插件的
npm install @better-scroll/core --save//基本滚动
//按需下载插件
//使用插件
import BScroll from '@better-scroll/core'
//import Pullup from '@better-scroll/pull-up'
// 注册插件
//BScroll.use(Pullup)
let bs = new BScroll('.wrapper', {
probeType: 3,
pullUpLoad: true,
})
import BScroll from '@better-scroll/core'
//.wrapper设置宽高,里面设置一个盒子,在盒子里面再写结构
created () {
this.getSingerList()//获取数据
},
mounted () {
this.$nextTick(() => {//设置scroll
this.SetScroll();
});
},
methods: {
SetScroll(){
this.scroll = new BScroll('.wrapper',{
scrollY: true,
click: true,
probeType: 3
})
}}
beforeDestroy(){//卸载,防止内存泄漏
this.scroll.destroy()
}
当数据发生变化的时候,就调用函数里面的$nextTick,然后进行刷新滚动的方法(scroll.refresh())
watch: {
cities: function () {
this.$nextTick(() => {
this.scroll.refresh()
})
}
}
把Ajax响应过来的数据进行监听(如上:cities)
当数据发生变化的时候,就调用函数里面的$nextTick,然后进行刷新滚动的方法(scroll.refresh())
mounted () {
this.$nextTick(() => {
this.singerScroll();//把获得的数据存到newSingerList里面
});
},
watch(){
newSingerList(){//监听newSingerList的改变
this.$nextTick(()=>{
this.$refs.scrollItem.forEach(i=>{//获取左边每块到顶部的距离
this.heigtList.push(i.offsetTop)
})
this.scroll1.refresh()//数据发生变化,从新渲染
})
}
}
methods: {
singerScroll(){
this.scroll1 = new BScroll('.swiper',{
scrollY: true,
click: true,
probeType: 3
})
this.scroll1.on('scroll', ({ y }) => {//获得滚动y轴高度
this.heigtList.forEach((item,index)=>{ //判断在那个区间,高亮
if(-y>=item&&-y<this.heigtList[index+1]){
this.currentIndex=index
}else if(-y>=this.heigtList[this.heigtList.length-1]){//判断到最后一个高亮
this.currentIndex=this.heigtList.length-1
}
})
})
},
onShortcutStart(index){ //点击右边导航,滚动到指定位置
this.currentIndex=index
this.scroll1.scrollTo(0,-this.heigtList[index],300)
},
}
//根目录创建test.js
const axios=require('axios')
axios.post('http://jxsjs.com/equipment/login').then(({data})=>{console.log(data)})
vue.config.js里面
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
// 开发服务器
devServer: {
// 服务器接口代理
proxy: {
// 请求的路径中包含/api就转发
'/api': {
// 代理的不同域远程服务器
target: 'http://m.jxsjs.com'
}
}
}
})
import { get, post } from './http'
export const apiAddress = p => post('/hot-music', p);//这里就不用写/api
/**axios封装
* 请求拦截、相应拦截、错误统一处理
*/
import axios from 'axios';
import QS from 'qs';
import { Toast } from 'vant';
import store from '../store/index'
import router from '../router/index'
// 环境的切换
if (process.env.NODE_ENV == 'development') {
axios.defaults.baseURL = '/api';
} else if (process.env.NODE_ENV == 'debug') {
axios.defaults.baseURL = '';
} else if (process.env.NODE_ENV == 'production') {
axios.defaults.baseURL = 'http://api.123dailu.com/';
}
// 请求超时时间
axios.defaults.timeout = 10000;
// post请求头
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8';
// 请求拦截器
axios.interceptors.request.use(
config => {
// 每次发送请求之前判断是否存在token,如果存在,则统一在http请求的header都加上token,不用每次请求都手动添加了
// 即使本地存在token,也有可能token是过期的,所以在响应拦截器中要对返回状态进行判断
const token = store.state.token;
token && (config.headers.Authorization = token);
return config;
},
error => {
return Promise.error(error);
})
// 响应拦截器
axios.interceptors.response.use(
response => {
if (response.status === 200) {
return Promise.resolve(response);
} else {
return Promise.reject(response);
}
},
// 服务器状态码不是200的情况
error => {
if (error.response.status) {
switch (error.response.status) {
// 401: 未登录
// 未登录则跳转登录页面,并携带当前页面的路径
// 在登录成功后返回当前页面,这一步需要在登录页操作。
case 401:
router.replace({
path: '/login',
query: { redirect: router.currentRoute.fullPath }
});
break;
// 403 token过期
// 登录过期对用户进行提示
// 清除本地token和清空vuex中token对象
// 跳转登录页面
case 403:
Toast({
message: '登录过期,请重新登录',
duration: 1000,
forbidClick: true
});
// 清除token
localStorage.removeItem('token');
store.commit('loginSuccess', null);
// 跳转登录页面,并将要浏览的页面fullPath传过去,登录成功后跳转需要访问的页面
setTimeout(() => {
router.replace({
path: '/login',
query: {
redirect: router.currentRoute.fullPath
}
});
}, 1000);
break;
// 404请求不存在
case 404:
Toast({
message: '网络请求不存在',
duration: 1500,
forbidClick: true
});
break;
// 其他错误,直接抛出错误提示
default:
Toast({
message: error.response.data.message,
duration: 1500,
forbidClick: true
});
}
return Promise.reject(error.response);
}
}
);
/**
* get方法,对应get请求
* @param {String} url [请求的url地址]
* @param {Object} params [请求时携带的参数]
*/
export function get(url, params){
return new Promise((resolve, reject) =>{
axios.get(url, {
params: params
})
.then(res => {
resolve(res.data);
})
.catch(err => {
reject(err.data)
})
});
}
/**
* post方法,对应post请求
* @param {String} url [请求的url地址]
* @param {Object} params [请求时携带的参数]
*/
export function post(url, params) {
return new Promise((resolve, reject) => {
axios.post(url, QS.stringify(params))
.then(res => {
resolve(res.data);
})
.catch(err => {
reject(err.data)
})
});
}
import { apiAddress } from '@/request/api';// 导入我们的api接口
export default {
name: 'Address',
created () {
this.onLoad();
},
methods: {
// 获取数据
async onLoad() {
// 调用api接口,并且提供了两个参数
const list=await apiAddress({type: 0})
this.singList=list.data
}
}
}
import axios from 'axios'
import {
Message,
// Loading
} from "element-ui";
// 此时需要自行下载一下qs
// import qs from 'qs'
//判断是否是生产环境
var isPro = process.env.NODE_ENV === "production" //process.env.NODE_ENV用于区分是生产环境还是开发环境
//配置不同的baseURL
// 原理:
// 在生产环境(给客户部署项目)下使用的是"/weixin-api"(后端给的路径域名后边的部分),此时会自动拿到ip地址 + /weixin-api(路径拼接),在本地跑项目时拿到的时/api 也就是vue.config.js配置跨域下的路径;"/weixin-api" : "/api"
let baseURL = isPro ? "/api" : "/api"
const service = axios.create({
baseURL: baseURL,
timeout: 30000 // 请求超时时间
})
let loading = "";
// 请求拦截器
service.interceptors.request.use(
(config) => {
if(store.state.user.token){//判断vuex里面是否有token,有就每次请求携带token
config.headers.token=store.state.user.token
}
// console.log(config)
// 在请求发送之前做一些处理
if (!(config.headers['Content-Type'])) {
// loading = Loading.service({//全局加载
// lock: true,
// text: "加载中...",
// spinner: "el-icon-loading",
// background: "rgba(255,255,255,0.7)",
// customClass: "request-loading",
// });
if (config.method == 'post') {
config.headers['Content-Type'] =
'application/x-www-form-urlencoded;charset=UTF-8'
for (var key in config.data) {
if (config.data[key] === '') {
delete config.data[key]
}
}
// qs用于序列化data传输的数据 不然后端拿到的话会出现data数据套了一层拿不到数据的问题
// config.data = qs.stringify(config.data)
} else {
config.headers['Content-Type'] =
'application/x-www-form-urlencoded;charset=UTF-8'
// config.params
}
}
const token = localStorage.getItem("token");
// 让每个请求携带token-- ['X-Token']为自定义key 请根据实际情况自行修改
if (token) {
config.headers['Authorization'] = token
}
return config
},
(error) => {
// loading.close();
// 发送失败
console.log('发送失败', error)
return Promise.reject(error)
}
)
// 响应拦截器
service.interceptors.response.use(
(response) => {
// loading.close();
const dataAxios = response.data
// 这个状态码是和后端约定的
return dataAxios
},
(error) => {
Message({
message: error,
type: 'error',
duration: 3 * 1000
})
// 如果请求接口失败,取消loading,否则中间有一个接口错误就一直白屏loading转圈;
loading.close();
return Promise.reject(error)
}
)
export default service
import axios from "@/assets/request";
axios.get('product/getBaseCategoryList', {})
添加链接描述
//yemian.vue
import axios from 'axios';
const CancelToken = axios.CancelToken;
let cancel;
getSongCont(){//请求函数
if(cancel){ cancel()}//如果有请求就取消
axios({
method:'post',
data:{keyword:this.value},
url:'/api/song-search',//地址
cancelToken: new CancelToken(function executor(c) {//设置CancelToken
cancel = c;
}),})
},
export default new Router({
routes: [
{
path: '/',
redirect:'/home'
},
{
path: '/home',
name: 'home',
component: home
}
],
linkActiveClass:'router-active'//覆盖默认的路由高亮的类
})
//一级路由用这个
.router-link-active{
color: yellow;
}
//二级路由用这个
.router-link-exact-active{
color: yellow;
}
.a {
background-color: red;
}
.b {
background-color: green;
}
.left-enter {
transform: translateX(100%);
}
.left-enter-to {
transform: translateX(0);
}
.left-enter-active {
transition: transform 1s;
}
.left-leave {
transform: translateX(0);
}
.left-leave-to {
transform: translateX(-100%);
}
.left-leave-active {
transition: transform 1s;
}
.right-enter {
transform: translateX(-100%);
}
.right-enter-to {
transform: translateX(0);
}
.right-enter-active {
transition: transform 1s;
}
.right-leave {
transform: translateX(0);
}
.right-leave-to {
transform: translateX(100%);
}
.right-leave-active {
transition: transform 1s;
}
</style>
<body>
<div id="app">
<transition :name="transitionName">
<router-view></router-view>
</transition>
</div>
<script>
new Vue({
data: {
transitionName: 'left'//通过transitionName变化left/right改变效果
},
watch: {
$route(to, from) {//判断路由父子关系
const toPath=to.path.split('/').length
const fromPath=from.path.split('/').length
this.transitionName=toPath<fromPath ? 'right':'left'
}
}
})
</script>
</body>
</html>
.left-enter-from {
transform: translateX(100%);
}
.left-enter-to {
transform: translateX(0);
}
.left-enter-active {
transition: transform 1s;
}
.left-leave-from {
transform: translateX(0);
}
.left-leave-to {
transform: translateX(-100%);
}
.left-leave-active {
transition: transform 1s;
}
.right-enter-from {
transform: translateX(-100%);
}
.right-enter-to {
transform: translateX(0);
}
.right-enter-active {
transition: transform 1s;
}
.right-leave-from {
transform: translateX(0);
}
.right-leave-to {
transform: translateX(100%);
}
.right-leave-active {
transition: transform 1s;
}
<router-view v-slot="{ Component }">
<transition :name="transitionName">
<component :is="Component" />
</transition>
</router-view>
<script setup lang="ts">
import { ref } from "vue";
import { onBeforeRouteUpdate } from "vue-router";
let transitionName = ref(" ");
onBeforeRouteUpdate((to) => {//监听路由的改变
if (to.path == "/daka2/addpeople") {
transitionName.value = "right";
} else {
transitionName.value = "left";
}
});
</script>
根据虚线的间距
stroke-dasharray用于创建虚线,之所以后面跟的是array的,是因为值其实是数组,一个参数时: 其实是表示虚线长度和每段虚线之间的间距,两个参数或者多个参数时:一个表示长度,一个表示间距
svg {
vertical-align: middle;
width: 150px;
height: 150px;
}
.pie-bg {
stroke: #bacd0c;
opacity: .3;
}
.pie-bar {
stroke: #f2ff00eb;
}
circle {
stroke-dashoffset: 0;
transform: rotate(-90deg);
transform-origin: center;
transition: all .2s;
stroke: currentColor;
z-index: 2;
}
<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" viewBox="0 0 50 50">
<circle fill="transparent" class="pie-bg" stroke-width="1" cx="25" cy="25" r="20"></circle>
<circle fill="transparent" class="pie-bar" stroke-width="1" cx="25" cy="25" r="20"
style="stroke-dasharray: 23.654, 160.664;"></circle>//第一个值代表长度
</svg>
var pieBar = document.querySelector('.pie-bar');
var pathLen = 40 * Math.PI;//圆的周长
var percent = 45;//占百分比,在这设置
pieBar.style.strokeDasharray = pathLen * percent / 100 + " " + pathLen;
//浏览器请求报文=响应头+响应体(post参数)
//服务器返回响应报文=响应头+响应体(服务器返回的数据)
//cookie,响应头包含set-cookie告诉浏览器把这个给我保存起来,之后的请求浏览器都会携带cookie
--package.json
serve:" vue-cli-service serve --open"
//在根目录下vue.config.js
//关闭eslint
lintOnSave:false
忽略,不检查
修改eslint,最简单的一种,就是忽略…也就是不检查,一了百了,在项目的根目录下新建一个文件,文件名:.eslintignore(就是这个,不要加多余的东西),然后文件里可以添加待忽略的文件或文件夹
# 忽略目录
build/
tests/
node_modules/
src/micpo
# 忽略文件
**/*-min.js
**/*.min.js
iconfont.js
//方法一:v-show
<!-- 底部位置判断路由路径显示footer -->
//home,search有其他没有
<footer-index v-show="$route.path=='/home'||$route.path=='/search'"></footer-index>
//方法二:元 信息
<footer-index v-show="$route.meta.show"></footer-index>
//路由配置meta
{
path:'/home',
component:Home,
meta:{show:true}
},
{
path:'/login',
component:Login,
meta:{show:false}
},
声明式导航不会有这样的错误
为什么会这样是因为编程式导航会返回一个promise,拥有成功和失败的回调
//解决方法
Vue.use(VueRouter)
//先把vueRouter原型对象的push,replace,先保存一份
let originPush=VueRouter.prototype.push;
let originReplace=VueRouter.prototype.replace;
//重写push和replace方法
//第一个参数:往那跳(传递什么参数)
//第二个参数:成功回调
//第三个参数:失败回调
VueRouter.prototype.push=function(location,resolve,reject){
if(resolve&& reject){
originPush.call(this,location,resolve,reject)
}else{
originPush.call(this,location,()=>{},()=>{})
}
}
VueRouter.prototype.replace=function(location,resolve,reject){
if(resolve&& reject){
originReplace.call(this,location,resolve,reject)
}else{
originReplace.call(this,location,()=>{},()=>{})
}
}
//只需要跳转自己就可以了
this.$router.push("/search");
<el-input
placeholder="请输入内容"
v-model="phoneCode"
>
<template slot="append">
<el-button
@click="getCode"
:disabled="isdisable||inputPhone.length==0"//判断是否禁用,一开始输入框为空禁用,点击获取验证验证手机号是否正确
>{{codeBtnDisabled}}</el-button>
</template>
</el-input>
countNum :10//初始倒计时时间
isdisable :false//禁用设置
computed: {
codeBtnDisabled() {
if (this.isdisable) {//如果已经被禁用就显示倒计时
return "倒计时" + this.countNum;
} else {//没有就显示获取验证码
return "获取验证码";
}
},
},
this.isdisable = true;
this.t = setInterval(() => {
this.countNum = this.countNum -= 1;
if (this.countNum == 0) {
clearInterval(this.t);
this.countNum = 10;
this.isdisable = false;
}
}, 1000);
//todolist.vue
<template>
<div>
<input
type="text"
v-model="inp"
><button @click="add">添加</button>
<ul>
<list-item
v-for="item in list"
:key="item.id"
:value="item"
@del="deleItem"
@xg="xg1"
></list-item>
</ul>
</div>
</template>
<script>
import ListItem from "./ListItem.vue";
export default {
components: { ListItem },
data() {
return {
list: [
{ id: 1, name: "AAA" },
{ id: 2, name: "BBB" },
{ id: 3, name: "CCC" },
],
inp: "",
};
},
methods: {
xg1(name) {
this.list.splice(
this.list.findIndex((i) => i.id == name.id),
1,
name
);
},
add() {
this.list.push({ id: new Date().getTime(), name: this.inp });
this.inp = "";
},
deleItem(id) {
this.list.splice(
this.list.findIndex((i) => {
return i.id == id;
}),
1
);
},
},
};
</script>
<style lang="scss" scoped>
</style>
//ListItem
<template>
<div>
<li>
<input
v-if="isShow"
type="text"
placeholder="请输入"
v-model="inp"
>
<span v-else>{{value.name}}</span>
<button @click="del">删除</button>
<button @click="xiugaiAndChange">{{change}}</button>
</li>
</div>
</template>
<script>
export default {
props: {
value: {
type: Object,
},
},
data() {
return {
isShow: false,
inp: "",
};
},
computed: {
change() {
if (this.isShow) {
return "保存";
} else {
return "修改";
}
},
},
methods: {
del() {
this.$emit("del", this.value.id);
},
xiugaiAndChange() {
if (this.isShow) {
this.isShow = !this.isShow;
this.$emit("xg", { id: this.value.id, name: this.inp });
} else {
this.isShow = !this.isShow;
}
},
},
};
</script>
<style lang="scss" scoped>
</style>
//a.vue
<template>
<div>
<input type="text" v-model="inp1" /><button @click="add">添加</button>
<item-git
v-for="(item, index) in list"
:key="item.id"
v-model="list[index]"
:index="index + ''"
@del=" (index) => { list.splice(index, 1)} " ></item-git>
</div>
</template>
<script>
import itemGit from "./itemGit.vue";
export default {
components: { itemGit },
data() {
return {
inp1: "",
list: [
{ id: 1, name: "111" },
{ id: 2, name: "222" },
],
};
},
methods: {
add() {
this.list.push({ id: new Date().getTime(), name: this.inp1 });
this.$nextTick(() => {});
},
},
};
</script>
//b.vue
<template>
<div>
<span v-show="show">{{ value.name }}</span>
<input v-show="!show" type="text" v-model="inp" :ref="value.id" />
<button @click="chang">{{ changeTitle }}</button>
<button @click="$emit('del', index)">删除</button>
</div>
</template>
<script>
export default {
props: {
value: {
type: Object,
default: () => {},
},
index: { type: String },
},
computed: {
changeTitle() {
if (this.show) {
return "修改";
} else {
return "保存";
}
},
},
data() {
return {
show: true,
inp: JSON.parse(JSON.stringify(this.value.name)),
};
},
methods: {
chang() {
this.show = !this.show;
if (this.changeTitle == "保存") {
this.$nextTick(() => {
this.$refs[this.value.id].focus();
});
} else {
this.$emit("input", { id: this.value.id, name: this.inp });
}
},
},
};
</script>
<el-table-column min-width="300px" label="标题">
<template scope="scope">
<el-input v-show="scope.row.edit" size="small" v-model="scope.row.title"></el-input>
<span v-show="!scope.row.edit">{{ scope.row.title }}</span>
</template>
</el-table-column>
<el-table-column align="center" label="编辑" width="120">
<template scope="scope">
<el-button v-show='!scope.row.edit' type="primary" @click='scope.row.edit=true' size="small" icon="edit">编辑</el-button>
<el-button v-show='scope.row.edit' type="success" @click='scope.row.edit=false' size="small" icon="check">完成</el-button>
</template>
</el-table-column>
//a.vue
<script setup lang="ts">
import HelloWorld from "./components/HelloWorld.vue";
import { ref, reactive } from "vue";
interface Obj {
id?: Number;
name?: String;
}
let list: Obj[] = reactive([
{ id: 1, name: "123" },
{ id: 21, name: "123" },
]);
let inp = ref("");
const add = () => {
list.push({ id: 4, name: inp.value });
};
</script>
<template>
<input type="text" v-model="inp" /><button @click="add">添加</button>
<HelloWorld
v-for="(item, index) in list"
v-model="list[index]"
:index="index"
@del=" (i:number) => { list.splice(i, 1); } " ></HelloWorld>
</template>
//b.vue
<template>
<div>
<p v-show="isShow">{{ modelValue.name }}</p>
<input v-show="!isShow" type="text" v-model="inpp" />
<button @click="update">{{ changStr }}</button>
<button @click="$emit('del', index)">删除</button>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from "vue";
const props = defineProps(["modelValue", "index"]);
const emit = defineEmits(["update:modelValue", "del"]);
let isShow = ref(true);
let inpp = ref(props.modelValue.name);
let changStr = computed(() => {
if (!isShow.value) {
return "保存";
} else {
return "修改";
}
});
const update = () => {
isShow.value = !isShow.value;
if (isShow.value) {
emit("update:modelValue", { id: props.modelValue.id, name: inpp.value });
}
};
</script>
npm install --save nprogress
//这里在封装的request.js里面引入,请求的时候呈现
import nprogress from 'nprogress';//进度条效果
//引入进度条样式
import "nprogress/nprogress.css"
//请求拦截器
nprogress.start()
// 响应拦截器
nprogress.done()
简单写法
1.下载
npm install mockjs
3.引入
//main.js
import '@/mock' //引入mock
4.index.js注册所有的mock服务
// 首先引入Mock
import Mock from "mockjs";
// 设置拦截ajax请求的相应时间
Mock.setup({
timeout: '200-600'
});
let configArray = [];
// 使用webpack的require.context()遍历所有mock文件
const files = require.context('.', true, /\.js$/);
files.keys().forEach((key) => {
if (key === './index.js') return;
configArray = configArray.concat(files(key).default);
});
// 注册所有的mock服务
configArray.forEach((item) => {
for (let [path, target] of Object.entries(item)) {
let protocol = path.split('|');
Mock.mock(new RegExp('^' + protocol[1]), protocol[0], target);
}
});
5.get,post请求获取数据
//demoList .js
let demoList = [{
id: 1,
name: 'zs',
age: '23',
job: '前端工程师'
},{
id: 2,
name: 'ww',
age: '24',
job: '后端工程师'
}]
//axios.get请求
export default {
'get|/parameter/query': () => {//get(设置请求方式post、get)|(option) 参数option传递的数据
return {
status: 200,
message: 'success',
data: demoList
};
}
}
6.Mock.mock拦截请求
Mock.mock( rurl, rtype, template|function( options ) )
rurl
可选。
表示需要拦截的 URL,可以是 URL 字符串或 URL 正则。例如 '/domian/list.json'。
rtype
可选。
表示需要拦截的 Ajax 请求类型。例如 GET、POST、PUT、DELETE 等。
template
可选。
表示数据模板,可以是对象或字符串。
数据模板中的每个属性由 3 部分构成:属性名、生成规则、属性值:
// 属性名 name
// 生成规则 rule
// 属性值 value
'name|rule': value
例如:'name|1-10':1 会产生一个1-10之间的整数,详细规则参见官方文档
function(options)
可选。
表示用于生成响应数据的函数。
options
指向本次请求的 Ajax 选项集,含有 url、type 和 body 三个属性
7.登录数据拦截,如果密码用户名正确就返回
import Mock from "mockjs";
import vue from 'vue'
let vm=new vue()
// mock
Mock.mock(/\/api\/login/, "post", (option) => {
console.log(option);
// 获取post请求传过来的参数
let params = JSON.parse(option.body);
let response = {
code: 0,
data: [],
};
//判断用户名和密码是否是admin和123
if (params.username === "admin" && params.password === "123") {
response.data = {
token: window.btoa("token"),
};
vm.$message({
message: '登录成功',
type: 'success'
});
} else {
response = {
code: 401,
data: null,
};
vm.$message({
message: '密码或者用户错误',
type: 'warning'
});
}
return response;
});
export default {
Mock
}
因为以后要打包,打包需要放到poblic文件夹下,因为打包会生成dist文件夹
添加链接描述
在项目跟文件下的data数据文件夹里
cmd 输入 json-server cost.json
页面刷新后,原有的 vuex 中的 state 会发生改变,如果在页面刷新之前,可以将 state 信息保存到localstorage,页面重新加载时,从localstorage中取出,再将该值赋给 state,那么该问题即可解决。
【在某组件中添加如下钩子函数。比如 App.vue中】
created() {
//在页面加载时读取localStorage里的状态信息
if (localStorage.getItem("store") ) {
this.$store.replaceState(Object.assign({}, this.$store.state,JSON.parse(localStorage.getItem("store"))))
}
//在页面刷新时将vuex里的信息保存到localStorage里
window.addEventListener("beforeunload",()=>{
localStorage.setItem("store",JSON.stringify(this.$store.state))
})
}
注:
this.$store.replaceState() 用于替换 store 的信息(状态合并)。
Object.assign(target, ...source) 将source的值 赋给 target,若有重复的数据,则覆盖。其中...表示可以多个source。
JSON.stringify() 用于将对象转为 JSON
JSON.parse() 用于将 JSON 转为对象
//从新发起请求获取数据
ssr服务器端渲染,浏览器直接呈现服务器返回的htm利用node搭建页面渲染。浏览器加载html文件—>服务端填好内容—>返回浏览器渲染
csr浏览器端渲染:浏览器加载html文件—>下载js—>运行vue—>渲染页面
优势:更好的seo,搜索引擎优化,提高页面页面加载速度
1.vue官网 vue ssr
https://vuejs.org/guide/scaling-up/ssr.html#rendering-an-app
2.简易ssr
需要和vue版本一样
//下载这些
"dependencies": {
"express": "^4.17.3",
"vue": "2.6.14",
"vue-router": "^4.0.12",
"vue-server-renderer": "2.6.14"
}
1.修改vue.config.js
publicPath:“./”
2.如果使用histor打包需要和后端商量配置
3.把histor更改为hash路由模式
//放到父元素上,没有经历循环,所以只产生一个回调函数,用事件委托
//1.根标签
class="all-sort-list2"
@click="goSearch($event)"
>
//2.给子节点添加指定
:data-categoryName="c1.categoryName"
:data-category1Id="c1.categoryId"
>{{c1.categoryName}}</a>
goSearch(event) {
//路由跳转的方法
let element = event.target;//获得标签上的属性
let { categoryname, category1id, category2id, category3id } =
element.dataset;
if (categoryname) {//如果有就表示是a标签
let location = { name: "search" };
let query = { categoryName: categoryname };
if (category1id) {//判断一二三级分类
query.category1Id = category1id;
} else if (category2id) {
query.category2Id = category2id;
} else if (category3id) {
query.category3Id = category3id;
}
location.query = query;
this.$router.push(location);//路由跳转,对象模式
}
},
切换路由列表重复加载问题
有的列表信息,只需要加载一次,每次切换组件都会从新加载,这时候就在app.vue里面写加载一次就行,根组件的mounted只执行一次
合并路由跳转的params,query参数
goSearch() {
//点击搜索跳转search路由,自己是params参数//携带三级分类的query参数
if (this.$route.query) {
let location = {
name: "search",
params: {
keyword: this.keyword || undefined,
},
};
location.query = this.$route.query;
this.$router.push(location);
}
},
{
path:'/search/:keyword?',
component:Search,
name:'search',
meta:{show:true}
},
//点击三级分类,自己是query,携带pramas
let element = event.target;//data-categoryname=“c2.categoryName”
let { categoryname, category1id, category2id, category3id } =
element.dataset;//获得data- 属性
let location = { name: "search" };
let query = { categoryName: categoryname };
//判断路由跳转,是否有params参数,有就一起带过去
if (this.$route.params) {
location.params = this.$route.params;
location.query = query;
this.$router.push(location);
}
}
路由登录跳转之前点击的页面
//beforeEach
//未登录不能去哪里
let toPath = to.path
if (toPath.indexOf('/trade') == -1 || toPath.indexOf('/pay') != -1 || toPath.indexOf('/center') !== -1) {
//都没登录进入login
next('/login?redirect=' + toPath) //登录后跳转之前点的页面
//把未登录时候的要去成的信息,存储在地址栏中,使用query参数
页面中使用
//看是否包含query参数,如果有,跳到query参数,指定的路由,没有就去首页
let toPath=this.$route.query.redirect||'/home'
this.$router.push(toPath);
判断是否是从指定页面过来,否则停留在当前页面
router
{
path: '/trade',
component: TradeIndex,
name: 'trade',
meta: {
show: true
},
beforeEnter: (to, from, next) => {
if (from.path == '/shopcart') {
next()
} else {
next(false)
//终端当前导航,如果浏览器的url改变了,那么url的地址会重置到from路由对应的地址
}
}
},
vue使用懒加载lazyload
//package.json 如果报错就修改版本
# 先写在原有的安装
npm uninstall vue-lazyload --save
# 再安装低版本的
npm install vue-lazyload@1.3.3 --save
npm网站
//动态数据的时候需要设置一个key值
<ul>
<li v-for="img in list">
<img v-lazy="img.src" :key="img.src" >
</li>
</ul>
数字增加效果组件requestAnimationFrame
<template>
<div>
<p> {{n}}</p>
</div>
</template>
<script>
export default {
data() {
return {
n: 0,//页面显示数字
step: 0,//刷新率
};
},
props: {
value: {
type: Number,
default: 0,
},
},
created() {
requestAnimationFrame(this.render);
},
methods: {
render() {
if (this.value === 0) {
return;
}
this.n += this.step;
if (this.n < this.value) {
//在动画没有结束前,递归渲染
requestAnimationFrame(this.render);
} else {
this.n = this.value;
}
},
},
watch: {
//因为props传过来是异步的所以,需要watch来监听数据变化
value() {
this.step = Math.floor(this.value / 70);
this.n = 0;
requestAnimationFrame(this.render);
},
},
};
</script>
使用vue-animate-number实现数据动态改变
npm install vue-animate-number
//main.js
import VueAnimateNumber from 'vue-animate-number'
Vue.use(VueAnimateNumber)
//组件中使用
<animate-number from="0" :to="item.num" duration="2000"></animate-number>
//动态获取的数据
// 添加一个key值 等于item.num
<animate-number from="0" :to="item.num" :key="item.num" duration="2000"></animate-number>
父组件异步数据通过props方式传递给子组件,子组件接收不到的问题
1. 使用v-if控制子组件渲染的时机
缺点:有一段空白期
<child :msg="msg" v-if="isGetData"></child>
2.子组件使用watch监听父组件传递过来的数据
例如swiper动态获取数据
//子组件
props: {
msg: {
type: String,
default: "",
},
},
watch: {
// 监听到父组件传递过来的数据后,加工一下,
// 存到data中去,然后在页面上使用
msg(newnew, oldold) {
},
},
兄弟之间组件传递对象数据,深拷贝浅拷贝问题
//ab兄弟组件,a传给b,传递一个对象,如果要修改对象的一个属性,
obj={name:'张三',age:18}
this.$emit('b',obj)
//b.vue 使用watch来监听这个值,如果是浅拷贝,就只有一开始的时候执行一次watch
watch:{
obj(){}
}
//解决办法:1.deep:true
// 2.this.$emit('b',{...obj})//深拷贝对象
vuex 修改对象数据,深浅拷贝监听问题
//a.vue 传
this.$store.commit('setFormData', JSON.parse(JSON.stringify(this.formData)));//需要转化成深拷贝
// store/index.js
setFormData(state, formData) {
state.formData = formData;
}
//b.vue 取
watch: {
'$store.state.formData'() {//只有对象是深拷贝后,才会监听到变化
this.pageNo = 1;
this.getList();
}
},
keep-alive路由切换,点击过的数据就不从新加载,每点击过再从新获取
<nav>
<router-link to="/home"></router-link>
<router-link to="/about"></router-link>
</nav>
<keep-alive :include="['AboutView','HomeView']"(组件名)>//如果不配置include 会把router-view展示区域的所有东西都缓存下来,10个切换项就要保存10个浪费性能
<router-view></router-view>
</keep-alive>
//点击列表vue
数据一
数据二
数据三
//详情页vue
要求第一次点击进去发送请求,再一次点击相同的进去不发送请求,点击不一样的从新发送请求
1.保存id值
id=""
created(){
this.id=this.$route.query.id
this.getList()//先发一次请求
}
methods:{
getList(){
id:this.id
}//发送请求
}
activeted(){
//this.id//之前记录下来的id
//this.$route.query.id//最新的id
//判断俩是否相等
if(this.id!==this.$route.query.id){
this.id=this.$route.query.id
this.getList()//发送请求
}
}
当前页面下,监听路由的变化,再次发送请求
不用beforeRouteUpdate因为获取不到路由信息
watch: {
$route(to,from){
console.log(to.path);
}
},
项目登录流程
登录接口完成后 会得到token ,vuex保存token,本地存储保存token,然后跳转到home页面,home页面一加载,mounted就调用接口获取用户信息,携带token
store 里面
token: localStorage.getItem(“TOKEN”),
//本地存储token
async userLogin({commit},user){ //登录
const result=await $axios.post("/user/passport/login/",user)
if(result.code==200){
//服务器下发token ,vuex保存token,
commit("USERLOGIN",result.data.token)
localStorage.setItem("TOKEN",result.data.token)
return 'ok'
}else{
return Promise.reject(new Error('faile'))
}
},
路由前置守卫
router.beforeEach(async (to,from,next)=>{
let token=store.state.user.token//token
let name=store.state.user.userInfo.name//用户信息
if(token){
if(to.path=='/login'||to.path=='/register'){//如果登录了,就不能去login界面
next('/home')
}else{
if(name){//如果有用户信息就直接放行
next()
}else{//没有就去获取
try {
await store.dispatch('user/userInfo')//获取用户信息
// async userInfo({commit}){ //获取用户信息
// const result= await $axios.get("/user/passport/auth/getUserInfo")
// if(result.code==200){
// commit('GETUSERINFO',result.data)
// //持久化存储
// return 'ok'
// }else{
// return Promise.reject(new Error('faile'))
//}
// },
next()
} catch (error) {
//如果token已经失效 就登出 并且回到login
store.dispatch('user/userLogout')
// async userLogout({commit}){ //获取用户信息
// const result= await $axios.get( '/user/passport/logout')
// if(result.code==200){
//持久化存储
// commit('CLEAR')(state.token='', state.userInfo={} localStorage.removeItem("TOKEN"))
// return 'ok'
// }else{
// return Promise.reject(new Error('faile'))
// }
// },
next('/login')
}
}
next()
}
}else{
next()
}
})
退出登录流程
1.发送请求告诉服务器,提交登陆
2.清除浏览器的所以缓存,token,vuex保存的用户信息,token
登录token 刷新问题
token会有过期时间,使用refresh_token进行token刷新,保持登录状态
添加链接描述
路由设置是否登录,登录就跳转,没有就跳转登录,登录鉴权
//router/index.js
{
path: '/mine',
name: 'mine',
component: Mine,
meta:{
auth:false//判断是否需要登录判断
}
},
router.beforeEach((to,from,next)=>{
let token=localStorage.getItem('userName')
// 判断该页面是否需要登录
if(to.meta.auth){
// 如果token存在直接跳转
if(token){
next()
}else{
// 否则跳转到login登录页面
next({
path:'/login',
// 跳转时传递参数到登录页面,以便登录后可以跳转到对应页面
query:{
redirect:to.fullPath
}
})
}
}else{
// 如果不需要登录,则直接跳转到对应页面
next()
}
})
//登录.vue
methods: {
submit() {
//登录成功后存储用户信息
localStorage.setItem("userName", this.name);
// 接收参数,如果存在redirect参数,登录后重定向到指定页面
if (this.$route.query.redirect) {
this.$router.push({ path: this.$route.query.redirect });
// 如不存在,直接跳转到首页
} else {
this.$router.push({ path: "/home" });
}
}
}
后台路由鉴权,权限
1.登录后会返回用户信息,路由权限数组,用户信息,按钮权限,使用vuex保存起来。
2.拆分现有的路由,分为固定路由(登录,首页,404等),暴露动态路由(需要权限的路由)
3.vuex里面根据暴露的动态路由和接口获得的权限,使用filter+indexOf!==-1判断是否有这个权限,同时需要注意2,3级路由,利用递归传递子路由进行对比。
4.concat合并固定路由动态路由.
5.在beforEach中addRouter添加路由
6.左侧菜单权限,遍历路由信息,利用递归组件,判断是否用于children属性
路由点击页面权限校验
{
path: '/about',
name: 'about',
component: AboutView,
meta:{isAuth:true}//需要权限校验就写这个
},
{
path: '/home',
name: 'home',
component: HomeView,
},
router.beforeEach((to,from,next)=>{//全局前置路由守卫,在切换之前进行调用+初始化的时候调用
//to:到去哪 from:从哪来 next:next() 放行
if(to.meta.isAuth){//根据meta是否有isAuth属性判断需不需鉴定权限
if(localStorage.getItem('username')==='张三'){//切换路径之前判断一下,本地存储里面用户名是否是张三,是就放行
next()
}else{
alert('权限不够')
}
}else{
next()
}
})
按钮鉴权自定义指令
<el-button v-btnlimit="'edit'">修改</el-button>
//首先登陆后会获得按钮权限,保存在vuex/session里面
//使用自定义指令,在inseted(插入到节点的时候判断)
//然后判断edit是否在数组里面显示,如果有就显示,没有就 el.parentNode.removeChild(el)
import Vue from 'vue';
Vue.directive('btnlimit', {
// 当被绑定的元素插入到 DOM 中时……
inserted: (el, binding) => {
// el 当前绑定的元素 binding.value指令的绑定值
let permissionList = sessionStorage.getItem('permission_button');
// 判断一下是否包含这个元素,如果不包含的话,那就让他爸爸元素把子元素扔进垃圾堆
if (!permissionList.includes(binding.value)) {
el.parentNode.removeChild(el)
}
}
})
// 大家可以把自己定义的指令写在一个directive.js文件中,在main.js总入口引入下就可以了,简单而不失优雅
在子组件标签上添加click事件不生效.native
某个组件的根元素上绑定事件,直接使用 @click=‘‘function’ 是不生效的,我们可以添加.native修饰符 @click.native=’‘function’'。
<div id="app">
<div class="box">
<Son @click='handlerFun'></Son>
</div>
</div>
<script>
const Son = Vue.component('Son', {
template: '',
methods: {}
})
new Vue({
el: "#app",
components: {
Son
},
methods: {
handlerFun() {
console.log('父级')
}
}
})
</script>
v2封装分页器
//PaginationView.vue分页器组件
<template>
<div class="pagination">
{{startNumAndEndnum}}
<button
:disabled="pageNo==1"
@click="$emit('getPageNo',pageNo-1)"
>上一页</button>
<button
v-if="startNumAndEndnum.start>1"
@click="$emit('getPageNo',1)"
:class="{active:pageNo==1}"
>1</button>
<button v-if="startNumAndEndnum.start>2">...</button>
<template v-for="(page,index) in startNumAndEndnum.end">
<button
:key="index"
@click="$emit('getPageNo',page)"
v-if="page>=startNumAndEndnum.start"
:class="{active:pageNo==page}"
>
{{page}}</button>
</template>
<template v-if="startNumAndEndnum.end< totalPage - 1">
<button>...</button>
</template>
<template v-if="startNumAndEndnum.end>
<button
@click="$emit('getPageNo',totalPage)"
:class="{active:pageNo==totalPage}"
>{{totalPage}}</button>
</template>
<button
:disabled="pageNo==totalPage"
@click="$emit('getPageNo',pageNo+1)"
>下一页</button>
<button>共{{total}}条</button>
</div>
</template>
<script>
// :pageNo="1" :total="91" :pageSize="3" (每页显示) :continues="5"(连着的)
export default {
data() {
return {};
},
props: ["pageNo", "total", "pageSize", "continues"],
computed: {
totalPage() {
//总共多少页
if (!Math.ceil(this.total ?? 0 / this.pageSize ?? 0)) {
return 0;
} else {
return Math.ceil(this.total / this.pageSize);
}
},
startNumAndEndnum() {
const { continues, totalPage, pageNo } = this;
let start = 0,
end = 0;
//连续页面是5,当不满5页
if (continues > totalPage) {
start = 1;
end = totalPage;
} else {
start = pageNo - parseInt(continues / 2);
end = pageNo + parseInt(continues / 2);
if (start < 1) {
start = 1;
end = continues;
}
if (end > totalPage) {
end = totalPage;
start = totalPage - continues + 1;
}
}
return { start, end };
},
},
};
</script>
<style lang="scss" scoped>
.pagination {
width: 100%;
height: 50px;
justify-content: center;
display: flex;
button {
width: 50px;
height: 50px;
border: 1px solid red;
margin: 0 10px;
}
}
.active {
background: pink;
}
</style>
//使用页面
<PaginationView
:pageNo="searchParams.pageNo"//当前页面
:total="searchList.total"//总数
:pageSize="searchParams.pageSize"//一页几个
:continues="5"//连续数字5个
@getPageNo="getPageNo"//点击接收的方法
></PaginationView>
methods:{
getPageNo(pageNo) {
//获取当前第几页
//整理参数
this.searchParams.pageNo = pageNo;
//再次发请求
this.getData();
},
}
路由拆分到一个单独js文件
//index.js
import routes from './routes'
//routes.js
import Home from '@/views/Home/HomeView.vue'
import Login from '@/views/Login/LoginView.vue'
export default [{
path:'/',
redirect:'/home'
},
{
path:'/home',
component:Home,
meta:{show:true},
},
{
path:'/detail/:id?',
component:Detail,
name:'detail',
meta:{show:true},
}]
无法找到模块“…”的声明文件。“…”隐式拥有 “any” 类型。
在src目录下新建一个types目录,然后在types 目录下新建一个 index.d.ts文件然后在文件中添加代码 declare module “第三方类库名”。
declare module '@antv/g6-editor';
要关闭any类型的警告
在 .eslintrc.js文件中找到rules 添加一行代码即可
"@typescript-eslint/no-explicit-any": ["off"]
vue使用Day.js库
官网
1 .下载
npm install dayjs
2.引入
const dayjs = require('dayjs')
//import dayjs from 'dayjs' // ES 2015
dayjs().format()
3.使用
//1.修改格式
dayjs().format()
// 默认返回的是 ISO8601 格式字符串 '2020-04-02T08:02:17-05:00'
dayjs('2019-01-25').format('DD/MM/YYYY') // '25/01/2019'
//2.时间差
const date1 = dayjs('2019-01-25')
const date2 = dayjs('2018-06-05')
date1.diff(date2) // 20214000000
date1.diff(date2, 'month') // 7
date1.diff(date2, 'month', true) // 7.645161290322581
date1.diff(date2, 'day') // 233
//3.一个时间在另一个时间之前
dayjs().isBefore(Dayjs, unit? : String);
dayjs().isBefore(dayjs()); // false
dayjs().isBefore(dayjs(), 'year'); // false
//4、一个时间是否等于另一个时间
dayjs().isSame(Dayjs, unit? : String);
dayjs().isSame(dayjs()); // true
dayjs().isSame(dayjs(), 'year'); // true
//5、一个时间在另一个时间之后。
dayjs().isAfter(Dayjs, unit? : String);
dayjs().isAfter(dayjs()); // false
dayjs().isAfter(dayjs(), 'year'); // false
vue ajax 组件,Vue.js中ajax请求代码应该写在组件的methods中还是vuex的actions中?
1.如果一个axios请求要多组件复用,就放到actions中
2.如果actions请求回来的数据要放到vuex中就放到actions中
3.如果只执行一次,或者不返回什么数据就放到页面中
或者
1.定义一个api来管理全部请求
2.在actions中发送请求
3.用promise来处理返回数据
页面try,catch接收 vuex 返回的请求状态
//store
//页面触发按钮,请求vuex接口,想要获取一个状态,如页面详情加入购物车,向后台发送请求,如果返回成功再跳转到购物车页面
actions:{
async addOrUpdateShopCart({commit},{skuId,skuNum}){
const result=await $axios.post("/cart/addToCart/"+skuId+"/"+skuNum)//会返回一个code:200的状态,并没有返回值
if(result.code==200){
return 'ok'
}else{
return Promise.reject(new Error('faile'))//如果不等于200可以抛出一个错误
}
},
}
//组件
<a @click="addShopcar">加入购物车</a>
methods: {
...mapActions("detail", ["addOrUpdateShopCart"]),
async addShopcar() {//使用try,catch来捕获状态,actions返回一个promise
try {
await this.addOrUpdateShopCart({//发送请求
skuId: this.$route.params.id,
skuNum: this.skuNum,
});
//在这做页面跳转+携带参数
//参数过多使用sessionStorage
} catch (error) {//捕获错误
alert(error.meaasge);
}
},}
vue3中reactive定义的引用类型直接赋值导致数据失去响应式
//这样直接赋值就会失去响应式
let data = reactive(['小猫', '小狗'])
data = reactive(['大猫', '大狗'])
//解决方法1 再套一个对象
let data = reactive({ animals: ['小猫', '小狗'] })
data.animals = ['大猫', '大狗']
//解决方法2 用ref定义
let data = ref(['小猫', '小狗'])
data.value = ['大猫', '大狗']
如何修改props传进来的值
方法一:v-model的另一种写法
<draggable :value="$store.state.vuexArr" @input="update($event)"> </draggable>
update($event){
//向后端发送请求,提交修改的数据
//axios.post('updata',{}).then((res)=>{commit修改数据})
this.$store.commit('updateVuexArr',$event)
}
方法二:使用官方使用的计算属性
<draggable v-model="vuexArr"></draggable>
computed: {
vuexArr: {
get() {
return this.$store.state.vuexArr
},
set(value) {
this.$store.commit('updateVuexArr', value)
}
}
}
方法三 使用emit
方法四 v-model
单例 生成全局唯一uuid,存到store里面
//uuid_token.js
import { v4 as uuidv4 } from 'uuid';
//生成的随机字符串不能发生变化,游客身份持久存储
export const getUUID=()=>{
//先从本地存储获取uuid
let uuid_token=localStorage.getItem('UUIDTOKEN')
//如果没有,就生成一个,只存储一次
if(!uuid_token){
uuid_token=uuidv4()
localStorage.setItem('UUIDTOKEN',uuid_token)
}
return uuid_token
}
import {getUUID} from '@/utils/uuid_token'//游客身份模块(生成随机uuid字符串)
export default{
namespaced:true,
state:{
goodInfo:{},
uuid_token:getUUID()//获取
},
v3使用@路径符(不全)
看链接
npm install @types/node --save-dev
1、tsconfig.json
2.vite.config.js
resolve: {
alias: {
'@':resolve(__dirname,'./src')
}
},
vue3 封装axios ts+api.js+vite
1.在src下新建http 文件夹 ,再新建index.ts
import axios from 'axios'
// http/index.ts
import axios from 'axios'
//创建axios的一个实例
var instance = axios.create({
// baseURL: import.meta.env.VITE_RES_URL, //接口统一域名
timeout: 6000, //设置超时
headers: {
'Content-Type': 'application/json;charset=UTF-8;',
}
})
//请求拦截器
instance.interceptors.request.use((config: any) => {
// 每次发送请求之前判断是否存在token,如果存在,则统一在http请求的header都加上token,不用每次请求都手动添加了
const token = window.localStorage.getItem('token');
token && (config.headers.Authorization = token)
//若请求方式为post,则将data参数转为JSON字符串
if (config.method === 'POST') {
config.data = JSON.stringify(config.data);
}
return config;
}, (error) =>
// 对请求错误做些什么
Promise.reject(error));
//响应拦截器
instance.interceptors.response.use((response) => {
//响应成功
console.log('响应成功');
return response.data;
}, (error) => {
console.log(error)
//响应错误
if (error.response && error.response.status) {
const status = error.response.status
console.log(status);
return Promise.reject(error);
}
return Promise.reject(error);
});
export default instance;
2.再http下新建axios.ts
import instance from "./index"
/**
* @param {String} method 请求的方法:get、post、delete、put
* @param {String} url 请求的url:
* @param {Object} data 请求的参数
* @param {Object} config 请求的配置
* @returns {Promise} 返回一个promise对象,其实就相当于axios请求数据的返回值
*/
const axios = async ({
method,
url,
data,
config
}: any): Promise<any> => {
method = method.toLowerCase();
if (method == 'post') {
return instance.post(url, data, { ...config })
} else if (method == 'get') {
return instance.get(url, {
params: data,
...config
})
} else if (method == 'delete') {
return instance.delete(url, {
params: data,
...config
})
} else if (method == 'put') {
return instance.put(url, data, { ...config })
} else {
console.error('未知的method' + method)
return false
}
}
export {
axios
}
3.在src 下新建api 文件夹 ,在新建api.ts
import { axios } from "../http/axios"
//敏感词校验
export const getUser = (data: any) => {
return axios({
url: "/getUser",
data,
method: "get",
config: {
// headers: {
// 'Request-Type': 'wechat'
// },
timeout: 10000
}
})
}
4.页面使用
<script setup lang="ts">
import { getUser } from "../api/index";
const s = await getUser({
text: "里斯",
});
console.log(s);
</script>
配置跨域vite+ts+v3
vite.config.js
server: {
proxy: {
'/api': {
target: 'http://seec.xinyuefei.com',
changeOrigin: true,//v3里面必须设置
rewrite: (path) => path.replace(/^\/api/, '')//让路径没有api
},
}
}
过渡效果列表增加删除换位置,有过渡效果
<script setup>
import { shuffle as _shuffle } from 'lodash-es'
import { ref } from 'vue'
const getInitialItems = () => [1, 2, 3, 4, 5]
const items = ref(getInitialItems())
let id = items.value.length + 1
function insert() {
const i = Math.round(Math.random() * items.value.length)
items.value.splice(i, 0, id++)
}
function reset() {
items.value = getInitialItems()
}
function shuffle() {
items.value = _shuffle(items.value)
}
function remove(item) {
const i = items.value.indexOf(item)
if (i > -1) {
items.value.splice(i, 1)
}
}
</script>
<template>
<button @click="insert">insert at random index</button>
<button @click="reset">reset</button>
<button @click="shuffle">shuffle</button>
<TransitionGroup tag="ul" name="fade" class="container">
<div v-for="item in items" class="item" :key="item">
{{ item }}
<button @click="remove(item)">x</button>
</div>
</TransitionGroup>
</template>
<style>
.container {
position: relative;
padding: 0;
}
.item {
width: 100%;
height: 30px;
background-color: #f3f3f3;
border: 1px solid #666;
box-sizing: border-box;
}
/* 1. 声明过渡效果 */
.fade-move,
.fade-enter-active,
.fade-leave-active {
transition: all 0.5s cubic-bezier(0.55, 0, 0.1, 1);
}
/* 2. 声明进入和离开的状态 */
.fade-enter-from,
.fade-leave-to {
opacity: 0;
transform: scaleY(0.01) translate(30px, 0);
}
/* 3. 确保离开的项目被移除出了布局流
以便正确地计算移动时的动画效果。 */
.fade-leave-active {
position: absolute;
}
</style>
v3 element plus 使用messagebox
//main.js
import "element-plus/theme-chalk/index.css";
全局挂载
v3 element plus 引入icon vite
npm i @iconify-json/ep -D
npm i unplugin-vue-components unplugin-icons unplugin-auto-import -D
github
<i-ep-edit />
//vite.config.js
import path from 'path'
import { defineConfig } from 'vite'
import Vue from '@vitejs/plugin-vue'
import Icons from 'unplugin-icons/vite'
import IconsResolver from 'unplugin-icons/resolver'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import Inspect from 'vite-plugin-inspect'
const pathSrc = path.resolve(__dirname, 'src')
export default defineConfig({
resolve: {
alias: {
'@': pathSrc,
},
},
plugins: [
Vue(),
AutoImport({
// Auto import functions from Vue, e.g. ref, reactive, toRef...
// 自动导入 Vue 相关函数,如:ref, reactive, toRef 等
imports: ['vue'],
// Auto import functions from Element Plus, e.g. ElMessage, ElMessageBox... (with style)
// 自动导入 Element Plus 相关函数,如:ElMessage, ElMessageBox... (带样式)
resolvers: [
ElementPlusResolver(),
// Auto import icon components
// 自动导入图标组件
IconsResolver({
prefix: 'Icon',
}),
],
dts: path.resolve(pathSrc, 'auto-imports.d.ts'),
}),
Components({
resolvers: [
// Auto register icon components
// 自动注册图标组件
IconsResolver({
enabledCollections: ['ep'],
}),
// Auto register Element Plus components
// 自动导入 Element Plus 组件
ElementPlusResolver(),
],
dts: path.resolve(pathSrc, 'components.d.ts'),
}),
Icons({
autoInstall: true,
}),
Inspect(),
],
})
promise.all 循环请求多个次
//store
deleteCartListBySkuId(){}//删除单个
//实现删除多个,需要多次调用删除单个的函数
deleteAllCheckedCart({dispatch,getters}){//删除全部勾选的
let PromiseAll=[]
getters.cartList.cartInfoList.forEach((item)=>{
let promise= item.isChecked==1? dispatch('deleteCartListBySkuId',item.skuId):''
PromiseAll.push(promise)
})
return Promise.all(PromiseAll)
//有一个错就全错,都对才对
}
//页面使用
//删除全部选中的产品
async deleteAllChecked() {
try {
await this.deleteAllCheckedCart();
this.getData();
} catch (error) {
alert(error);
}
},
后台管理系统路由面包屑+tags删除添加
使用element面包屑+tag实现,在vuex中配置一个数组用来存放路径设置。
1.首页是不可以删除的
2.添加:点击路由如果数组没有就添加,有就切换到那个页面
3.删除:用splice删除点击判断是否是最后一个选项,如果是最后一个,就展示前一个。不是最后一个就展示当前删除的后一个
4.页面使用watch监听route变化。使用router.push()实现跳转
v3+ts代码.
//store
routerArr: ["/home"],
//vue
<template>
<ul>
<li v-for="item,i in routerArr" @click="app(item,i)" :class="{active:i==show}">
{{item}}<span @click.stop="del(i)">X</span></li>
</ul>
</template>
<script setup lang="ts">
import { useRoute, useRouter } from 'vue-router';
import { watch, ref } from 'vue';
import { useMainStore } from '../store'
import { storeToRefs } from 'pinia';
const mainStoreI = useMainStore()
const { routerArr } = storeToRefs(mainStoreI)
const router = useRouter()
const route = useRoute()
let show = ref(0)
watch(() => router.currentRoute.value.path, (newValue, oldValue) => {
show.value =routerArr.value.findIndex(i=>i==newValue)
if (routerArr.value.indexOf(newValue) == -1 && newValue !== '/') {
routerArr.value.push(newValue)
show.value = routerArr.value.length - 1
}
}, { immediate: true })
const del = (i: number) => {
if (i == show.value) {
if (i === routerArr.value.length - 1) {
routerArr.value.splice(i, 1)
router.push(routerArr.value[routerArr.value.length-1])
show.value = routerArr.value.length-1
} else {
routerArr.value.splice(i, 1)
router.push(routerArr.value[i])
}
} else if (i < show.value) {
routerArr.value.splice(i, 1)
show.value = i
} else if (i > show.value) {
routerArr.value.splice(i, 1)
}
}
const app = (item: string, i: number) => {
router.push(item)
show.value = i
}
</script>
<style scoped>
.active {
color: red;
}
</style>
vue3文件上传下载
<template>
<p v-if="info.file_name">
<a
:href="info.file_url"
:download="info.file_name"
>{{ info.file_name }}</a>
<el-button type="primary">删除</el-button>
</p>
<p v-else>
<el-button type="primary" @click="onClick">上传文件</el-button>
<input
ref="file"
v-show="false"
type="file"
@change="onChange"
/>
</p>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import type { Info } from '@/assets/api';
import { ElMessage } from 'element-plus';
import { uploadFile } from '@/assets/api';
const props = defineProps<{
info: Info
}>();
const emit = defineEmits(['update']);
const check = (file: File) => {
const { name, size } = file;
let msg = '';
if (size / 1024 / 1024 > 10) {
msg = '文件大小不能大于10M';
} else {
const type = name.split('.').pop();
if (type && !['js', 'png', 'jpg'].includes(type)) {
msg = '文件类型不正确';
}
}
if (msg) ElMessage.error(msg);
return !msg;
};
const upload = (file: File) => {
const formData = new FormData();
formData.append('file', file);
formData.append('id', props.info.id + '');
uploadFile(formData).then(() => emit('update'));
};
const file = ref<HTMLInputElement | null>(null);
const onChange = (e: Event) => {
const { files } = e.target as HTMLInputElement;
// const file = (files as FileList)[0];
if (files) {
const file = files[0];
check(file) && upload(file);
}
};
const onClick = () => file.value?.click();
</script>
使用vee-validate vue表单验证
npm i vee-validate@2 --save
使用2版本,3版本有一些困难
1.创建文件
//src/plugins/validate.js
//npm i vee-validate@2 --save
import Vue from 'vue'
import VeeValidate from 'vee-validate';
//中文提示信息
import zh_CN from 'vee-validate/dist/locale/zh_CN';
Vue.use(VeeValidate)
//表单验证规则
VeeValidate.Validator.localize('zh_CN', {
messages: {
...zh_CN.messages,
is: (field) => `${field}必须和密码相同`
},
attributes: { //给校验的field属性名映射中文名称
phone: '手机号',
code1: '验证码',
password: '密码',
password1: '确认密码',
agree: '协议'
}
})
//自定义校验规则,比如协议勾选项
//tongyi校验规则的名字
VeeValidate.Validator.extend('tongyi', {
validate: value => {
return value
},
getMessage: field => field + '必须同意'
})
main.js中引入
import ‘@/plugins/validate.js’; //表单验证
使用页面
//输入框
<input
type="text"
placeholder="请输入你的手机号"
v-model="phone"
name="phone"
v-validate="{required:true,regex:/^1\d{10}$/}"
>
<span class="error-msg">{{errors.first('phone')}}</span>
//密码框
<input
type="text"
placeholder="请输入你的密码"
v-model="password"
name="password"
v-validate="{required:true,regex:/^[0-9A-Za-z]{8,20}$/}"
>
<span class="error-msg">{{errors.first('password')}}</span>
//密码再次确认
<input
type="text"
placeholder="请输入确认你的密码"
v-model="password1"
name="password1"
v-validate="{required:true,is:password}"
:class="{invalid:errors.has('password1')}"
>
<!-- is:password 判断两次密码相同-->
<span class="error-msg">{{errors.first('password1')}}</span>
//确认勾选框(需要自定义校验)
<input
type="checkbox"
v-model="agree"
name="agree"
v-validate="{required:true,'tongyi':true}"
:class="{invalid:errors.has('agree')}"
>
<span>同意协议并注册《尚品汇用户协议》</span>
<span class="error-msg">{{errors.first('agree')}}</span>
//都满足条件再发送请求
async userRegiser1() {
const success = await this.$validator.validateAll(); //判断是否都满足条件
if (success) {
//都满足再发送请求
try {
this.phone &&
this.code1 &&
this.password == this.password1 &&
(await this.userRegister({
phone: this.phone,
code: this.code1,
password: this.password,
}));
this.$router.push("/login");
} catch (error) {
alert(error);
}
}
},
后台管理系统模板github
简单版
复杂版
在这里插入代码片
v3+ts+scss 修改主题颜色
//theme.scss
//.el-header,.el-footer 的背景色
$background-main-color1:#3b69d6; //背景色
$background-main-color2:#209F5C; //背景色
$background-main-color3:#283444; //背景色
$background-main-color4:#c73c27; //背景色
//style.scss
//可以写颜色由于多个页面避免重复的样式编写
@import "./theme.scss"; //引入声明的皮肤文件
//初次进入调用
@mixin background-main-color($color){ //@mixin 后面的函数名称为自定义。
background-color: $color; //背景色默认为参数
[background-main-color="background-main-color2"] & { //如果条件成立,背景色则用$background-main-color2
background-color: $background-main-color2; //这个$background-main-color2已经在theme.scss中定义过了。
}
[background-main-color="background-main-color3"] & {
background-color: $background-main-color3;
}
[background-main-color="background-main-color4"] & {
background-color: $background-main-color4;
}
}
3.页面使用
<style scoped lang="scss">
.el-header,.el-footer{
@include background-main-color($background-main-color1);
}
</style>
4.切换背景
defaultTheme(command){
if(command == "blue") {window.document.documentElement.setAttribute("background-main-color","background-main-color1");}
else if(command == "green") {window.document.documentElement.setAttribute("background-main-color","background-main-color2");}
else if(command == "gray") {window.document.documentElement.setAttribute("background-main-color","background-main-color3");}
else { window.document.documentElement.setAttribute("background-main-color","background-main-color4"); }}
重置路由方法
//router/index.js
const createRouter = () => new Router({
scrollBehavior: () => ({ y: 0 }),
routes: constantRoutes
})
const router = createRouter()//重置路由
export function resetRouter() {
const newRouter = createRouter()
router.matcher = newRouter.matcher // reset router
}
export default router
addRoute 动态添加路由
添加链接描述
router.beforeEach里面先判断是否添加过路由,然后再添加路由
import store from '@/store'
//这里我用vuex的一个变量 asyncRoutestMark 来标识是否拼接过路由
router.beforeEach((to, from, next) => {
if (!store.state.asyncRoutestMark) {
// navigationList 是上面模拟接口返回的数据
// 这里将新的路由都作为 home 的子路由(实际开发根据情况)
// meta 是存储一些信息,可以用于权限校验或其他
navigationList.forEach( navigation => {
router.addRoute('home', {
path: navigation.url,
meta: { name: navigation.name, isAsync: true, icon: navigation.icon },
name: menu.url,
component: () => import(`../views/${menu.url}`)
})
})
console.log(router.getRoutes(), '查看现有路由')
store.commit('setAsyncRoutestMark', true) // 添加路由后更改标识为true
next({ ...to, replace: true }) //路由进行重定向放行
} else {
next()
}
})
navigationList : [
{
id: 1,
icon: 'icon-jurassic_user',
name: '用户管理',
url: '/user'
},
{
id: 2,
icon: 'icon-jurassic_user',
name: '角色管理',
url: '/role'
},
{
id: 3,
icon: 'icon-shebei',
name: '设备管理',
url: '/device'
}
]
v2侧边栏动态渲染
添加链接描述
//父组件
<template>
<div class="bg">
<div class="main">
<el-menu
background-color="#304156"
@open="handleOpen"
@close="handleClose"
class="el-menu-vertical-demo"
text-color="#BFCBD9"
router
>
<SideBarItem :routes="routes"></SideBarItem>
</el-menu>
</div>
</div>
</template>
<script>
import { routes } from "@/router/index";
import SideBarItem from "./SideBarItem.vue";
export default {
name: "SideBar",
data() {
return {
routes,
};
},
methods: {
clickMenu() {},
handleOpen(key, keyPath) {
console.log(key, keyPath);
},
handleClose(key, keyPath) {
console.log(key, keyPath);
},
},
components: {
SideBarItem,
},
};
</script>
<style lang="scss" scoped>
.el-menu-vertical-demo:not(.el-menu--collapse) {
width: 200px;
height: 100%;
position: fixed;
top: 50px;
left: 0;
bottom: 0;
}
</style>
//TreeItem.vue
//递归的子组件
<template>
<div class="main">
<template v-for="(item, index) in routes">
<!-- 首页 -->
<div :key="index" v-if="item.name === '1'">
<el-menu-item :index="item.path">
<i class="el-icon-menu"></i>
<span slot="title">{{ item.meta.title }}</span>
</el-menu-item>
</div>
<!-- 没有子路由 -->
<div :key="index" v-if="!item.children">
<el-menu-item :index="item.path">
<i class="el-icon-menu"></i>
<span slot="title">{{ item.meta.title }}</span>
</el-menu-item>
</div>
<!-- 有子路由把home排除的 -->
<div :key="index" v-if="item.children && item.name !== '1'">
<el-submenu :index="item.path">
<template slot="title">
<i class="el-icon-menu"></i>
<span>{{ item.meta.title }}</span>
</template>
<!-- 二级子路由遍历 -->
<template v-for="child in item.children">
<!-- 无子路由 -->
<div :key="child.meta.title" v-if="!child.children">
<el-menu-item :index="item.path + '/' + child.path">{{
child.meta.title
}}</el-menu-item>
</div>
</template>
</el-submenu>
</div>
</template>
</div>
</template>
<script>
export default {
name: "SideBarItem",
props: {
routes: {
type: Array,
default: () => {},
},
},
data() {
return {};
},
};
</script>
vue中使用element-resize-detector
init 初始化setOption()挂载
添加链接描述
这是一个用于监听DOM元素尺寸变化的插件。我们已经对窗口缩放做了监听,但是有时候其父级容器的大小也会动态改变的。
我们对父级容器的宽度进行监听,当父级容器的尺寸发生变化时,echart能调用自身的resize方法,保持视图正常。
当然,这个不适用于tab选项卡的情况,在tab选项卡中,父级容器从display:none到有实际的clientWidth,可能会比注册一个resizeDetector先完成,所以等开始监听父级容器resize的时候,可能为时已晚。
解决这个问题,最有效的方法还是在切换选项卡时手动去通过ref获取echart实例,并手动调用resize方法,这是最安全的,也是最有效的。
解决没有触发window.onresize的问题
在echart 页面大小发生改变,设置了resize方法,但是图表没有改变的时候使用
1.下载
npm install element-resize-detector --save
2.引入
import resizeDetector from 'element-resize-detector'
3.使用
import resizeDetector from 'element-resize-detector'
export default {
mounted() {
this.chartResize()
},
methods: {
chartResize() {
let erd = resizeDetector()
erd.listenTo(this.$el, () => {
this.chart.resize()
console.log('chart resize')
})
}
}
}
//要移除
beforeDestroy () {
window.removeEventListener('resize', this.chartResize)
},
【vue】+【Echarts】+【element-resize-detector】通过自定义指令实现图表自适应
适配大屏幕的EChart
解决 他图表变大,但是字体太小的问题
1.使用移动端的方法求出字体大小(1080/12=>2160/24)
2.使用element-resize-detector对父级容器的宽度进行监听,当父级容器的尺寸发生变化时,echart能调用自身的resize方法,保持视图正常。
普通响应式echart
在有些场景下,我们希望当容器大小改变时,图表的大小也相应地改变。
比如,图表容器是一个高度为 400px、宽度为页面 100% 的节点,你希望在浏览器宽度改变的时候,始终保持图表宽度是页面的 100%。
这种情况下,可以监听页面的 window.onresize 事件获取浏览器大小改变的事件,然后调用 echartsInstance.resize 改变图表的大小。
<style>
#main,
html,
body {
width: 100%;
}
#main {
height: 400px;
}
</style>
<div id="main"></div>
<script type="text/javascript">
var myChart = echarts.init(document.getElementById('main'));
window.onresize = function() {
myChart.resize();
};
</script>
vue2封装三级下拉框联动
<template>
<div>
<el-form :inline="true" class="demo-form-inline" :model="cForm">
<el-form-item label="一级分类">
<el-select
placeholder="请选择"
v-model="cForm.coategory1Id"
@change="handler1"
:disable="show"
>
<el-option
:label="c1.name"
:value="c1.id"
v-for="c1 in list1"
:key="c1.id"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="二级分类">
<el-select
placeholder="请选择"
:disable="show"
v-model="cForm.coategory2Id"
@change="handler2"
>
<el-option
:label="c2.name"
:value="c2.id"
v-for="c2 in list2"
:key="c2.id"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="三级分类">
<el-select
placeholder="请选择"
:disable="show"
v-model="cForm.coategory3Id"
@change="handler3"
>
<el-option
:label="c3.name"
:value="c3.id"
v-for="c3 in list3"
:key="c3.id"
></el-option>
</el-select>
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
name: "categorySelect",
data() {
return {
//一级分类
list1: [],
list2: [],
list3: [],
cForm: {
coategory1Id: "",
coategory2Id: "",
coategory3Id: "",
},
};
},
props: {
show: {
type: Boolean,
},
},
mounted() {
//获取一级分类数据的方法
this.getCategory1List();
},
methods: {
async getCategory1List() {
let result = await this.$API.attr.reqCategory1List();
if (result.code == 200) {
this.list1 = result.data;
}
},
async handler1() {
//一级分类事件回调
this.list2 = [];
this.list3 = [];
this.cForm.coategory2Id = "";
this.cForm.coategory3Id = "";
const { coategory1Id } = this.cForm;
this.$emit("getCategoryId", { categoryId: coategory1Id, level: 1 });
let result = await this.$API.attr.reqCategory2List(coategory1Id);
if (result.code == 200) {
this.list2 = result.data;
}
},
async handler2() {
//2级分类事件回调
this.list3 = [];
this.cForm.coategory3Id = "";
const { coategory2Id } = this.cForm;
this.$emit("getCategoryId", { categoryId: coategory2Id, level: 2 });
let result = await this.$API.attr.reqCategory3List(coategory2Id);
if (result.code == 200) {
this.list3 = result.data;
}
},
handler3() {
const { coategory3Id } = this.cForm;
this.$emit("getCategoryId", { categoryId: coategory3Id, level: 3 });
},
},
};
</script>
使用
<category-select
@getCategoryId="getCategoryId"
:show="!isShowTable"
></category-select>
getCategoryId({ categoryId, level }){//id,第几级
}
输入input span切换,可以给obj的每一项添加一个属性
根据添加的flag属性判断是显示还是隐藏,后面提交数据的时候delete删除这个属性,
vue2中清空data对象,变为初始化
//表单提交完成后,清空数据
Object.assign(this._data, this.$options.data());
// this.$options.data()执行完成后,返回初始化data数据,赋值给data
Vue打开新页面
//方法一
handleRouterBlank(val) {
let {href} = this.$router.resolve({
name: val
});
window.open(href, '_blank');
},
//router-link添加 tag="a"
<router-link tag="a" target="_blank" :to="{name:'detail',query:{goodsId:'1111'}}">热门好货</router-link>
//给a标签动态设置href路径,然后主动触发click事件来跳转。
就是在页面添加一个a标签,款高设为零或者隐藏,只要看不到就行,设置好target属性为_blank,href设为空
<a class="target" href="" target="_blank"></a>
给目标元素绑定@click=“test”事件,当点击目标元素的时候触发:
test() {
let target = this.$refs.target
target.setAttribute('href', window.location.origin + '/home/integral-record')
target.click()
}
vue3 deep 样式穿透 element-plugs
:deep(.el-form-item__content) {
flex-wrap: nowrap;
}
vue3使用md5
npm install --save js-md5
npm i --save-dev @types/js-md5
//需要这么引入
const md5 = require("js-md5");
md5('hello')
分页删除/修改一条数据,留在当前页
<el-pagination
:current-page="page"
@current-change="getSkuList"
>
</el-pagination>
data() {
return {
page: 1,
};
},
async getSkuList(pages = 1) {
this.page = pages;
//获取spu列表
const { page, limit, category3Id } = this;
let result = await this.$API.spu.reqSpuList(page, limit, category3Id);
},
//修改数据
getSpuList()//如果不传值就用默认值1 ,请求第一页
getSpuList(2)//传值就用
//如果是删除就需要判断,最后一页还有东西么,要是都没了,就请求上一页
getSkuList(this.records.length>1?this.page:this.page-1);
vue 动态设置主题颜色,切换主题
element-ui更改主题
效果
<div data-theme="dark"></div>
在该节点的父节点切换data-theme属性,即可实现主题切换。
//新建 theme.scss 文件。
//主题参数
$themes: (
default: (
color: #000000,
bg_color: #FFFFFF,
border_color: #000000
),
colorful: (
color: #EE6666,
bg_color: #9DD3E8,
border_color: #879BD7
)
);
//生成对应元素的主题样式代码
@mixin theme {
@each $themes-key, $themes-map in $themes {
$themes-map: $themes-map !global;
[data-theme=#{$themes-key}] & {
@content;
}
}
}
//获取对应的主题数据
@function t($key){
@return map-get($themes-map, $key);
};
<template>
<div class="container" :data-theme="theme">
<p class="texture">
<span @click="theme = theme == 'default'?'colorful':'default'">Hello World!</span>
</p>
</div>
</template>
<script>
export default {
data() {
return {
theme: 'default'
};
}
}
</script>
<style lang="scss" scoped>
@import '@/assets/styles/theme.scss';
.texture{
padding: 10rem 0 0;
text-align: center;
span{
display: inline-block;
padding: 10px;
font-size: 40px;
border-radius: 10px;
cursor: pointer;
transition: .5s all;
@include theme{
color: t('color');
background-color: t('bg_color');
border: 1px solid t('border_color');
}
}
}
</style>
element-ui tree组件没办法获取到父节点的id
需要修改源码
修改源码
不修改源码
tree 只获取到子节点的id
我只想获取9,10
this.$refs.tree.getCheckedKeys(true)
//true只返回子节点的id
vue tree组件把有子属性所有的展开
//默认只展示2级,这样设置完全部都展示
add(item) {
for (let i = 0; i < item.length; i++) {
if (item[i].children !== undefined) {
this.node.push(item[i].id);
this.add(item[i].children);
}
}
},
vue tree 只能单选
<el-tree
:data="data"
show-checkbox
node-key="id"
ref="tree"
:props="defaultProps"
@check-change="handleCheckChange"
>
</el-tree>
listQuery: {
menuId: 0,
},
handleCheckChange(data, checked) {
if (checked) {
this.listQuery.menuId = data.id;
this.$refs.tree.setCheckedKeys([data.id], true);
console.log(this.listQuery.menuId);
} else {
this.listQuery.menuId = null;
}
},
},
vue 刷新页面,实现滚动条还停留在上次访问的位置
vue用户在列表页进入详情之后,回来还在之前的浏览位置
使用keep-alive
方法1
方法2
使用scrollBehavior :区别主要是针对#app元素的,其他元素滚动就用自定义的吧
{
path: '/',
name: 'home',
component: Home,
meta: {
keepAlive: true // 需要缓存
}
}
<keep-alive>
<router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
<router-view v-if="!$route.meta.keepAlive"></router-view>
//页面使用
data: {
box: '',
scrollY: ''
}
mounted: {
// 监听scroll变化
this.$nextTick(()=>{
this.box = document.querySelector('.recordContent')
this.box.addEventListener('scroll', function(){
this.scrollY = document.querySelector('.recordContent').scrollTop
}, false)
})
},
beforeRouteEnter (to, from, next) {
next(vm => {
//因为当钩子执行前,组件实例还没被创建
// vm 就是当前组件的实例相当于上面的 this,所以在 next 方法里你就可以把 vm 当 this 来用了。
console.log(vm);//当前组件的实例
// 为div元素重新设置保存的scrollTop值
document.querySelector('.recordContent').scrollTop = vm.scrollY
});
},
//记录离开时的位置
beforeRouteLeave (to, from, next) {
//保存滚动条元素div的scrollTop值
this.scrollY = document.querySelector('.recordContent').scrollTop
next()
},
瀑布流的封装V3+ts
原文链接
富文本tinymce
原文
富文本simplemde-markdown-editor
github链接
富文本vue-quill-editor
添加链接描述
使用 el-upload 作为上传组件
默认情况下,此组件隐藏
点击 vue-quill-editor 中的图片按钮时,触发 el-upload 组件的单击事件,打开文件选择框
上传成功后,获取图片地址,插入到光标处
富文本wangeditor vue
Vue3使用
添加链接描述
//wangeditor自定义的图片上传设置
[添加链接描述](https://www.wangeditor.com/v5/getting-started.html)
instance.config.uploadFileName = "file";//修改图片上传的属性名
instance.config.uploadImgHooks = {//返回的参数数据自定义
customInsert: function (insertImgFn: any, result: any) {
// result 即服务端返回的接口
console.log("customInsert", result);
// insertImgFn 可把图片插入到编辑器,传入图片 src ,执行函数即可
insertImgFn(result.data);
},
};
vue更改document.title 浏览器的 title 跟随路由的名称变化
添加链接描述
1.先设置一个公共文件setting.js
2.路由引入setting设置路由拦截beforEach,document.title=“xxx”
3.在 vue.config.js 中设置 name属性和 public/index.html中的title
vue2 使用国际化i18n
vue2使用i18n
elementui+i18n
下载@8的版本 “vue-i18n”: “^8.26.7”,
先创建一个一个文件夹存放语言的配置文件(inex.js,zh.js,en.js)
index.js中引入elementui的语言文件,和zh/en.js的文件,合并所有语言文件。设置默认语言(从本地存储中获取)
main.js中修改成Vue.use(Element,{i18n:})
页面中展示{{$ t(‘message’)}},切换语言环境this.$i18n.locale=‘zh’/‘en’
navigator.language;可以获取浏览器使用的语言环境
如果数据是动态获取的就把用到的全写上,用属性的方式 $ t(info.name)查找渲染
剪贴板功能vue2
npm install --save vue-clipboard2
在 main.js 中引入
import VueClipboard from 'vue-clipboard2'
Vue.use(VueClipboard)
<span>{{msg}}</span>
<img src="../../static/img/d1.png"
@click="handleFun"
v-clipboard:copy="msg"
v-clipboard:success="copy"
v-clipboard:error="onError">
data() {
return {
msg: ''
}
},
methods: {
copy(e) {
console.log(e.text);
},
onError(e) {
console.log(e);
},
handleFun () {
this.msg = '8Z0W'
}
}
下载base64的图片保存到本地vue2
借助canvas
//或者
<a download="bbbbb.jpg" href="data:image/jpg;base64,iVBORw..." >下载</a>
vue 大文件上传,如何监听文件上传进度——切片上传(1)
如何监听上传进度,使用progress
1.先用input file进行上传,@change监听
2.e.target.files[0]拿到上传文件的name,size,type
3.对文件进行一个切割按照1兆,2M兆为一个基数,file.slice(start,start+基数)
4.循环上传每一片,使用创建new FromDate对象上传,fromDate.append(new File)传递每一片加索引值参数。更新propgress的百分比
如果是控制并发利用(async-pool分为es6/7版本),传递三个参数(并发数,任务数组)Promise.all 和 Promise.race 函数特点,再结合 ES7 中提供的 async await 特性,
文件上传(2)通过Blob实现大文件切片上传,通过async-pool控制并发限制
async-pool
vue2文件下载使用blob
downloadFile(id) {
this.downloading = true
downloadVideo(id).then((res) => {
// type是文件类,详情可以参阅blob文件类型
let blob = new Blob([res], { type: 'video/mp4' })
let objectUrl = URL.createObjectURL(blob)// 生成下载链接
let a = document.createElement('a')// 创建a标签用于文件下载
a.href = objectUrl// 赋值下载路径
a.download = Math.random().toString(36).slice(-6) + '.mp4'// 下载的文件名称(非必填)
document.body.appendChild(a)// 插入DOM树
a.click()// 点击a标签触发
document.body.removeChild(a)// 删除a标签
this.downloading = false
})
},
blob
Value
blob
表示二进制大对象,专门存放二进制数据 var blob = new Blob([“Hello World!”],{type:“text/plain”});
FormData
FormData我们可以异步上传一个二进制文件,而这个二进制文件,就是我们上面讲的Blob对象。
评论
websocket 发送和接收消息
重连和心跳
添加链接描述
vue-cli中使用腾讯TcPlayer播放器
vue 三级联动
页面初始请求第一个搜索框,监听chang事件获取下一个。
把三级联动封装成全局组件,
页面使用 <category-select @getCategoryId="getCategoryId" ></category-select>
getCategoryId({ categoryId, level }) {}//传回来一个对象categoryId是id,level 是第几层
//category-select 组件内部
this.$emit("getCategoryId", { categoryId: coategory2Id, level: 2 });
echarts 数据堆叠
{
name: '视频广告',
type: 'bar',
stack: 'overlap1',//堆叠效果(字符需要统一)
data: [150, 232, 201, 154, 190, 330, 410]
},
{
name: '百度',
type: 'bar',
stack: 'overlap1',//堆叠效果(字符需要统一)
data: [620, 732, 701, 734, 1090, 1130, 1120]
},
由折线图可以看到,华夏的实际值为932,但在图表中的点值在1837左右,说明数据堆叠
去掉series中stack属性,或者将stack设置为不同的值
生成word文档
element -UI el-dropdown 单独绑定@click无效
@click.native=“seeTable” 绑定
<el-dropdown>
<span class="el-dropdown-link" ref="echarType">
柱状图<i class="el-icon-arrow-down el-icon--right"></i>
</span>
<el-dropdown-menu slot="dropdown" >
<el-dropdown-item>柱状图</el-dropdown-item>
<el-dropdown-item @click.native="seeTable">表格</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
element-ui v2密码输入框小眼睛
主要实现根据一个变量控制图标的不同和type=password和text的效果
添加链接描述
百度地图画出区域并获取区域坐标范围
添加链接描述
vue 复制功能,剪贴板
copyCoordinate() {
//点击经纬坐标复制
navigator.clipboard
.writeText(document.getElementById("coordinate").value)
.then(() => {
this.$message.success("复制成功");
});
},
百度地图问题BMap is not defined
在mounted初始化地图的时候,因为异步问题会导致BMap is not
defined,也就是百度的api还没完全引入或者加载完成,就已经进行地图初始化了
添加链接描述
图片保存在阿里ali-oss
import { client, getFileNameUUID } from "../../../../static/js/ali-oss.js"; //前面的ali-js文件内的两个封装函数
echarts 数值接近怎么让他更明显
yAxis: {
type: 'value',
scale: true//这个控制
},
小程序引入vr链接
使用 webview
swiper 下面的图片存在加载慢的问题
如果swiper 小程序图片过于多,有可能存在底下图片加载缓慢
导出按钮导出文件
exportBtn() {
this.axios
.post(
this.websiteUrl2 +
"/pcbackend/web/api/SpecialInspectionExport/exportSpecialInspectionList",
this.taizhang_canshu
)
.then(res => {
if (res.data.code == 200) {
this.exportHref = res.data.data;
this.downloadImg();
}
})
.catch(function(error) {
console.log(error, "直接走error,");
});
},
//导出下载地址
downloadImg() {
let alink = document.createElement("a");
alink.href = this.exportHref;
alink.download = "teamplte"; //文件名
alink.click();
},
echart 节点点击事件
mychartsale.on('click', function (param) {
console.log(param);//这里根据param填写你的跳转逻辑
}
背景图片居中
background: #000 url('../images/banner-1.jpg') no-repeat;
width: 100%;
height: 600px;
background-position: center 0;
display: block;
position: relative;
z-index: 10;
top: -47px;
overflow: hidden;
element -plugs checkbox 点击2次问题
if (e.target.tagName === "INPUT") return;
element -plugs message被el-dialog遮住
给el-dialog设置属性,官网有,给他设置一个class类名然后全局设置类名的z-index
或者不能有scope