前端自学Vue笔记干货(第一版,持续更新中~~~)

学习笔记

Vue笔记

nprogress使用
npm i nprogress -S
基本上都是在对axios进行二次封装、前置守卫路由或者封装成工具函数的.js文件中用到
import nprogress from 'nprogress'
//引入进度条的样式
import 'nprogress/nprogress.css'
//start:进度条开始 done:进度条结束

//进度条开始动
  nprogress.start()
//进度条结束
  nprogress.done()
法一:
// 显示全屏loading
export function showFullLoading(){
  nprogress.start()
}

// 隐藏全屏loading
export function hideFullLoading(){
  nprogress.done()
}
法二:
import router, { asyncRoutes } from './router'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style
import store from '@/store'
import getPageTitle from '@/utils/get-page-title'

NProgress.configure({ showSpinner: false }) // NProgress Configuration

const whiteList = ['/login', '/404'] // 白名单: 无需登录, 可以跳转查看的路由地址(在路由表里)

// 问题: 为何动态路由添加后, 在动态路由地址上刷新会404?
// 前提1: 刷新时, 所有代码重新执行, 回归初始化
// 前提2: 刷新时, 路由会从/ 跳转到浏览器地址栏所在的路由地址 (走一次路由守卫代码)
// 动态的还未添加, 所以404了

// 问题: 右上角退出登录+重新登录, 进入到首页时, 网页刷新不? (不刷新)
// 网页本身是不刷新的, 完全依赖路由业务场景的切换 (单页面应用好处: 用户的体验更好, 切换业务场景更快)
// 内存里路由表, 之前添加的筛选后路由规则对象还在不? (在)
// 问题2: 为何重新登录, 路由定义重复了?
// 退出登录的时候, 把token和用户信息清除了
// 登录的时候, 先获取到token保存到vuex和本地, 然后才是跳转路由, 才执行路由守卫(所以判断token有值)
// 但是用户信息没有, 重新请求, 再添加一遍筛选后的路由对象, 所以导致了路由重复

// 解决: 退出登录的时候, 让路由也回归初始化

// 问题: 什么是路由(导航)守卫?
// 答案: 当路由发生跳转的时候, 会触发一个钩子"函数", 在函数中可以通过跳转或取消或强制切换跳转地址来守卫导航
// 路由守卫里必须要有一个next()调用, 出口, 让路由页面跳转
router.beforeEach(async(to, from, next) => {
  NProgress.start()
  const token = store.getters.token
  // 登录了->不能去登录页
  // 非登录->只能去登录页
  if (token) { // 登陆了
    if (to.path === '/login') { // 去登录页
      // 中断要跳转/login这次导航, 重新跳转到/(首页)
      next('/')
      NProgress.done()
    } else { // 去别的页面
      next() // 如果手动让cookie里token改错误, 刷新以后, vuex才会从本地取出错误token
      // 刷新时, 路由守卫会从 / 跳转到地址栏里路由地址, 所以先让页面跳转进去
      // 执行下面请求会401, 被动退出时, 才能拿到跳转后的路由地址(未遂地址给登录页面, 否则next在下面, 未遂地址一直是/)
      if (!store.getters.name) {
        await store.dispatch('user/getUserInfoActions')
        // const menus = await store.dispatch('user/getUserInfoActions')
        // 用menus权限点英文字符串, 和路由规则对象name匹配
        // 把所有准备好的8个路由规则对象, 取出, 看看名字和menus里是否匹配, 匹配就证明
        // 此登录的用户有这个页面的访问权限, 让filter收集此路由规则对象到新数组里
        // const filterList = asyncRoutes.filter(routeObj => {
        //   const routeName = routeObj.children[0].name.toLowerCase()
        //   return menus.includes(routeName)
        // })

        // filterList.push({ path: '*', redirect: '/404', hidden: true })

        // 始终都动态添加先8个路由规则对象
        // 知识点: 路由切换匹配的路由规则对象数组存在于内存中的
        // new Router时, 有一些初始的路由规则对象
        // addRoutes, 会给路由表, 再额外的增加一个规则对象
        // 现象: 路由规则对象添加成功, 但是左侧的导航不见了
        const filterList = asyncRoutes
        router.addRoutes(filterList)

        // 给vuex也同步一份
        store.commit('permission/setRoutes', filterList)

        // 路由再跳转一次, 因为上面next() 会导致白屏(因为放行时, 动态路由还没有加入到内存中路由表里)
        // 添加完, 立刻再跳转一次
        next({
          path: to.path,
          replace: true // 不让回退 类似于this.$router.replace() 防止进入刚才的白屏
        })
      }
    }
  } else { // 没有登录
    if (whiteList.includes(to.path)) { // 要去的路由地址字符串, 是否在白名单数组里出现过, 出现过就放行
      next()
    } else { // 去别的页面(内部项目, 不登录别的页面不能去)
      next('/login')
      NProgress.done()
    }
  }
})
// 验证: 把本地cookie里token手动删除掉, 刷新, 看看是否走最后一个else内
router.afterEach((to, from) => {
  // 正常next()放行了跳转了, 才会走后置守卫, 关闭正常流程进度条
  //动态改变title
  document.title = getPageTitle(to.meta.title)
  NProgress.done()
})
法三:
//对于axios进行二次封装
import axios from 'axios'
import nprogress from 'nprogress'
//引入进度条的样式
import 'nprogress/nprogress.css'
//start:进度条开始 done:进度条结束

//1:利用axios对象的方法create,去创建一个axios实例
//2:requests就是axios,只不过稍做配置一下
const requests = axios.create({
  //配置对象
  //基础路径,发请求URL携带api【发现:真实服务器接口都携带/api】
  baseURL: '/mock',
  //超时的设置
  timeout: 3000
})
//请求拦截器:将来项目中【N个请求】,只要发请求,会触发请求拦截器!!!
requests.interceptors.request.use(config => {
  //请求拦截器:请求头【header】,请求头能否给服务器携带参数
  //请求拦截器:其实项目中还有一个重要的作用,给服务器携带请求们的公共的参数
  //config:配置对象,对象里面有一个属性很重要,headers请求头

  //进度条开始动
  nprogress.start()
  return config
})
//响应拦截器:请求数据返回会执行
requests.interceptors.response.use(
  res => {
    //res:实质就是项目中发请求、服务器返回的数据

    //进度条结束
    nprogress.done()
    return res.data
  },
  err => {
    //温馨提示:某一天发请求,请求失败,请求失败的信息打印出来
    //终止Promise链
    return Promise.reject(new Error('failed'))
  }
)

//最后需要暴露:暴露的是添加新的功能的axios,即为requests
export default requests
dayjs使用
import dayjs from 'dayjs'
<el-table-column prop="timeOfEntry" label="入职时间" :formatter="timeFormatter" />
// 时间格式化
    // 后台返回的时间格式不一定是什么?(后端没有做数据的验证, 录入新员工不同的同学, 录入的时间格式不同)
    timeFormatter(row) {
      return dayjs(row.timeOfEntry).format('YYYY-MM-DD')
},
vue2实现上传本地照片
***npm i cos-js-sdk-v5 --save
	"core-js": "3.6.5",
    "cos-js-sdk-v5": "^1.3.5",

<template>
  <div class="user-info">
    <!-- 个人信息 -->
    <el-form label-width="220px">
      <!-- 工号 入职时间 -->
      <el-row class="inline-info">
        <el-col :span="12">
          <el-form-item label="工号">
            <el-input v-model="userInfo.workNumber" class="inputW" />
          </el-form-item>
        </el-col>

        <el-col :span="12">
          <el-form-item label="入职时间">
            <!--
              数据 "2018-01-01" -> 影响视图显示
              视图选择 -> 默认绑定日期对象 -> v-model变量

              type="date" (选择年-月-日) 控制选择日期格式 (组件渲染内容)
              value-format 选择的值绑定格式(默认不写, v-model绑定的是日期对象)

             -->
            <el-date-picker
              v-model="userInfo.timeOfEntry"
              style="width: 300px"
              type="date"
              class="inputW"
            />
          </el-form-item>
        </el-col>
      </el-row>
      <!-- 姓名 部门 -->
      <el-row class="inline-info">
        <el-col :span="12">
          <el-form-item label="姓名">
            <el-input v-model="userInfo.username" class="inputW" />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="部门">
            <el-input v-model="userInfo.departmentName" class="inputW" readonly />
          </el-form-item>
        </el-col>
      </el-row>
      <!--手机 聘用形式  -->
      <el-row class="inline-info">
        <el-col :span="12">
          <el-form-item label="手机">
            <el-input v-model="userInfo.mobile" style="width: 300px" readonly />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="聘用形式">
            <el-select v-model="userInfo.formOfEmployment" class="inputW">
              <el-option
                v-for="item in EmployeeEnum.hireType"
                :key="item.id"
                :label="item.value"
                :value="item.id"
              />
            </el-select>
          </el-form-item>
        </el-col>
      </el-row>
      <!-- 员工照片 -->
      <el-row class="inline-info">
        <el-col :span="12">
          <el-form-item label="员工头像">
            <!-- 放置上传图片 -->
            <upload-img ref="uploadImg" />

          </el-form-item>
        </el-col>
      </el-row>
      <!-- 保存个人信息 -->
      <el-row class="inline-info" type="flex" justify="center">
        <el-col :span="12">
          <el-button type="primary" @click="saveUser">保存更新</el-button>
          <el-button @click="$router.back()">返回</el-button>
        </el-col>
      </el-row>
    </el-form>
  </div>
</template>

<script>
import { getUserPhotoAPI, updateEmployeesAPI } from '@/api'
import EmployeeEnum from '@/api/constant'
export default {
  name: 'UserInfo',
  data() {
    return {
      userInfo: {}, // 个人信息-对象(提前声明属性为了见名知意)
      EmployeeEnum
      // 知识点: v-model="userInfo.workNumber"
      // 当输入框有值的时候
      // 如果对象里有这个属性, 则赋值
      // 如果对象里无这个属性, 则会添加属性并赋值
    }
  },
  created() {
    // 请求-个人信息
    this.getUserInfoFn()
  },
  methods: {
    async getUserInfoFn() {
      const res = await getUserPhotoAPI(this.$route.query.id)
      this.userInfo = res.data
      this.$refs.uploadImg.imageUrl = res.data.staffPhoto
      // 额外加入一个聘用形式
      // 问题: 下面这样写, 为何点击页面下拉菜单, 标签里显示的值不变, vue里数据名里值变了
      // 问题: 视图 -> 数据(v), 但是数据 -> 响应没有更新给视图
      // Vue框架原理: 响应式原理
      // Vue内部会检测data里每个变量(如果变量本身改变了->上面那句话, 响应式更新视图所有)
      // 检测userInfo里每个属性(检测到变化, 会更新数据+视图)
      // 上面数据劫持已经绑定完毕

      // 走到这句话的时候, 数据->视图 (但是没有绑定数据劫持)
      // 给"对象后续添加一个属性"的时候, "还想双向绑定好用" 不会应该对象本身的响应式触发
      // this.userInfo.formOfEmployment = parseInt(this.$route.query.form)
      // 解决: 如果你要后续给对象添加属性
      // $set() Vue内部提供的一个专门添加数组/对象某个值的(并且额外添加数据劫持)
      // 参数1: 数组/对象 目标
      // 参数2: 下标/属性名
      // 参数3: 值
      this.$set(this.userInfo, 'formOfEmployment', parseInt(this.$route.query.form))
    },

    // 保存更新按钮->点击事件
    async saveUser() {
      // 把头像地址保存到userInfo里一起带给后台
      this.userInfo.staffPhoto = this.$refs.uploadImg.imageUrl

      const res = await updateEmployeesAPI(this.userInfo)
      this.$message.success(res.message)
      this.$router.back()
    }
  }
}
</script>

<style lang="scss" scoped></style>
***组件中:
<!-- 放置上传图片 -->
<upload-img ref="uploadImg" />

this.$refs.uploadImg.imageUrl = res.data.staffPhoto
uuid使用
****uuid.js
//利用uuid生成未登录用户临时标识符
import { v4 as uuidv4 } from 'uuid'
//封装函数:只能生成一次用户临时身份
export const getUUID = () => {
  let uuid_token = localStorage.getItem('UUIDTOKEN')
  //如果没有
  if (!uuid_token) {
    //生成一个随机的临时身份
    uuid_token = uuidv4()
    //本地存储一次
    localStorage.setItem('UUIDTOKEN', uuid_token)
  }
  return uuid_token
}

***store.js
//封装游客身份模块uuid  生成一个随机字符串(不能在变了)
import { getUUID } from '@/utils/uuid_token'
const state = {
  //游客临时身份
  uuid_token: getUUID()
}
VueRouter中重写push和replace方法
在vue中如果我们使用编程是跳转路由,然后跳转的还是同一个路由页面,那么控制台会出现报错
//先把VueRouter原型对象的push,先保存一份
let originPush = VueRouter.prototype.push
let originReplace = VueRouter.prototype.replace
//重写push|replace
//第一个参数:告诉原来push方法,你往哪里跳转(传递哪些参数)
VueRouter.prototype.push = function (location, resolve, reject) {
  if (resolve && reject) {
    //call||apply区别:
    // 相同点,都可以调用函数一次,都可以篡改函数的上下文一次
    //不同点: call与apply传递参数: call传递参数用逗号隔开,apply方法执行,传递数组
    originPush.call(this, location, resolve, reject)
  } else {
    originPush.call(
      this,
      location,
      () => {},
      () => {}
    )
  }
}
VueRouter.prototype.replace = function (location, resolve, reject) {
  if (resolve && reject) {
    //call||apply区别:
    // 相同点,都可以调用函数一次,都可以篡改函数的上下文一次
    //不同点: call与apply传递参数: call传递参数用逗号隔开,apply方法执行,传递数组
    originReplace.call(this, location, resolve, reject)
  } else {
    originReplace.call(
      this,
      location,
      () => {},
      () => {}
    )
  }
}
全局前置守卫
import { router, addRoutes } from '@/router'
import { getToken } from '@/composables/auth'
import { toast, showFullLoading, hideFullLoading } from '@/composables/util'
import store from './store'
// 全局前置守卫
let hasGetInfo = false
router.beforeEach(async (to, from, next) => {
  //显示loading
  showFullLoading()
  const token = getToken()
  // 没有登录,强制跳转回登录页
  if (!token && to.path != '/login') {
    toast('请先登录', 'error')
    return next({ path: '/login' })
  }
  // 防止重复登录
  if (token && to.path == '/login') {
    toast('请勿重复登录', 'error')
    return next({ path: from.path ? from.path : '/' })
  }
  // 如果用户登录了,自动获取用户信息,并存储在vuex当中
  let hasNewRoutes = false
  if (token && !hasGetInfo) {
    let { menus } = await store.dispatch('getInfo')
    hasGetInfo = true
    //动态添加路由
    hasNewRoutes = addRoutes(menus)
  }
  // 设置页面标题
  let title = (to.meta.title ? to.meta.title : '') + '-帝莎编程商城后台'
  document.title = title

  hasNewRoutes ? next(to.fullPath) : next()
})

// 全局后置守卫
router.afterEach((to, from) => hideFullLoading())
************************************************************************************************************
import router, { asyncRoutes } from './router'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style
import store from '@/store'
import getPageTitle from '@/utils/get-page-title'

NProgress.configure({ showSpinner: false }) // NProgress Configuration

const whiteList = ['/login', '/404'] // 白名单: 无需登录, 可以跳转查看的路由地址(在路由表里)

// 问题: 为何动态路由添加后, 在动态路由地址上刷新会404?
// 前提1: 刷新时, 所有代码重新执行, 回归初始化
// 前提2: 刷新时, 路由会从/ 跳转到浏览器地址栏所在的路由地址 (走一次路由守卫代码)
// 动态的还未添加, 所以404了

// 问题: 右上角退出登录+重新登录, 进入到首页时, 网页刷新不? (不刷新)
// 网页本身是不刷新的, 完全依赖路由业务场景的切换 (单页面应用好处: 用户的体验更好, 切换业务场景更快)
// 内存里路由表, 之前添加的筛选后路由规则对象还在不? (在)
// 问题2: 为何重新登录, 路由定义重复了?
// 退出登录的时候, 把token和用户信息清除了
// 登录的时候, 先获取到token保存到vuex和本地, 然后才是跳转路由, 才执行路由守卫(所以判断token有值)
// 但是用户信息没有, 重新请求, 再添加一遍筛选后的路由对象, 所以导致了路由重复

// 解决: 退出登录的时候, 让路由也回归初始化

// 问题: 什么是路由(导航)守卫?
// 答案: 当路由发生跳转的时候, 会触发一个钩子"函数", 在函数中可以通过跳转或取消或强制切换跳转地址来守卫导航
// 路由守卫里必须要有一个next()调用, 出口, 让路由页面跳转
router.beforeEach(async(to, from, next) => {
  NProgress.start()
  const token = store.getters.token
  // 登录了->不能去登录页
  // 非登录->只能去登录页
  if (token) { // 登陆了
    if (to.path === '/login') { // 去登录页
      // 中断要跳转/login这次导航, 重新跳转到/(首页)
      next('/')
      NProgress.done()
    } else { // 去别的页面
      next() // 如果手动让cookie里token改错误, 刷新以后, vuex才会从本地取出错误token
      // 刷新时, 路由守卫会从 / 跳转到地址栏里路由地址, 所以先让页面跳转进去
      // 执行下面请求会401, 被动退出时, 才能拿到跳转后的路由地址(未遂地址给登录页面, 否则next在下面, 未遂地址一直是/)
      if (!store.getters.name) {
        await store.dispatch('user/getUserInfoActions')
        // const menus = await store.dispatch('user/getUserInfoActions')
        // 用menus权限点英文字符串, 和路由规则对象name匹配
        // 把所有准备好的8个路由规则对象, 取出, 看看名字和menus里是否匹配, 匹配就证明
        // 此登录的用户有这个页面的访问权限, 让filter收集此路由规则对象到新数组里
        // const filterList = asyncRoutes.filter(routeObj => {
        //   const routeName = routeObj.children[0].name.toLowerCase()
        //   return menus.includes(routeName)
        // })

        // filterList.push({ path: '*', redirect: '/404', hidden: true })

        // 始终都动态添加先8个路由规则对象
        // 知识点: 路由切换匹配的路由规则对象数组存在于内存中的
        // new Router时, 有一些初始的路由规则对象
        // addRoutes, 会给路由表, 再额外的增加一个规则对象
        // 现象: 路由规则对象添加成功, 但是左侧的导航不见了
        const filterList = asyncRoutes
        router.addRoutes(filterList)

        // 给vuex也同步一份
        store.commit('permission/setRoutes', filterList)

        // 路由再跳转一次, 因为上面next() 会导致白屏(因为放行时, 动态路由还没有加入到内存中路由表里)
        // 添加完, 立刻再跳转一次
        next({
          path: to.path,
          replace: true // 不让回退 类似于this.$router.replace() 防止进入刚才的白屏
        })
      }
    }
  } else { // 没有登录
    if (whiteList.includes(to.path)) { // 要去的路由地址字符串, 是否在白名单数组里出现过, 出现过就放行
      next()
    } else { // 去别的页面(内部项目, 不登录别的页面不能去)
      next('/login')
      NProgress.done()
    }
  }
})
// 验证: 把本地cookie里token手动删除掉, 刷新, 看看是否走最后一个else内
router.afterEach((to, from) => {
  // 正常next()放行了跳转了, 才会走后置守卫, 关闭正常流程进度条
  //动态改变title
  document.title = getPageTitle(to.meta.title)
  NProgress.done()
})
************************************************************************************************************
//配置路由的地方
import Vue from 'vue'
import VueRouter from 'vue-router'
//使用插件
Vue.use(VueRouter)
import routes from './routes'
//引入仓库
import store from '@/store'
//先把VueRouter原型对象的push,先保存一份
let originPush = VueRouter.prototype.push
let originReplace = VueRouter.prototype.replace
//重写push|replace
//第一个参数:告诉原来push方法,你往哪里跳转(传递哪些参数)
VueRouter.prototype.push = function (location, resolve, reject) {
  if (resolve && reject) {
    //call||apply区别:
    // 相同点,都可以调用函数一次,都可以篡改函数的上下文一次
    //不同点: call与apply传递参数: call传递参数用逗号隔开,apply方法执行,传递数组
    originPush.call(this, location, resolve, reject)
  } else {
    originPush.call(
      this,
      location,
      () => {},
      () => {}
    )
  }
}
VueRouter.prototype.replace = function (location, resolve, reject) {
  if (resolve && reject) {
    //call||apply区别:
    // 相同点,都可以调用函数一次,都可以篡改函数的上下文一次
    //不同点: call与apply传递参数: call传递参数用逗号隔开,apply方法执行,传递数组
    originReplace.call(this, location, resolve, reject)
  } else {
    originReplace.call(
      this,
      location,
      () => {},
      () => {}
    )
  }
}
//配置路由
let router = new VueRouter({
  //配置路由
  //第一:路径的前面需要有/(不是二级路由)
  //路径中单词都是小写的
  // component右侧v别给我加单引号【字符串:组件是对象(VueComponent类的实例)】
  routes,
  //滚动行为
  scrollBehavior(to, from, savedPosition) {
    //y代表滚动条在最上方
    return { y: 0 }
  }
})
//全局守卫:前置守卫(在路由跳转之间进行判断)
//全局守卫:只要项目中有任何路由变化,全局守卫都会进行拦截【符合条件走你,不符合条件不能访问】
//全局守卫:全局前置守卫【访问之前进行触发】
router.beforeEach(async (to, from, next) => {
  //to:可以获取到你要跳转到哪个路由信息
  //from:可以获取到你从哪个路由而来的信息
  //next:放行函数  next()放行
  //第一种:next(),放行函数,全部放行!!!
  //第二种:next(path),守卫指定放行到那个路由去
  //token
  //用户登录了,才会有token,未登录一定不会有token
  let hasToken = store.state.user.token
  //用户信息
  let hasName = store.state.user.userInfo.name
  //用户登录
  if (hasToken) {
    //用户登录了,不能去login
    if (to.path == '/login') {
      next('/home')
    } else {
      //用户登陆了,而且还有用户信息【去的并非是login】
      //登陆,去的不是login 去的是【home |search|detail|shopcart】
      //如果用户名已有
      if (hasName) {
        next()
      } else {
        //用户登陆了,但是没有用户信息 派发action让仓库存储用户信息在跳转
        try {
          //发请求获取用户信息以后在放行
          await store.dispatch('getUserInfo')
          next()
        } catch (error) {
          //用户没有信息,还携带token发请求获取用户信息【失败】
          //token【*****失效了】
          //token失效:本地清空数据、服务器的token通知服务器清除
          await store.dispatch('userLogout')
          //回到登录页,重新获取一个新的学生证
          next('/login')
        }
      }
    }
  } else {
    //用户未登录||目前的判断都是放行.将来这里会'回手掏'增加一些判断
    //用户未登录:不能进入/trade、/pay、/paysuccess、/center、/center/myorder  /center/grouporder
    let toPath = to.path
    //要去的路由存在
    if (toPath.indexOf('/trade') != -1 || toPath.indexOf('/pay') != -1 || toPath.indexOf('/center') != -1) {
      //把未登录的时候想去而没有去成的路由地址,存储于地址栏中【路由】
      next('/login?redirect=' + toPath)
    } else {
      next()
    }
  }
})
export default router

swiper插件使用
***main.js
npm i swiper
//引入swiper样式
import 'swiper/css/swiper.css'
***组件中:
<template>
  <!-- 轮播图 -->
  <div class="swiper-container" ref="cur">
    <div class="swiper-wrapper">
      <div class="swiper-slide" v-for="carousel in list" :key="carousel.id">
        <img :src="carousel.imgUrl" />
      </div>
    </div>
    <!-- 如果需要分页器 -->
    <div class="swiper-pagination"></div>

    <!-- 如果需要导航按钮 -->
    <div class="swiper-button-prev"></div>
    <div class="swiper-button-next"></div>
  </div>
</template>

<script>
//引入Swiper
import Swiper from 'swiper'
export default {
  name: 'Carousel',
  props: ['list'],
  watch: {
    list: {
      //立即监听:不管你数据有没有变化,我上来立即监听一次
      //为什么watch监听不到list:因为这个数据从来没有发生变化《数据是父亲给的,父亲给的时候就是一个对象,对象里面该有的数据都是有的)
      immediate: true,
      handler() {
        //只能监听到数据已经有了,但是v-for动态渲染结构我们还是没有办法确定的,因此还是需要用nextTick
        this.$nextTick(() => {
          var mySwiper = new Swiper(this.$refs.cur, {
            loop: true,
            autoplay: true,
            //如果需要分页器
            pagination: {
              el: '.swiper-pagination',
              //点击小球的时候也切换图片
              clickable: true
            },
            //如果需要前进后退按钮
            navigation: {
              nextEl: '.swiper-button-next',
              prevEl: '.swiper-button-prev'
            }
          })
        })
      }
    }
  }
}
</script>

<style scoped></style>

支付代码、QRCode插件使用、ElementUI注册组件
***main.js
import { Button, MessageBox,Message } from 'element-ui'
//注册全局组件
Vue.component(Button.name, Button)
//ElementUI注册组件的时候,还有一种写法,挂在原型上
Vue.prototype.$msgbox = MessageBox
Vue.prototype.$alert = MessageBox.alert
Vue.prototype.$confirm = MessageBox.confirm
Vue.prototype.$message = Message

***$alert、$msgbox使用:
npm install --save qrcode
import QRCode from 'qrcode'
//支付弹出框函数
    async open() {
      //生成二维码地址
      let url = await QRCode.toDataURL(this.payInfo.codeUrl)
      this.$alert(`<img src=${url} />`, '请您微信支付', {
        //是否将 message属性作为HTML片段处理
        dangerouslyUseHTMLString: true,
        //居中
        center: true,
        //显示取消按钮
        showCancelButton: true,
        //取消按钮的文本内容
        cancelButtonText: '支付遇见问题',
        //确定按钮的文本
        confirmButtonText: '已支付成功',
        //右上角的叉子
        showClose: true,
        //关闭弹出框的配置值
        beforeClose: (type, instance, done) => {
          //type:区分取消|确定按钮
          //instance:当前组件实例
          //done:关闭弹出框的方法
          if (type == 'cancel') {
            // alert('请联系管理员')
            //清除定时器
            clearInterval(this.timer)
            this.timer = null
            //关闭弹出框
            done()
          } else {
            //判断是否真的支付了
            //开发人员后门
            if (this.code == 200) {
            	clearInterval(this.timer)
            	this.timer = null
            	done()
            	//跳转到下一路由
            	this.$router.push('/paysuccess')
            }
          }
        }
      }).catch(() => {}) //没有进行错误捕获,就会提示Uncaught (in promise) cancel错误。
      //需要知道支付成功与否   每隔1s就判断支付成功没
      //支付成功,路由的跳转,如果支付失败,提示信息
      //定时器没有,开启一个新的定时器
      if (!this.timer) {
        // console.log(this.timer)  null
        // console.log(Boolean(this.timer))   false
        // console.log(Boolean(!this.timer))   true
        this.timer = setInterval(async () => {
          //发请求获取用户支付状态
          let result = await this.$API.reqPayStatus(this.orderId)
          if (result.code == 200) {
            //第一步:停止定时器
            clearInterval(this.timer)
            this.timer = null
            //保存支付成功返回的code
            this.code = result.code
            //关闭弹出框
            this.$msgbox.close()
            //跳转到下一路由
            this.$router.push('/paysuccess')
          }
        }, 1000)
      }
    }
***$confirm使用:
// 点击退出登录的回调
    logout() {
      this.$confirm('要退出登录吗?', '提示', {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning'
        }).then(async() => {
          let res = await this.$request("/logout",{ timestamp: getTimeStamp() });
          // console.log(res);
          if (res.data.code != 200) {
            this.$message("退出登录失败, 请稍后重试!");
            return;
          }
          // 清空data和localstorage中的数据,以及cookie
          // window.localStorage.setItem("userInfo", "");
          // this.clearAllCookie();
          // 删除localstoarge的userId
          window.localStorage.removeItem("userId");
          //   在vuex中更新登录状态
          this.$store.commit("updataLoginState", false);
          this.$message.success("退出成功!");
          this.isCurrentUser = false;
        }).catch(() => {
          this.$message({
            type: 'info',
            message: '已取消登录'
          });          
        });
    },
***$message使用:
this.$message.error("请先进行登录操作");
b u s 、 bus、 busAPI全局绑定
***main.js
//统一接口api文件夹里面全部请求函数
//统一引入
import * as API from '@/api'

new Vue({
  render: h => h(App),
  //全局时间总线$bus配置
  // beforeCreate 函数就是 Vue 实例被创建出来之前,会执行它。在 beforeCreate 生命周期函数执行的时候,Vue实例中的 data 和 methods 中的数据都还没有被初始化。
  beforeCreate() {
    //向外共享Vue的实例对象  固定写法
    Vue.prototype.$bus = this
    // console.log(Vue.prototype.$bus)
    Vue.prototype.$API = API
    // console.log(Vue.prototype.$API)里面是reqAddOrUpdateShopCart,reqAddressInfo等接口
  },
  //注册路由:底下的写法KV一致省略V[router小写]
  //注册路由信息:当这里书写router的时候,组件身上都拥有$route,$router属性
  router,
  //注册仓库:组件实例的身上会多一个属性$store属性
  store
}).$mount('#app')
***$bus使用场景:
//通知兄弟组件:当前的索引值为几
      this.$bus.$emit('getIndex', this.currentIndex)
//全局事件总线:获取兄弟组件传递过来的索引值
    this.$bus.$on('getIndex', index => {
      //修改当前响应式数据
      this.currentIndex = index
    })
***$API使用场景:
//获取我的订单方法
    async getData() {
      //结构出参数
      const { page, limit } = this
      //好处:不用引入reqMyOrderList接口
      let result = await this.$API.reqMyOrderList(page, limit)
      if (result.code == 200) {
        this.myOrder = result.data
      }
},
vee-validate使用
npm install vee-validate --save
***main.js
//引入表单检验插件
import '@/plugins/validate'
***validate.js
//vee-validate插件:表单验证区域
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}必须与密码相同` //修改内置规则的message,让确认密码和密码相同
  },
  attributes: {
    //给校验的-field-属性名映射中文名称
    //给每个字段转为中文
    phone: '手机号',
    code: '验证码',
    password: '密码',
    password1: '确认密码',
    agree: '协议'
  }
})
//自定义校验规则
VeeValidate.Validator.extend('agree', {
  validate: value => {
    return value
  },
  getMessage: field => field + '必须同意'
})
***组件中:
<div class="content">
        <label>手机号:</label>
        <!-- has('field') – 当前filed是否有错误(true/false) -->
        <!-- errors.first('field') – 获取关于当前field的第一个错误信息 -->
        <input placeholder="请输入你的手机号" v-model="phone" name="phone" v-validate="{ required: true, regex: /^1\d{10}$/ }" :class="{ invalid: errors.has('phone') }" />
        <span class="error-msg">{{ errors.first('phone') }}</span>
      </div>
      <div class="content">
        <label>验证码:</label>
        <input placeholder="请输入你的验证码" v-model="code" name="code" v-validate="{ required: true, regex: /^\d{6}$/ }" :class="{ invalid: errors.has('code') }" />
        <button style="width: 100px; height: 38px" @click="getCode">获取验证码</button>
        <span class="error-msg">{{ errors.first('code') }}</span>
      </div>
      <div class="content">
        <label>登录密码:</label>
        <input type="password" placeholder="请输入你的密码" v-model="password" name="password" v-validate="{ required: true, regex: /^[0-9A-Za-z]{8,20}$/ }" :class="{ invalid: errors.has('password') }" />格式:8-20位密码,禁止输入符号
        <span class="error-msg">{{ errors.first('password') }}</span>
      </div>
      <div class="content">
        <label>确认密码:</label>
        <input type="password" placeholder="请输入确认密码" v-model="password1" name="password1" v-validate="{ required: true, is: password }" :class="{ invalid: errors.has('password1') }" />
        <span class="error-msg">{{ errors.first('password1') }}</span>
      </div>
      <div class="controls">
        <input type="checkbox" v-model="agree" name="agree" v-validate="{ required: true, agree: true }" :class="{ invalid: errors.has('agree') }" />
        <span>同意协议并注册《尚品汇用户协议》</span>
        <span class="error-msg">{{ errors.first('agree') }}</span>
      </div>
      
<script>
data() {
    return {
      //收集表单数据--手机号
      phone: '',
      //验证码
      code: '',
      //密码
      password: '',
      //确认密码
      password1: '',
      //确认协议
      agree: true
    }
  },
	//注册信息全部合格后才能注册
      const success = await this.$validator.validateAll()
      // console.log(success);//布尔值
      //全部表单验证成功,在向服务器发请求,进行注册
      //只要有一个表单没有成功,不会发请求
</script>
懒加载插件使用
npm i vue-lazyload -S
//引入图片懒加载插件
import VueLazyload from 'vue-lazyload'
import atm from '@/assets/logo.png'
Vue.use(VueLazyload, {
  // 放入懒加载的图片,就是atm
  loading: atm
})
***组件中:
<img v-lazy="good.defaultImg" />
i18n包使用
***main.js
import ELEMENT from 'element-ui'
// 引入语言对象
import i18n from '@/lang'
Vue.use(ELEMENT, {
  // 配置 ELEMENT 语言转换关系
  // 每个组件都会调用一次
  i18n: (key, value) => {
    // 组件内容处, 使用的相关参数和值↓
    // key: el.pagination.total (好比是要查找语言包的属性路径)
    // value: 对应要传入的值 {total: 10}
    // i18n.t 好比 Vue组件$t
    // key就是去语言包环境找到对应的中文/英文值
    // value就是要传入的值 会替换掉{} 位置, 换成对应值在原地显示
    return i18n.t(key, value)
  }
}) // 只是为了注册elementUI组件(语言切换, 一会儿和Vuei18n集成)

***组件中:
<template>
  <!--
      trigger 是下拉菜单的触发时机
      @command 自定义事件 (检测菜单项的点击行为)
   -->
  <el-dropdown trigger="click" @command="changeLanguage">
    <!-- 第一个子标签是上来就显示的标签 -->
    <div>
      <svg-icon style="color:#fff;font-size:20px" icon-class="language" />
    </div>
    <!-- 就会出现真正的下拉菜单项 -->
    <el-dropdown-menu slot="dropdown">
      <!-- command 点击时, 传入给@command事件里参数
        $i18n 是Vue.use(Vuei18n)添加给Vue原型的全局属性, 通过它可以拿到i18n里locale环境的英文标识('zh'/'en')
       -->
      <el-dropdown-item command="zh" :disabled="'zh'=== $i18n.locale ">中文</el-dropdown-item>
      <el-dropdown-item command="en" :disabled="'en'=== $i18n.locale ">English</el-dropdown-item>
    </el-dropdown-menu>
  </el-dropdown>
</template>

<script>
export default {
  methods: {
    // 下拉菜单项的点击事件
    // lang的值就是 "zh" "en"
    changeLanguage(lang) {
      this.$i18n.locale = lang // 设置给本地的i18n插件
      this.$message.success('切换多语言成功')
    }
  }
}
</script>

***lang.js
import Vue from 'vue'
import VueI18n from 'vue-i18n'
// ElementUI的中英文语言包引入
// 语言包:对象
// 相同的key(键)名, 对应的对象(值, 不同的语言包, 对应值不同)
import enLocale from 'element-ui/lib/locale/lang/en'
import zhLocale from 'element-ui/lib/locale/lang/zh-CN'

Vue.use(VueI18n)

// 通过选项创建 VueI18n 实例
const i18n = new VueI18n({
  // 隐藏警告
  silentTranslationWarn: true,
  locale: 'zh-CN', // 设置地区
  messages: {
    en: {
      navbar: {
        companyName: 'Jiangsu Chuanzhi podcast Education Technology Co., Ltd',
        name: '{name}'
      },
      sidebar: {
        dashboard: 'Dashboard',
        approvals: 'Approvals',
        departments: 'Departements',
        employees: 'Employees',
        permission: 'Permission',
        attendances: 'Attendances',
        salarys: 'Salarys',
        setting: 'Company-Settings',
        social: 'Social'
      },
      ...enLocale,
      message: {
        hello: 'hello world'
      }
    },
    zh: {
      navbar: {
        companyName: '江苏传智播客教育科技股份有限公司',
        name: '{name}'
      },
      sidebar: {
        dashboard: '首页',
        approvals: '审批',
        departments: '组织架构',
        employees: '员工',
        permission: '权限',
        attendances: '考勤',
        salarys: '工资',
        setting: '公司设置',
        social: '社保'
      },
      ...zhLocale,
      message: {
        hello: '你好, 世界'
      }
    }
  } // 设置地区信息
})

// vuei18n内部的工作原理
// 1. 会给Vue原型上添加$t方法
// 2. 我们自己业务vue文件中, 文字部分都要换成$t方法, 然后在方法中传入要获取的对象的属性值路径字符串
// 3. $t方法内, 会根据locale的值, 去messages里面取出对应环境的语言对象, 然后再拼接本次寻找值对象属性的路径, 找到 对应的值返回到$t函数位置

export default i18n

map使用
1.案例一:取给定数组的某一字段组成新数组
的后台传来的数据 data(json):
[   //data的数据
	{"txt":"09:00-12:00","codId":"1","flgDel":"0","id":1},
	{"txt":"13:00-16:00","codId":"1","flgDel":"0","id":2},
	{"txt":"18:00-20:00","codId":"1","flgDel":"0","id":3}
]
前台使用要为:
['09:00-12:00', '13:00-16:00', '18:00-20:00']
用到map()只需一行。快捷方法出来了学去吧。
let time = data.map(item =>(item.txt))
console.log(time) 
//控制台输出如下
//['09:00-12:00', '13:00-16:00', '18:00-20:00']
2.案例二:取给定数组的某些字段重命名并组成新数组
新的接口传来data(json):
[  //新data数据
{"txt":"拜访","flgDel":"0","id":1},
{"txt":"面试","flgDel":"0","id":2},
{"txt":"其他","flgDel":"0","id":3}
]
前台使用数组结构:
[{ name: '拜访' }, { name: '面试' }, { name: '其他' }]
//这里看到相比于案例一有字段了,还新命名了
//只需一行map()
let resion = data.map(item =>({name: item.txt}))
console.log(resion) 
//控制台输出
//[{ name: '拜访' }, { name: '面试' }, { name: '其他' }]
当然,或许你要的这样⬇ :
[{ name: '拜访',id:'1' }, { name: '面试',id:'2' }, { name: '其他',id:'3'}]
//要两个字段的数据
let resion2 = data.map(item =>({name: item.txt, id: item.id}))
console.log(resion2) 
//控制台输出
//[{ name: '拜访',id:'1' }, { name: '面试',id:'2' }, { name: '其他',id:'3'}]
又或许你想要这样⬇ :
[{ name: '拜访1' }, { name: '面试2' }, { name: '其他3'}]
//要拼接的数据
let resion3 = data.map(item =>({name: item.txt + item.id}))
console.log(resion3) 
//控制台输出
//[{ name: '拜访1' }, { name: '面试2' }, { name: '其他3'}]
配置动态路由
*********************************************************************法一:
import { createRouter, createWebHashHistory } from 'vue-router'
import Index from '@/pages/index.vue'
import Login from '@/pages/login.vue'
import NotFound from '@/pages/404.vue'
import Admin from '@/layouts/admin.vue'
import GoodList from '@/pages/goods/list.vue'
import CategoryList from '@/pages/category/list.vue'
import UserList from '@/pages/user/list.vue'
import OrderList from '@/pages/order/list.vue'
import CommentList from '@/pages/comment/list.vue'
import ImageList from '@/pages/image/list.vue'
import NoticeList from '@/pages/notice/list.vue'
import SettingBase from '@/pages/setting/base.vue'
import CouponList from '@/pages/coupon/list.vue'
import ManagerList from '@/pages/manager/list.vue'
import AccessList from '@/pages/access/list.vue'
import RoleList from '@/pages/role/list.vue'
import SkusList from '@/pages/skus/list.vue'
import LevelList from '@/pages/level/list.vue'
import SettingBuy from '@/pages/setting/buy.vue'
import SettingShip from '@/pages/setting/ship.vue'
import DistributionIndex from '@/pages/distribution/index.vue'
import DistributionSetting from '@/pages/distribution/setting.vue'
//默认路由,所有用户共享
const routes = [
  {
    path: '/',
    name: 'admin',
    component: Admin
  },
  {
    path: '/login',
    component: Login,
    meta: {
      title: '登录页'
    }
  },
  {
    path: '/:pathMatch(.*)*',
    name: 'NotFound',
    component: NotFound
  }
]
//动态路由
const asyncRoutes = [
  {
    path: '/',
    name: '/',
    component: Index,
    meta: {
      title: '后台首页'
    }
  },
  {
    path: '/goods/list',
    name: '/goods/list',
    component: GoodList,
    meta: {
      title: '商品管理'
    }
  },
  {
    path: '/category/list',
    name: '/category/list',
    component: CategoryList,
    meta: {
      title: '分类列表'
    }
  },
  {
    path: '/user/list',
    name: '/user/list',
    component: UserList,
    meta: {
      title: '用户列表'
    }
  },
  {
    path: '/order/list',
    name: '/order/list',
    component: OrderList,
    meta: {
      title: '订单列表'
    }
  },
  {
    path: '/comment/list',
    name: '/comment/list',
    component: CommentList,
    meta: {
      title: '评价列表'
    }
  },
  {
    path: '/image/list',
    name: '/image/list',
    component: ImageList,
    meta: {
      title: '图库列表'
    }
  },
  {
    path: '/notice/list',
    name: '/notice/list',
    component: NoticeList,
    meta: {
      title: '公告列表'
    }
  },
  {
    path: '/setting/base',
    name: '/setting/base',
    component: SettingBase,
    meta: {
      title: '配置'
    }
  },
  {
    path: '/coupon/list',
    name: '/coupon/list',
    component: CouponList,
    meta: {
      title: '优惠券列表'
    }
  },
  {
    path: '/manager/list',
    name: '/manager/list',
    component: ManagerList,
    meta: {
      title: '管理员管理'
    }
  },
  {
    path: '/access/list',
    name: '/access/list',
    component: AccessList,
    meta: {
      title: '菜单权限管理'
    }
  },
  {
    path: '/role/list',
    name: '/role/list',
    component: RoleList,
    meta: {
      title: '角色管理'
    }
  },
  {
    path: '/skus/list',
    name: '/skus/list',
    component: SkusList,
    meta: {
      title: '规格管理'
    }
  },
  {
    path: '/level/list',
    name: '/level/list',
    component: LevelList,
    meta: {
      title: '会员等级'
    }
  },
  {
    path: '/setting/buy',
    name: '/setting/buy',
    component: SettingBuy,
    meta: {
      title: '支付设置'
    }
  },
  {
    path: '/setting/ship',
    name: '/setting/ship',
    component: SettingShip,
    meta: {
      title: '物流设置'
    }
  },
  {
    path: '/distribution/index',
    name: '/distribution/index',
    component: DistributionIndex,
    meta: {
      title: '分销员管理'
    }
  },
  {
    path: '/distribution/setting',
    name: '/distribution/setting',
    component: DistributionSetting,
    meta: {
      title: '分销设置'
    }
  }
]

export const router = createRouter({
  history: createWebHashHistory(),
  routes
})

// 动态添加路由的方法
export function addRoutes(menus) {
  // 是否有新的路由
  let hasNewRoutes = false
  const findAndAddRoutesByMenus = arr => {
    arr.forEach(e => {
      let item = asyncRoutes.find(o => o.path == e.frontpath)
      if (item && !router.hasRoute(item.path)) {
        //添加到名字叫admin路由的子路由
        router.addRoute('admin', item)
        hasNewRoutes = true
      }
      if (e.child && e.child.length > 0) {
        findAndAddRoutesByMenus(e.child)
      }
    })
  }
  findAndAddRoutesByMenus(menus)
  return hasNewRoutes
}

***全局前置守卫.js:
  // 如果用户登录了,自动获取用户信息,并存储在vuex当中
  let hasNewRoutes = false
  if (token && !hasGetInfo) {
    let { menus } = await store.dispatch('getInfo')
    hasGetInfo = true
    //动态添加路由
    hasNewRoutes = addRoutes(menus)
  }
  
  
*********************************************************************法二:
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
import Layout from '@/layout'
import approvalsRouter from './modules/approvals'
import departmentsRouter from './modules/departments'
import employeesRouter from './modules/employees'
import permissionRouter from './modules/permission'
import attendancesRouter from './modules/attendances'
import salarysRouter from './modules/salarys'
import settingRouter from './modules/setting'
import socialRouter from './modules/social'

// 动态路由表,项目中不同的用户可以访问不同的功能
// 暂时让所有人都看到这8个页面(最后2天再去做筛选)
// 动态路由规则  异步路由
//只做了前4个。后4个自己扩展
export const asyncRoutes = [
  departmentsRouter,
  settingRouter,
  employeesRouter,
  permissionRouter,
  approvalsRouter,
  attendancesRouter,
  salarysRouter,
  socialRouter
]

export const constantRoutes = [
  {
    path: '/login',
    component: () => import('@/views/login/index'),
    hidden: true
  },

  {
    path: '/404',
    component: () => import('@/views/404'),
    hidden: true
  },

  {
    path: '/',
    component: Layout,
    redirect: '/dashboard',
    children: [{
      path: 'dashboard',
      name: 'Dashboard',
      component: () => import('@/views/dashboard/index'),
      meta: { title: '首页', icon: 'dashboard' }
    }]
  },

  {
    path: '/excel',
    component: Layout,
    children: [
      {
        path: '',
        component: () => import('@/views/excel')
      }
    ]
  }

  // { path: '*', redirect: '/404', hidden: true }
]

const createRouter = () => new Router({
  // mode: 'history', // require service support
  scrollBehavior: () => ({ y: 0 }),
  // routes: [...constantRoutes, ...asyncRoutes]
  routes: [...constantRoutes]
})

const router = createRouter()

// Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465
export function resetRouter() {
  const newRouter = createRouter()
  router.matcher = newRouter.matcher // reset router
  // 重置的是路由对象内部match方法(匹配routes选项中的路由规则的)
  // match里会使用newRouter里routes一起代替掉了
}

export default router

***路由守卫.js
import router, { asyncRoutes } from './router'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style
import store from '@/store'
import getPageTitle from '@/utils/get-page-title'

NProgress.configure({ showSpinner: false }) // NProgress Configuration

const whiteList = ['/login', '/404'] // 白名单: 无需登录, 可以跳转查看的路由地址(在路由表里)

// 问题: 为何动态路由添加后, 在动态路由地址上刷新会404?
// 前提1: 刷新时, 所有代码重新执行, 回归初始化
// 前提2: 刷新时, 路由会从/ 跳转到浏览器地址栏所在的路由地址 (走一次路由守卫代码)
// 动态的还未添加, 所以404了

// 问题: 右上角退出登录+重新登录, 进入到首页时, 网页刷新不? (不刷新)
// 网页本身是不刷新的, 完全依赖路由业务场景的切换 (单页面应用好处: 用户的体验更好, 切换业务场景更快)
// 内存里路由表, 之前添加的筛选后路由规则对象还在不? (在)
// 问题2: 为何重新登录, 路由定义重复了?
// 退出登录的时候, 把token和用户信息清除了
// 登录的时候, 先获取到token保存到vuex和本地, 然后才是跳转路由, 才执行路由守卫(所以判断token有值)
// 但是用户信息没有, 重新请求, 再添加一遍筛选后的路由对象, 所以导致了路由重复

// 解决: 退出登录的时候, 让路由也回归初始化

// 问题: 什么是路由(导航)守卫?
// 答案: 当路由发生跳转的时候, 会触发一个钩子"函数", 在函数中可以通过跳转或取消或强制切换跳转地址来守卫导航
// 路由守卫里必须要有一个next()调用, 出口, 让路由页面跳转
router.beforeEach(async(to, from, next) => {
  NProgress.start()
  const token = store.getters.token
  // 登录了->不能去登录页
  // 非登录->只能去登录页
  if (token) { // 登陆了
    if (to.path === '/login') { // 去登录页
      // 中断要跳转/login这次导航, 重新跳转到/(首页)
      next('/')
      NProgress.done()
    } else { // 去别的页面
      next() // 如果手动让cookie里token改错误, 刷新以后, vuex才会从本地取出错误token
      // 刷新时, 路由守卫会从 / 跳转到地址栏里路由地址, 所以先让页面跳转进去
      // 执行下面请求会401, 被动退出时, 才能拿到跳转后的路由地址(未遂地址给登录页面, 否则next在下面, 未遂地址一直是/)
      if (!store.getters.name) {
        await store.dispatch('user/getUserInfoActions')
        // const menus = await store.dispatch('user/getUserInfoActions')
        // 用menus权限点英文字符串, 和路由规则对象name匹配
        // 把所有准备好的8个路由规则对象, 取出, 看看名字和menus里是否匹配, 匹配就证明
        // 此登录的用户有这个页面的访问权限, 让filter收集此路由规则对象到新数组里
        // const filterList = asyncRoutes.filter(routeObj => {
        //   const routeName = routeObj.children[0].name.toLowerCase()
        //   return menus.includes(routeName)
        // })

        // filterList.push({ path: '*', redirect: '/404', hidden: true })

        // 始终都动态添加先8个路由规则对象
        // 知识点: 路由切换匹配的路由规则对象数组存在于内存中的
        // new Router时, 有一些初始的路由规则对象
        // addRoutes, 会给路由表, 再额外的增加一个规则对象
        // 现象: 路由规则对象添加成功, 但是左侧的导航不见了
        const filterList = asyncRoutes
        router.addRoutes(filterList)

        // 给vuex也同步一份
        store.commit('permission/setRoutes', filterList)

        // 路由再跳转一次, 因为上面next() 会导致白屏(因为放行时, 动态路由还没有加入到内存中路由表里)
        // 添加完, 立刻再跳转一次
        next({
          path: to.path,
          replace: true // 不让回退 类似于this.$router.replace() 防止进入刚才的白屏
        })
      }
    }
  } else { // 没有登录
    if (whiteList.includes(to.path)) { // 要去的路由地址字符串, 是否在白名单数组里出现过, 出现过就放行
      next()
    } else { // 去别的页面(内部项目, 不登录别的页面不能去)
      next('/login')
      NProgress.done()
    }
  }
})
// 验证: 把本地cookie里token手动删除掉, 刷新, 看看是否走最后一个else内
router.afterEach((to, from) => {
  // 正常next()放行了跳转了, 才会走后置守卫, 关闭正常流程进度条
  //动态改变title
  document.title = getPageTitle(to.meta.title)
  NProgress.done()
})

mixins使用
***lyricScroll.js
export default {
  props: {
    lyric: {
      type: Array,
      default: [],
    },
  },
  data() {
    return {
      // 当前歌词索引
      lyricsIndex: 0,
    };
  },
  methods: {
    // 实现歌词滚动
    lyricScroll(currentLyric) {
      let placeholderHeight = 0;
      // 获取歌词item
      let lyricsArr = document.querySelectorAll(".lyricsItem");
      // 获取歌词框
      let lyrics = document.querySelector(".lyrics");
      // console.log(lyrics.offsetTop)//123
      // console.log(lyricsArr[0].offsetTop)//123
      // placeholder的高度
      if (placeholderHeight == 0) {
        placeholderHeight = lyricsArr[0].offsetTop - lyrics.offsetTop;//123-123
        // console.log(placeholderHeight)//0
      }
      //   歌词item在歌词框的高度 = 歌词框的offsetTop - 歌词item的offsetTop
        // console.log(currentLyric);//歌词索引
        // console.log(lyricsArr[currentLyric - 1])//歌词第一句打印的是全部歌词,后面打印的是上一句歌词的div
      if (lyricsArr[currentLyric - 1]) {
        let distance = lyricsArr[currentLyric - 1].offsetTop - lyrics.offsetTop;
        // console.log(lyricsArr[currentLyric - 1].offsetTop)
        // console.log(lyrics.offsetTop)//123
        // console.log(distance)
        //   lyricsArr[currentLyric].scrollIntoView();
        lyrics.scrollTo({
          behavior: "smooth",
          top: distance - placeholderHeight,
        });
      }
    },
    //获取当前歌词索引
    getCurrentLyricsIndex(currentTime) {
      let lyricsIndex = 0;
      this.lyric.some((item) => {
        if (lyricsIndex < this.lyric.length - 1) {
          if (currentTime > item[0]) {
            lyricsIndex += 1;
          }
          return currentTime <= item[0];
        }
      });
      // console.log(lyricsIndex);
      this.lyricsIndex = lyricsIndex;
    },
  },
  watch: {
    // 监听当前播放时间
    "$store.state.currentTime"(currentTime, lastTime) {
      // 如果两个时间间隔有1秒,则可得知进度条被拖动 需要重新校准歌词index
      // 当歌词数量大于1并且索引为零时,可能歌词位置差距较大,走这个if进行快速跳转
      if (
        (lastTime && Math.abs(currentTime - lastTime) >= 1) ||
        (this.lyricsIndex == 0 && this.lyric.length > 1)
      ) {
        // 处理播放时间跳转时歌词位置的校准
        if (this.lyric.length > 1) {
          this.getCurrentLyricsIndex(currentTime);
          // 滑动到当前歌词
          this.lyricScroll(this.lyricsIndex);
        }
      }
      // 根据实时播放时间实现歌词滚动
      if (this.lyricsIndex < this.lyric.length) {
        if (currentTime >= this.lyric[this.lyricsIndex][0]) {
          this.lyricsIndex += 1;
          this.lyricScroll(this.lyricsIndex);
        }
      }
    },
    // 监听vuex中的musicId 重置歌词索引
    "$store.state.musicId"(musicId) {
      this.lyricsIndex = 0;
    },
    lyric(current) {
      // console.log("获取了歌词");
      // 大于一秒,说明歌词在1秒后才请求成功 歌词可能不能马上跳转到当前时间 这里进行校准
      if (this.$store.state.currentTime > 1) {
        // 处理播放时间跳转时歌词位置的校准
        if (this.lyric.length > 1) {
          this.getCurrentLyricsIndex(this.$store.state.currentTime);
          this.$nextTick(() => {
            // 滑动到当前歌词
            this.lyricScroll(this.lyricsIndex);
          });
        }
      }
    },
  },
};
***组件1
<script>
import LyricsScroll from './LyricsScroll.js'
export default {
  mixins: [LyricsScroll]
}
</script>
***组件2
<script>
import LyricsScroll from './LyricsScroll.js'
export default {
  mixins: [LyricsScroll]
}
</script>
后台工具函数
import { ref, reactive, computed } from "vue"
import { toast } from "@/composables/util"
// 列表,分页,搜索,删除,修改状态
export function useInitTable(opt = {}) {
    let searchForm = null
    let resetSearchForm = null
    if (opt.searchForm) {
        searchForm = reactive({ ...opt.searchForm })
        resetSearchForm = () => {
            for (const key in opt.searchForm) {
                searchForm[key] = opt.searchForm[key]
            }
            getData()
        }
    }

    const tableData = ref([])
    const loading = ref(false)

    // 分页
    const currentPage = ref(1)
    const total = ref(0)
    const limit = ref(10)

    // 获取数据
    function getData(p = null) {
        if (typeof p == "number") {
            currentPage.value = p
        }

        loading.value = true
        opt.getList(currentPage.value, searchForm)
            .then(res => {
                if (opt.onGetListSuccess && typeof opt.onGetListSuccess == "function") {
                    opt.onGetListSuccess(res)
                } else {
                    tableData.value = res.list
                    total.value = res.totalCount
                }
            })
            .finally(() => {
                loading.value = false
            })
    }

    getData()

    // 删除
    const handleDelete = (id) => {
        loading.value = true
        opt.delete(id).then(res => {
            toast("删除成功")
            getData()
        }).finally(() => {
            loading.value = false
        })
    }


    // 修改状态
    const handleStatusChange = (status, row) => {
        row.statusLoading = true
        opt.updateStatus(row.id, status)
            .then(res => {
                toast("修改状态成功")
                row.status = status
            })
            .finally(() => {
                row.statusLoading = false
            })
    }

    // 多选选中ID
    const multiSelectionIds = ref([])
    const handleSelectionChange = (e) => {
        multiSelectionIds.value = e.map(o => o.id)
    }
    // 批量删除
    const multipleTableRef = ref(null)
    const handleMultiDelete = () => {
        loading.value = true
        opt.delete(multiSelectionIds.value)
            .then(res => {
                toast("删除成功")
                // 清空选中
                if (multipleTableRef.value) {
                    multipleTableRef.value.clearSelection()
                }
                getData()
            })
            .finally(() => {
                loading.value = false
            })
    }

    // 批量修改状态
    const handleMultiStatusChange = (status) => {
        loading.value = true
        opt.updateStatus(multiSelectionIds.value,status)
            .then(res => {
                toast("修改状态成功")
                // 清空选中
                if (multipleTableRef.value) {
                    multipleTableRef.value.clearSelection()
                }
                getData()
            })
            .finally(() => {
                loading.value = false
            })
    }

    return {
        searchForm,
        resetSearchForm,
        tableData,
        loading,
        currentPage,
        total,
        limit,
        getData,
        handleDelete,
        handleStatusChange,
        handleSelectionChange,
        multipleTableRef,
        handleMultiDelete,
        handleMultiStatusChange,
        multiSelectionIds
    }
}

// 新增,修改
export function useInitForm(opt = {}) {
    // 表单部分
    const formDrawerRef = ref(null)
    const formRef = ref(null)
    const defaultForm = opt.form
    const form = reactive({})
    const rules = opt.rules || {}
    const editId = ref(0)
    const drawerTitle = computed(() => editId.value ? "修改" : "新增")

    const handleSubmit = () => {
        formRef.value.validate((valid) => {
            if (!valid) return

            formDrawerRef.value.showLoading()

            let body = {}
            if(opt.beforeSubmit && typeof opt.beforeSubmit == "function"){
                body = opt.beforeSubmit({ ...form })
            } else {
                body = form
            }

            const fun = editId.value ? opt.update(editId.value, body) : opt.create(body)

            fun.then(res => {
                toast(drawerTitle.value + "成功")
                // 修改刷新当前页,新增刷新第一页
                opt.getData(editId.value ? false : 1)
                formDrawerRef.value.close()
            }).finally(() => {
                formDrawerRef.value.hideLoading()
            })

        })
    }

    // 重置表单
    function resetForm(row = false) {
        if (formRef.value) formRef.value.clearValidate()
        for (const key in defaultForm) {
            form[key] = row[key]
        }
    }

    // 新增
    const handleCreate = () => {
        editId.value = 0
        resetForm(defaultForm)
        formDrawerRef.value.open()
    }

    // 编辑
    const handleEdit = (row) => {
        editId.value = row.id
        resetForm(row)
        formDrawerRef.value.open()
    }

    return {
        formDrawerRef,
        formRef,
        form,
        rules,
        editId,
        drawerTitle,
        handleSubmit,
        resetForm,
        handleCreate,
        handleEdit
    }
}
import { ElNotification,ElMessageBox } from 'element-plus'
import nprogress from 'nprogress'

//提示
export function toast(message,type="success",dangerouslyUseHTMLString=true){
  ElNotification({
    message,
    type,
    duration:3000,
    dangerouslyUseHTMLString
  })
}
export function showModal(content = "提示内容",type = "warning",title = ""){
  return ElMessageBox.confirm(
      content,
      title,
      {
        confirmButtonText: '确认',
        cancelButtonText: '取消',
        type,
      }
    )
}
// 显示全屏loading
export function showFullLoading(){
  nprogress.start()
}

// 隐藏全屏loading
export function hideFullLoading(){
  nprogress.done()
}
// 弹出输入框
export function showPrompt(tip,value = ""){
  return ElMessageBox.prompt(tip, '', {
    confirmButtonText: '确认',
    cancelButtonText: '取消',
    inputValue:value
  })
}
// 将query对象转成url参数
export function queryParams(query){
  let q = []
  for (const key in query) {
      if(query[key]){
          q.push(`${key}=${encodeURIComponent(query[key])}`)
      }
  }
  // console.log(q)//['limit=10', 'keyword=ceshi']
  let r = q.join("&")// limit=10&keyword=ceshi
  r = r ? ("?"+r) : ""
  return r
}
// 上移
export function useArrayMoveUp(arr,index){
  swapArray(arr,index,index - 1)
}

// 下移
export function useArrayMoveDown(arr,index){
  swapArray(arr,index,index + 1)
}

function swapArray(arr,index1,index2){
  arr[index1] = arr.splice(index2,1,arr[index1])[0]
  return arr
}

// sku排列算法
export function cartesianProductOf() {
  return Array.prototype.reduce.call(arguments, function (a, b) {
      var ret = [];
      a.forEach(function (a) {
          b.forEach(function (b) {
              ret.push(a.concat([b]));
          });
      });
      return ret;
  }, [
      []
  ]);
}
import {useCookies} from '@vueuse/integrations/useCookies'
const TokenKey = "admin-token"
const cookie = useCookies()
export function getToken(){
  return cookie.get(TokenKey)
}
export function setToken(token){
  return cookie.set(TokenKey,token)
}
export function removeToken(){
  return cookie.remove(TokenKey)
}
接口工具函数
// 将query对象转成url参数
export function queryParams(query){
  let q = []
  for (const key in query) {
      if(query[key]){
          q.push(`${key}=${encodeURIComponent(query[key])}`)
      }
  }
  // console.log(q)//['limit=10', 'keyword=ceshi']
  let r = q.join("&")// limit=10&keyword=ceshi
  r = r ? ("?"+r) : ""
  return r
}
导出文件
const onSubmit = () => {
  if (!form.tab) return toast('订单类型不能为空', 'error')
  loading.value = true
  let starttime = null
  let endtime = null
  if (form.time && Array.isArray(form.time)) {
    starttime = form.time[0]
    endtime = form.time[1]
  }
  exportOrder({
    tab: form.tab,
    starttime,
    endtime
  })
    .then(data => {
      let url = window.URL.createObjectURL(new Blob([data]))
      //定义一个a标签
      let link = document.createElement('a')
      //隐藏掉a标签
      link.style.display = 'none'
      link.href = url
      //文件命名
      let filename = new Date().getTime() + '.xlsx'
      link.setAttribute('download', filename)
      document.body.appendChild(link)
      link.click()
      close()
    })
    .finally(() => {
      loading.value = false
    })
}
可选链、??
先来看两个场景:
场景1

我需要判断数组对象中的某个值是否存在进而去做其他事情:

let title;
if(data&&data.children&&data.children[0]&&data.children[0].title) {
        title = data.children[0].title
}
场景2

我需要判断某个值是否有效进而去做其他事情

let isMan,text,person = {
    name: 'zhangsan',
    hasCount: 0,
    isMan: false
};
if(person.hasCount || person.hasCount === 0) {
    text = person.hasCount
} else {
    text = '暂无数据'
}
上面两个场景我在开发中经常用到,后来在公众号得知js的新语法可选链"?."以及双问号"??"能使这两个场景操作变得简单。


优化如下

//场景1
let title = data?.children?.[0]?.title
//场景2 
let {hasCount} = person;
text = hasCount ?? '暂无数据'
 
 
 
 
//除此之外,"??"还有其他应用场景
let a;
a ??= 6;
console.log(a); // 6
 
可选链的语法允许开发者访问嵌套得更深的对象属性,而不用担心属性是否真的存在。也就是说,如果可选链在挖掘过程遇到了null或undefined的值,就会通过短路(short-circuit)计算,返回undefined,而不会报错。

逻辑空分配运算符仅在空值或未定义(null  or undefined)时才将值分配给a
富文本编辑器

第一步:

cnpm i tinymce
cnpm i @tinymce/tinymce-vue

第二步:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Qj2DO9yJ-1681570610418)(F:\Roaming\Typora\typora-user-images\image-20230405183528985.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JLusu6CB-1681570610419)(F:\Roaming\Typora\typora-user-images\image-20230405183819682.png)]

第三步:创建editor组件

<template>
    <editor v-model="content" tag-name="div" :init="init" />
    <ChooseImage :preview="false" ref="ChooseImageRef" :limit="9"/>
</template>
<script setup>
import tinymce from "tinymce/tinymce";
import Editor from "@tinymce/tinymce-vue";
import ChooseImage from "@/components/ChooseImage.vue"
import { ref, watch } from "vue"
import "tinymce/themes/silver/theme"; // 引用主题文件
import "tinymce/icons/default"; // 引用图标文件
import 'tinymce/models/dom'
// tinymce插件可按自己的需要进行导入
// 更多插件参考:https://www.tiny.cloud/docs/plugins/
import "tinymce/plugins/advlist"
import "tinymce/plugins/anchor"
import "tinymce/plugins/autolink"
import "tinymce/plugins/autoresize"
import "tinymce/plugins/autosave"
import "tinymce/plugins/charmap" // 特殊字符
import "tinymce/plugins/code" // 查看源码
import "tinymce/plugins/codesample" // 插入代码
import "tinymce/plugins/directionality"
import "tinymce/plugins/emoticons"
import "tinymce/plugins/fullscreen" //全屏
import "tinymce/plugins/help"
import "tinymce/plugins/image" // 插入上传图片插件
import "tinymce/plugins/importcss" //图片工具
import "tinymce/plugins/insertdatetime" //时间插入
import "tinymce/plugins/link"
import "tinymce/plugins/lists" // 列表插件
import "tinymce/plugins/media" // 插入视频插件
import "tinymce/plugins/nonbreaking"
import "tinymce/plugins/pagebreak" //分页
import "tinymce/plugins/preview" // 预览
import "tinymce/plugins/quickbars"
import "tinymce/plugins/save" // 保存
import "tinymce/plugins/searchreplace" //查询替换
import "tinymce/plugins/table" // 插入表格插件
import "tinymce/plugins/template" //插入模板
import "tinymce/plugins/visualblocks"
import "tinymce/plugins/visualchars"
import "tinymce/plugins/wordcount" // 字数统计插件
// v-model
const props = defineProps({
    modelValue: String,
})
const emit = defineEmits(["update:modelValue"])
const ChooseImageRef = ref(null)
// 配置
const init = {
    language_url: '/tinymce/langs/zh-Hans.js', // 中文语言包路径
    language: "zh-Hans",
    skin_url: '/tinymce/skins/ui/oxide', // 编辑器皮肤样式
    content_css: "/tinymce/skins/content/default/content.min.css",
    menubar: false, // 隐藏菜单栏
    autoresize_bottom_margin: 50,
    max_height: 500,
    min_height: 400,
    // height: 320,
    toolbar_mode: "none",
    plugins:
        'wordcount visualchars visualblocks template searchreplace save quickbars preview pagebreak nonbreaking media insertdatetime importcss image help fullscreen directionality codesample code charmap link code table lists advlist anchor autolink autoresize autosave',
    toolbar:
        "formats undo redo fontsizeselect fontselect ltr rtl searchreplace media imageUpload | outdent indent aligncenter alignleft alignright alignjustify lineheight underline quicklink h2 h3 blockquote numlist bullist table removeformat forecolor backcolor bold italic strikethrough hr link preview fullscreen help ",
    content_style: "p {margin: 5px 0; font-size: 14px}",
    fontsize_formats: "12px 14px 16px 18px 24px 36px 48px 56px 72px",
    font_formats:
        "微软雅黑=Microsoft YaHei,Helvetica Neue,PingFang SC,sans-serif;苹果苹方 = PingFang SC, Microsoft YaHei, sans- serif; 宋体 = simsun, serif; 仿宋体 =  FangSong, serif; 黑体 = SimHei, sans - serif; Arial = arial, helvetica, sans - serif;Arial Black = arial black, avant garde;Book Antiqua = book antiqua, palatino; ",
    branding: false,
    elementpath: false,
    resize: false, // 禁止改变大小
    statusbar: false, // 隐藏底部状态栏
    setup:(editor)=>{
        editor.ui.registry.addButton("imageUpload",{
            tooltip:"插入图片",
            icon:"image",
            onAction(){
                ChooseImageRef.value.open((data)=>{
                    data.forEach(url=>{
                        editor.insertContent(`<img src="${url}" style="width:100%;"/>`)
                    })
                })
            }
        })
    }
};
tinymce.init; // 初始化
const content = ref(props.modelValue)
watch(props, (newVal) => content.value = newVal.modelValue)
watch(content, (newVal) => emit("update:modelValue", newVal))
</script>
<style>
.tox-tinymce-aux {
    z-index: 9999 !important;
}
</style>
上传图片
***UploadFile.vue
<template>
  <!-- uploadImageAction接口地址 -->
  <el-upload
    drag
    :action="uploadImageAction"
    multiple
    :headers="{
        token
    }"
    name="img"
    :data="data"
    :on-success="uploadSuccess"
    :on-error="uploadError"
  >
    <el-icon class="el-icon--upload"><upload-filled /></el-icon>
    <div class="el-upload__text">
      在此放置文件或者 <em>点击上传</em>
    </div>
    <template #tip>
      <div class="el-upload__tip">
        大小小于500kb的jpg/png文件
      </div>
    </template>
  </el-upload>
</template>
<script setup>
import { uploadImageAction } from "@/api/image"
import { getToken } from "@/composables/auth"
import { toast } from "@/composables/util"
const token = getToken()

defineProps({
    data:Object,
})

const emit = defineEmits(["success"])

const uploadSuccess = (response, uploadFile, uploadFiles)=>{
    emit("success",{
        response, uploadFile, uploadFiles
    })
    toast("上传成功!")
}

const uploadError = (error, uploadFile, uploadFiles)=>{
    let msg = JSON.parse(error.message).msg || "上传失败"
    toast(msg,"error")
}
</script>

***组件中:
<el-drawer v-model="drawer" title="上传图片">
    <UploadFile :data="{ image_class_id }" @success="handleUploadSuccess" />
</el-drawer>
自定义指令(用于设置有权限的用户才能看见的模块)
***main.js
import permission from "@/directives/permission.js"
app.use(permission)
***permission.js
import store from "@/store"
function hasPermission(value,el = false){
    if(!Array.isArray(value)){
        throw new Error(`需要配置权限,例如 v-permission="['getStatistics3,GET']"`)
    }
    const hasAuth = value.findIndex(v=>store.state.ruleNames.includes(v)) != -1
    if(el && !hasAuth){
        el.parentNode && el.parentNode.removeChild(el)
    }
    return hasAuth
}

export default {
    install(app){
        app.directive("permission",{
            mounted(el,binding){
                hasPermission(binding.value,el)
            }
        })
    }
}
***组件中:
    <el-row :gutter="20" class="mt-5">
      <el-col :span="12" :offset="0">
        <IndexChart v-permission="['getStatistics3,GET']" />
      </el-col>
      <el-col :span="12" :offset="0" v-permission="['getStatistics2,GET']">
        <IndexCard title="店铺及商品提示" tip="店铺及商品提示" :btns="goods" class="mb-3" />
        <IndexCard title="交易提示" tip="需要立即处理的交易订单" :btns="order" />
      </el-col>
    </el-row>
Echarts使用
cnpm i echarts

***IndexChart.vue
<template>
  <el-card shadow="never">
    <template #header>
      <div class="flex justify-between">
        <span class="text-sm">订单统计</span>
        <div>
          <el-check-tag v-for="(item, index) in options" :key="index" :checked="current == item.value" style="margin-right: 8px" @click="handleChoose(item.value)">{{ item.text }}</el-check-tag>
        </div>
      </div>
    </template>
    <div ref="el" id="chart" style="width: 100%; height: 300px"></div>
  </el-card>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import * as echarts from 'echarts'
import { useResizeObserver } from '@vueuse/core'

import { getStatistics3 } from '@/api/index.js'

const current = ref('week')
const options = [
  {
    text: '近1个月',
    value: 'month'
  },
  {
    text: '近1周',
    value: 'week'
  },
  {
    text: '近24小时',
    value: 'hour'
  }
]

const handleChoose = type => {
  current.value = type
  getData()
}

var myChart = null
onMounted(() => {
  var chartDom = document.getElementById('chart')
  if (chartDom) {
    myChart = echarts.init(chartDom)
    getData()
  }
})

onBeforeUnmount(() => {
  if (myChart) echarts.dispose(myChart)
})

function getData() {
  let option = {
    xAxis: {
      type: 'category',
      data: []
    },
    yAxis: {
      type: 'value'
    },
    series: [
      {
        data: [],
        type: 'bar',
        showBackground: true,
        backgroundStyle: {
          color: 'rgba(180, 180, 180, 0.2)'
        }
      }
    ]
  }

  myChart.showLoading()
  getStatistics3(current.value)
    .then(res => {
      option.xAxis.data = res.x
      option.series[0].data = res.y

      myChart.setOption(option)
    })
    .finally(() => {
      myChart.hideLoading()
    })
}
//图标等比例缩小和放大
const el = ref(null)
if(myChart){
  useResizeObserver(el, entries => myChart.resize())
}
</script>

***index.vue
<el-row :gutter="20" class="mt-5">
      <el-col :span="12" :offset="0">
        <IndexChart/>
      </el-col>
</el-row>
通过gsap库实现数字滚动变化
cnpm i gsap


<template>
  {{ d.num.toFixed(0) }}
</template>
<script setup>
import { reactive,watch } from "vue"
import gsap from "gsap"

const props = defineProps({
  value:{
      type:Number,
      default:0
  }
})

const d = reactive({
  num:0
})

function AnimateToValue(){
  gsap.to(d,{
      duration:0.5,
      num:props.value
  })
}

AnimateToValue()

watch(()=>props.value,()=>AnimateToValue())

</script>

vue3暴露给父组件props、方法、事件
    import { ref } from "vue"

    const showDrawer = ref(false)

    const props = defineProps({
        title:String,
        size:{
            type:String,
            default:"45%"
        },
        destroyOnClose:{
            type:Boolean,
            default:false
        },
        confirmText:{
            type:String,
            default:"提交"
        }
    })

    const loading = ref(false)
    const showLoading = ()=>loading.value = true
    const hideLoading = ()=>loading.value = false

    // 打开
    const open = ()=> showDrawer.value = true

    // 取消
    const close = ()=>showDrawer.value = false

    // 提交,传事件给父组件
    const emit = defineEmits(["submit"])
    const submit = ()=> emit("submit")

    // 向父组件暴露以下方法
    defineExpose({
        open,
        close,
        showLoading,
        hideLoading
    })
vue3实现全屏显示
cnpm i @vueuse/core
import { useFullscreen } from '@vueuse/core'
const {
  // 是否全屏状态
  isFullscreen,
  // 切换全屏
  toggle
} = useFullscreen()
***组件中
<el-icon class="icon-btn" @click="toggle">
          <full-screen v-if="!isFullscreen" />
          <aim v-else />
</el-icon>
进度条nprogress实现
cnpm i nprogress
***main.js
import "nprogress/nprogress.css"
actions中的写法
法一:
async deleteCartListBySkuId({ commit }, skuId) {
    let result = await reqDeleteCartById(skuId)
    if (result.code == 200) {
      return 'ok '
    } else {
      return Promise.reject(new Error('faile'))
    }
},
法二:
getInfo({commit}){
      return new Promise((resolve,reject)=>{
        getInfo().then(res=>{
          commit("SET_USERINFO",res)
          resolve(res)
        }).catch(err=>reject(err))
      })
}
通过VueUse使用cookie,封装token
法一:
cnpm i @vueuse/integrations
cnpm i universal-cookie

import {useCookies} from '@vueuse/integrations/useCookies'
const TokenKey = "admin-token"
const cookie = useCookies()
export function getToken(){
  return cookie.get(TokenKey)
}
export function setToken(token){
  return cookie.set(TokenKey,token)
}
export function removeToken(){
  return cookie.remove(TokenKey)
}
法二:
//对外暴露一个函数
//存储token
export const setToken = token => {
  localStorage.setItem('TOKEN', token)
}
//获取token
export const getToken = () => {
  return localStorage.getItem('TOKEN')
}
//清除本地token
export const removeToken = () => {
  localStorage.removeItem('TOKEN')
}
vite配置@地址、windi css、跨域
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import WindiCSS from 'vite-plugin-windicss'
import path from "path"
// https://vitejs.dev/config/
export default defineConfig({
  resolve:{
    alias:{
      "@":path.resolve(__dirname,"src")
    }
  },
  server:{
    proxy:{
      '/api': {
        target: 'http://ceshi13.dishait.cn',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      },
    }
  },
  plugins: [vue(),WindiCSS()],
})

JS高级

this指向(new绑定)
/*
      1.创建新的空对象
      2.将this指向这个空对象
      3.执行函数体中的代码
      4.没有显示返回非空对象时, 默认返回这个对象
    */
    function foo() {
      this.name = "why"
      console.log("foo函数:", this)
    }

    new foo()
this指向(显示绑定)
// 显式绑定
    var obj = {
      name: "why"
    }

    function foo() {
      console.log("foo函数:", this)
    }

    // 执行函数, 并且函数中的this指向obj对象
    // obj.foo = foo
    // obj.foo()

    // 执行函数, 并且强制this就是obj对象
    foo.call(obj)
    foo.call(123)
    foo.call("abc")

split

例子 1

在本例中,我们将按照不同的方式来分割字符串:

<script type="text/javascript">

var str="How are you doing today?"

document.write(str.split(" ") + "<br />")
document.write(str.split("") + "<br />")
document.write(str.split(" ",3))

</script>

输出:

How,are,you,doing,today?
H,o,w, ,a,r,e, ,y,o,u, ,d,o,i,n,g, ,t,o,d,a,y,?
How,are,you

例子 2

在本例中,我们将分割结构更为复杂的字符串:

"2:3:4:5".split(":")	//将返回["2", "3", "4", "5"]
"|a|b|c".split("|")	//将返回["", "a", "b", "c"]

slice 从索引start开始(包括start)至end(不包括end)

例子 1

在本例中,我们将提取从位置 6 开始的所有字符:

<script type="text/javascript">

var str="Hello happy world!"
document.write(str.slice(6))

</script>

输出:

happy world!

例子 2

在本例中,我们将提取从位置 6 到位置 11 的所有字符:

<script type="text/javascript">

var str="Hello happy world!"
document.write(str.slice(6,11))

</script>

输出:

happy

substr

例子 1

在本例中,我们将使用 substr() 从字符串中提取一些字符:

<script type="text/javascript">

var str="Hello world!"
document.write(str.substr(3))

</script>

输出:

lo world!

例子 2

在本例中,我们将使用 substr() 从字符串中提取一些字符:

<script type="text/javascript">

var str="Hello world!"
document.write(str.substr(3,7))

</script>

输出:

lo worl

小程序笔记

后台播放
const audioContext = wx.getBackgroundAudioManager()
//需要设置后台播放的歌名
audioContext.title = res.songs[0].name
顺序播放、随机播放、单曲循环
changeNewMusicAction(ctx, isNext = true) {
      // 1.获取当前索引
      let index = ctx.playListIndex

      // 2.根据不同的播放模式, 获取下一首歌的索引
      switch(ctx.playModeIndex) {
        case 0: // 顺序播放
          index = isNext ? index + 1: index -1
          if (index === -1) index = ctx.playListSongs.length - 1
          if (index === ctx.playListSongs.length) index = 0
          break
        case 1: // 单曲循环
          break
        case 2: // 随机播放
          index = Math.floor(Math.random() * ctx.playListSongs.length)
          break
      }

      console.log(index)

      // 3.获取歌曲
      let currentSong = ctx.playListSongs[index]
      if (!currentSong) {
        currentSong = ctx.currentSong
      } else {
        // 记录最新的索引
        ctx.playListIndex = index
      }

      // 4.播放新的歌曲
      this.dispatch("playMusicWithSongIdAction", { id: currentSong.id, isRefresh: true })
    }
var,let,const三者的特点和区别
https://blog.csdn.net/xiewenhui111/article/details/113133330
歌词滚动
<swiper-item class="lyric">
		<scroll-view class="lyric-list" scroll-y scroll-top="{{lyricScrollTop}}" scroll-with-animation>
			<block wx:for="{{lyricInfos}}" wx:key="index">
				<view class="item {{currentLyricIndex === index ? 'active': ''}}" style="padding-top: {{index === 0 ? (contentHeight/2-80): 0}}px; padding-bottom: {{index === lyricInfos.length - 1 ? (contentHeight/2+80): 0}}px;">
					{{item.text}}
				</view>
			</block>
		</scroll-view>
</swiper-item>
目标歌词展示
//获取当前时间
			const currentTime = audioContext.currentTime * 1000
			//根据当前时间修改currentTime/sliderValue
			if(!this.data.isSliderChanging){	
				const sliderValue = currentTime / this.data.durationTime * 100
				this.setData({sliderValue,currentTime})
			}
			//根据当前时间去查找播放的歌词
			let i = 0
			for (; i < this.data.lyricInfos.length; i++) {
				const lyricInfo = this.data.lyricInfos[i]
				if (currentTime < lyricInfo.time) {
					// 设置当前歌词的索引和内容
					//此处i为上面循环结束后拿到的后一句的i,要i-1才是当前的
					const currentIndex = i - 1
					if (this.data.currentLyricIndex !== currentIndex) {
						const currentLyricInfo = this.data.lyricInfos[currentIndex]
						console.log(currentLyricInfo.text);
						this.setData({ currentLyricText: currentLyricInfo.text, currentLyricIndex: currentIndex })
					}
					break
				}
			}
歌词转换
// 正则(regular)表达式(expression): 字符串匹配利器

// [00:58.65]  \是对[]和.转义
const timeRegExp = /\[(\d{2}):(\d{2})\.(\d{2,3})\]/

export function parseLyric(lyricString) {
  const lyricStrings = lyricString.split("\n")

  const lyricInfos = []
  for (const lineString of lyricStrings) {
    // [00:58.65]他们说 要缝好你的伤 没有人爱小丑
    const timeResult = timeRegExp.exec(lineString)
    if (!timeResult) continue
    // 1.获取时间
    const minute = timeResult[1] * 60 * 1000
    const second = timeResult[2] * 1000
    const millsecondTime = timeResult[3]
    const millsecond = millsecondTime.length === 2 ? millsecondTime * 10: millsecondTime * 1
    const time = minute + second + millsecond

    // 2.获取歌词文
    const text = lineString.replace(timeRegExp, "")
    lyricInfos.push({ time, text })
  }
  return lyricInfos
}
音乐播放器
// pages/music-player/index.js
import {getSongDetail,getSongLyric} from '../../services/api_player'
import {audioContext} from '../../store/player-store'
import {parseLyric} from '../../utils/parse-lyric'
Page({
	data: {
		id:0,
		currentSong:{},
		currentPage:0,
		contentHeight:0,
		//显示歌词
		isMusicLyric:true,
		//总时长
		durationTime:0,
		//当前时间
		currentTime:0,
		//滑动到的时间(百分比)
		sliderValue:0,
		//是否在滑动
		isSliderChanging:false,
		//歌词
		lyricInfos:[],
		//当前播放歌词
		currentLyricText:"",
		//当前播放歌词索引
		currentLyricIndex:0,
		//要滚动的距离
		lyricScrollTop:0
	},

	onLoad(options) {
		const id = options.id
		this.setData({id})
		this.getPageData(id)
		//动态计算高度,宽度
		const screenHeight = getApp().globalData.screenHeight
		const statusBarHeight = getApp().globalData.statusBarHeight
		const navBarHeight = getApp().globalData.navBarHeight
		const contentHeight = screenHeight - statusBarHeight - navBarHeight
		const deviceRadio = getApp().globalData.deviceRadio
		this.setData({contentHeight,isMusicLyric:deviceRadio >= 2})
		//创建播放器
		audioContext.stop()
		audioContext.src = `https://music.163.com/song/media/outer/url?id=${id}.mp3`
		audioContext.autoplay = true
		//audioContext事件监听
		this.setupAudioContextListener()
	},
// ========================   网络请求   ======================== 
	getPageData(id){
		getSongDetail(id).then(res => {
			this.setData({currentSong:res.songs[0],durationTime: res.songs[0].dt})
		})
		getSongLyric(id).then(res => {
			const lyricString = res.lrc.lyric
			const lyric = parseLyric(lyricString)
			this.setData({lyricInfos:lyric})
		})
	},
// ========================   事件处理   ======================== 
	handleSwiperChange(event){
		const current = event.detail.current
		this.setData({currentPage:current})
	},
	handleSliderChange(event){
		// 1.获取slider变化值(百分比)
		const value = event.detail.value
		// 2.计算需要播放的currentTime
		const currentTime = this.data.durationTime * value / 100
		// 3.设置context播放currentTime位置的音乐
		audioContext.pause()
		audioContext.seek(currentTime / 1000)
		// 4.记录最新的sliderValue, 并且需要讲isSliderChaning设置回false
		this.setData({ sliderValue: value, isSliderChanging: false })
	},
	handleSliderChanging(event){
		const value = event.detail.value
    	const currentTime = this.data.durationTime * value / 100
    	this.setData({ isSliderChanging: true, currentTime})
	},
	//事件监听
	setupAudioContextListener(){
		audioContext.onCanplay(() => {
			audioContext.play()
		})
		audioContext.onTimeUpdate(() => {
			//获取当前时间
			const currentTime = audioContext.currentTime * 1000
			//根据当前时间修改currentTime/sliderValue
			if(!this.data.isSliderChanging){	
				const sliderValue = currentTime / this.data.durationTime * 100
				this.setData({sliderValue,currentTime})
			}
			//根据当前时间去查找播放的歌词
			let i = 0
			for (; i < this.data.lyricInfos.length; i++) {
				const lyricInfo = this.data.lyricInfos[i]
				if (currentTime < lyricInfo.time) {
					// 设置当前歌词的索引和内容
					//此处i为上面循环结束后拿到的后一句的i,要i-1才是当前的
					const currentIndex = i - 1
					if (this.data.currentLyricIndex !== currentIndex) {
						const currentLyricInfo = this.data.lyricInfos[currentIndex]
						this.setData({ currentLyricText: currentLyricInfo.text, currentLyricIndex: currentIndex ,lyricScrollTop:currentIndex * 35})
					}
					break
				}
			}
		})
	}
})
image mode的属性
mode 有效值:

mode 有 13 种模式,其中 4 种是缩放模式,9 种是裁剪模式。

模式 值 说明
缩放 scaleToFill 不保持纵横比缩放图片,使图片的宽高完全拉伸至填满 image 元素
缩放 aspectFit 保持纵横比缩放图片,使图片的长边能完全显示出来。也就是说,可以完整地将图片显示出来。
缩放 aspectFill 保持纵横比缩放图片,只保证图片的短边能完全显示出来。也就是说,图片通常只在水平或垂直方向是完整的,另一个方向将会发生截取。
缩放 widthFix 宽度不变,高度自动变化,保持原图宽高比不变
裁剪 top 不缩放图片,只显示图片的顶部区域
裁剪 bottom 不缩放图片,只显示图片的底部区域
裁剪 center 不缩放图片,只显示图片的中间区域
裁剪 left 不缩放图片,只显示图片的左边区域
裁剪 right 不缩放图片,只显示图片的右边区域
裁剪 top left 不缩放图片,只显示图片的左上边区域
裁剪 top right 不缩放图片,只显示图片的右上边区域
裁剪 bottom left 不缩放图片,只显示图片的左下边区域
裁剪 bottom right 不缩放图片,只显示图片的右下边区域
使用多个插槽,要设置
//想使用多个插槽,要设置
Component({
	 options: {
  		multipleSlots: true
 	},
})
搜索关键字的高亮以及rich-text的使用
****string2nodes.js

export default function stringToNodes(keyword, value) {
	const nodes = []
	if (keyword.toUpperCase().startsWith(value.toUpperCase())) {
	  const key1 = keyword.slice(0, value.length)
	  const node1 = {
		name: "span",
		attrs: { style: "color: #26ce8a; font-size: 14px;" },
		children: [ { type: "text", text: key1 } ]
	  }
	  nodes.push(node1)
  
	  const key2 = keyword.slice(value.length)
	  const node2 = {
		name: "span",
		attrs: { style: "color: #000000; font-size: 14px;" },
		children: [ { type: "text", text: key2 } ]
	  }
	  nodes.push(node2)
	} else {
	  const node = {
		name: "span",
		attrs: { style: "color: #000000; font-size: 14px;" },
		children: [ { type: "text", text: keyword } ]
	  } 
	  nodes.push(node)
	}
	return nodes
  }
  
  ****组件内
  import stringToNodes from '../../utils/string2nodes'
  
  handleSearch(event){
		//获取输入的关键字
		const searchValue = event.detail
		this.setData({searchValue})
		if (!searchValue.length){
			this.setData({suggestSongs:[]})
			return
		} 
		debounceGetSearchSuggest(searchValue).then(res => {
			const suggestSongs = res.result.allMatch || []
			this.setData({suggestSongs})
			// 转成nodes节点
			const suggestKeywords = suggestSongs.map(item => item.keyword)
			const suggestSongsNodes = []
			for( const item of suggestKeywords){
				const nodes = stringToNodes(item,searchValue)
				suggestSongsNodes.push(nodes)
			}
			this.setData({suggestSongsNodes})
		})
	}
防抖使用(搜索框)
**组件内
import debounce from '../../utils/debounce'
const debounceGetSearchSuggest = debounce(getSearchSuggest,600)

debounceGetSearchSuggest(searchValue).then(res => {
			this.setData({suggestSongs:res.result.allMatch})
})

**debounce.js
export default function debounce(fn, delay = 500, immediate = false, resultCallback) {
	// 1.定义一个定时器, 保存上一次的定时器
	let timer = null
	let isInvoke = false
  
	// 2.真正执行的函数
	//...args是searchValue,输入的文字
	const _debounce = function(...args) {
	  return new Promise((resolve, reject) => {
		// 取消上一次的定时器
		if (timer) clearTimeout(timer)
  
		// 判断是否需要立即执行
		if (immediate && !isInvoke) {
		  const result = fn.apply(this, args)
		  if (resultCallback) resultCallback(result)
		  resolve(result)
		  isInvoke = true
		} else {
		  // 延迟执行
		  timer = setTimeout(() => {
			// 外部传入的真正要执行的函数
			const result = fn.apply(this, args)
			if (resultCallback) resultCallback(result)
			resolve(result)
			isInvoke = false
			timer = null
		  }, delay)
		}
	  })
	}
  
	// 封装取消功能
	_debounce.cancel = function() {
	  console.log(timer)
	  if (timer) clearTimeout(timer)
	  timer = null
	  isInvoke = false
	}
  
	return _debounce
  }
  
  
export和export default的区别

export和export default的区别 - 知乎 (zhihu.com)

data-使用以及动态key数组
	<block>
      <ranking-area-item item="{{originalRankingsongs}}" bindtap="handleMoreClickBtn" data-id="1"></ranking-area-item>
      <ranking-area-item item="{{newRankingsongs}}" bindtap="handleMoreClickBtn" data-id="2"></ranking-area-item>
      <ranking-area-item item="{{soarRankingsongs}}" bindtap="handleMoreClickBtn" data-id="3"></ranking-area-item>
    </block>
    
    
    **data-id  id为自定义名称,为下面事件event参数中添加一个id属性,rankingMap[id]动态id获取rankingMap数组中的value
    
    //排行榜点击事件
	handleMoreClickBtn(event){
		const rankingMap = {1:"originalRanking",2:"newRanking",3:"soarRanking"}
		const id = event.currentTarget.dataset.id
		const rankingName = rankingMap[id]
		this.navigateToDeatail(rankingName)
	}
引入hy-event-store,达到vuex效果
cnpm i hy-event-store

**index.js
import {rankingStore} from './ranking-store'
export {rankingStore}

**ranking-store.js
import {
	HYEventStore
} from 'hy-event-store'
import {
	getRankings
} from '../services/api_music'
const rankingStore = new HYEventStore({
	state: {
		hotRanking: {}
	},
	actions: {
		getRankingDataAction(ctx) {
			getRankings(3778678).then((res) => {
				console.log(res);
				ctx.hotRanking = res.playlist
			})
		}
	}
})
export {
	rankingStore
}

**组件
import {rankingStore} from '../../store/index'

onLoad(options) {
		this.getPageData()
		// 获取推荐音乐数据
		rankingStore.dispatch('getRankingDataAction')
		// 从store中获取数据
		rankingStore.onState("hotRanking",(res) =>{
			//刚开始的hotRanking为空对象
			if(!res.tracks) return
			const recommendSongs = res.tracks.slice(0,7)
			this.setData({recommendSongs})
		})
	},
小程序解决插槽动态显示方案
  **header.wxss
  
  .header .slot:empty + .default {
	display: flex;
  }
  .header .default {
	display: none;
	align-items: center;
	font-size: 28rpx;
	color: #777;
  }
  **header.wxml
  
  <!--components/area-header/index.wxml-->
<view class="header">
  <view class="title">{{title}}</view>
  <view class="right" wx:if="{{showRight}}" bindtap="handleRightClick">
    <view class="slot"><slot></slot></view>
    <view class="default">
      <text>{{rightText}}</text>
      <image class="icon" src="/assets/images/icons/arrow-right.png"></image>
    </view>
  </view>
</view>
**home.wxml

<!-- 推荐歌曲 -->
<view class="recommend-song">
	<header title="推荐歌曲"></header>
</view>
节流(规定时间内只能调用一次,普攻)
**throttle.js

export default function throttle(fn, interval = 1000, options = { leading: true, trailing: false }) {

  // 1.记录上一次的开始时间

  const { leading, trailing, resultCallback } = options

  let lastTime = 0

  let timer = null

 

  // 2.事件触发时, 真正执行的函数

  const _throttle = function(...args) {

   return new Promise((resolve, reject) => {

​    // 2.1.获取当前事件触发时的时间

​    const nowTime = new Date().getTime()

​    if (!lastTime && !leading) lastTime = nowTime

 

​    // 2.2.使用当前触发的时间和之前的时间间隔以及上一次开始的时间, 计算出还剩余多长事件需要去触发函数

​    const remainTime = interval - (nowTime - lastTime)

​    if (remainTime <= 0) {

​     if (timer) {

​      clearTimeout(timer)

​      timer = null

​     }

 

​     // 2.3.真正触发函数

​     const result = fn.apply(this, args)

​     if (resultCallback) resultCallback(result)

​     resolve(result)

​     // 2.4.保留上次触发的时间

​     lastTime = nowTime

​     return

​    }

 

​    if (trailing && !timer) {

​     timer = setTimeout(() => {

​      timer = null

​      lastTime = !leading ? 0: new Date().getTime()

​      const result = fn.apply(this, args)

​      if (resultCallback) resultCallback(result)

​      resolve(result)

​     }, remainTime)

​    }

   })

  }

 

  _throttle.cancel = function() {

   if(timer) clearTimeout(timer)

   timer = null

   lastTime = 0

  }
  return _throttle
 }
 
 
组件引用:

import throttle from '../../utils/throttle'
const throttleQueryRect = throttle(queryRect)

//动态计算swiper高度,防止手机不同样式不同
	handleSwiperHeight() {
		throttleQueryRect(".swiper-image").then(res =>{
			const rect = res[0]
			this.setData({swiperHeight:rect.height})
		})
}
导入vant weapp
npm init -y
cnpm i @vant/weapp@1.3.3 -S--production
将 app.json 中的 "style": "v2" 去除,小程序的新版基础组件强行加上了许多样式,难以覆盖,不关闭将造成部分组件样式混乱。
{
	"usingComponents": {
		"van-search": "@vant/weapp/search/index"
	}
}

打开微信开发者工具,点击 工具 -> 构建 npm,并勾选 使用 npm 模块 选项,构建完成后,即可引入组件。
动态计算swiper高度,防止手机不同样式不同
<swiper-item class="swiper-item">
			<image src="{{item.pic}}" mode="widthFix" class="swiper-image" bindload="handleSwiperHeight"/>
</swiper-item>
  
  
  swiperHeight:0
  
  handleSwiperHeight() {

​    //获取图片的高度

​    const query = wx.createSelectorQuery()

​    query.select('.swiper-image').boundingClientRect()

​    query.exec((res) =>{

​      const rect = res[0]

​      this.setData({swiperHeight:rect.height})

​    })

  }
(.wxs)日期和数量格式化
function formatCount(count) {
	var counter = parseInt(count)
	if (counter>100000000) {
		return (counter/100000000).toFixed(1) + '亿'
	}else if (counter>10000) {
		return (counter/10000).toFixed(1) + '万'
	}else{
		return counter + ''
	}
}
function addZero(time) {
	time = time + ''
	return ('00' + time).slice(time.length)
}
function formatDuration(time) {
	time = time/1000
	var minut = Math.floor(time/60)
	var second = Math.floor(time) % 60
	return addZero(minut) + ':' + addZero(second)
}
// commonjs
module .exports={
	formatCount:formatCount,
	formatDuration:formatDuration
}
<wxs src="../../utils/format.wxs" module="format"/>

<view class="count">
​     {{format.formatCount(item.playCount)}}
</view>
data-xxx的使用
data-xx 的作用是在事件中可以获取这些自定义的节点数据,用于事件的逻辑处理

比如 写一个list列表 想知道点击的list列表的那一个item ,比如获取点击的图片,等等

使用data-xx 需要注意的 xx 是自己取的名字, 后面跟着的渲染一定要是使用的值,否则无效

比如点击的是list 后面跟的是list的值,如果是图片后面就要是图片url的地址,
————————————————
版权声明:本文为CSDN博主「胡小牧」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_33210042/article/details/91983464

<view wx:for="{{topMvs}}" wx:key="id" class="item">
		<video-item-v1 item='{{item}}' bindtap="VideoItemBtn" data-item="{{item}}"></video-item-v1>
</view>


VideoItemBtn(event){
		const id = event.currentTarget.dataset.item.id
		//页面跳转
		wx.navigateTo({
		  url: `/pages/detail-video/index?id=${id}`,
		})
	}
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 10
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值