20230726----重返学习-vue3项目实战-知乎日报第3天-TS-简历

day-121-one-hundred-and-twenty-one-20230726-vue3项目实战-知乎日报第3天-TS-简历

vue3项目实战-知乎日报第3天

封装按钮组件

jsx函数式组件
  1. 只能做静态页面,内部没有方法让它自动更新。
封装第三方按钮-非计算属性版
  1. 封装第三方按钮-不使用计算属性
  • src/components/ButtonAgain.jsx
import { Button } from 'vant'
import { ref, useAttrs, useSlots } from 'vue'

// 把传递的属性,去除特殊的,其余的都赋值给Vant内部的组件
const filter = (attrs) => {
  let props = {}
  Reflect.ownKeys(attrs).forEach((key) => {
    if (key === 'loading' || key === 'onClick') return
    props[key] = attrs[key]
  })
  return props
}

const ButtonAgain = {
  inheritAttrs: false,
  setup() {
    const attrs = useAttrs(),
      slots = useSlots()

    // 自己控制loading效果
    const loading = ref(false)
    const handle = async (ev) => {
      loading.value = true
      try {
        await attrs.onClick(ev)
      } catch (_) {}
      loading.value = false
    }

    console.log(`1- 非计算属性版`)
    return () => {
    console.log(`2- 非计算属性版`)
      let props = filter(useAttrs())
      return (
        <Button {...props} loading={loading.value} onClick={handle}>
          {slots.default()}
        </Button>
      )
    }
  }
}
export default ButtonAgain
封装第三方按钮计算属性版
  1. 封装第三方按钮-使用计算属性。
  • src/components/ButtonAgain.jsx
import { Button } from 'vant'
import { ref, useAttrs, useSlots, computed } from 'vue'

const ButtonAgain = {
  inheritAttrs: false,
  setup() {
    const attrs = useAttrs(),
      slots = useSlots()
    const props = computed(() => {
      let attrs = useAttrs()
      let props = {}
      Reflect.ownKeys(attrs).forEach((key) => {
        if (key === 'loading' || key === 'onClick') return
        props[key] = attrs[key]
      })
      return props
    })

    // 自己控制loading效果
    const loading = ref(false)
    const handle = async (ev) => {
      loading.value = true
      try {
        await attrs.onClick(ev)
      } catch (_) {}
      loading.value = false
    }

    console.log(`计算属性版`)

    return () => {
      return (
        <Button {...props.value} loading={loading.value} onClick={handle}>
          {slots.default()}
        </Button>
      )
    }
  }
}
export default ButtonAgain
函数式调用组件的处理优化
  • src/components/overlay/Index.vue
<script setup></script>
<template>
  <van-overlay show>
    <van-loading color="#0094ff" vertical> 努力加载中,请稍后 </van-loading>
  </van-overlay>
</template>
<style lang="less" scoped>
.van-overlay {
  display: flex;
  align-items: center;
  justify-content: center;
}
</style>
  • src/components/overlay/index.js
import { createVNode, render } from 'vue'
import Index from './Index.vue'

export default function showOverlayLoading() {
  // 创建虚拟DOM。
  let vnode = createVNode(Index)
  // 渲染虚拟DOM
  // console.log(`vnode-->`, vnode);

  const frag = document.createDocumentFragment()
  render(vnode, frag)
  document.body.appendChild(vnode.el, frag)

  return function hiddenOverlayLoading() {
    if (vnode?.el) {
      document.body.removeChild(vnode.el)
      vnode = null
    }
    // render(null, frag)
  }
}

登录页

<script setup>
import useBaseStore from '@/stores/base'
import useAutoImport from '@/useAutoImport'
const { reactive, ref, onUnmounted, API, router, route, showSuccessToast, showFailToast, utils } =
  useAutoImport()

const baseStore = useBaseStore()
console.log(`baseStore-->`, baseStore)

/* 定义状态 */
const formIns = ref(null)
const state = reactive({
  phone: '',
  code: '',
  btn: {
    disabled: false,
    text: '发送验证码'
  }
})

/* 发送验证码 */
let timer = null,
  count = 30
const handleSendCode = async () => {
  try {
    // 先对手机号进行校验
    await formIns.value.validate('phone')
    // 向服务器发送请求
    let { code } = await API.userSendCode(state.phone)
    if (+code === 0) {
      // 开启倒计时
      state.btn.disabled = true
      state.btn.text = `30s后重发`
      timer = setInterval(() => {
        if (count === 1) {
          clearInterval(timer)
          count = 30
          state.btn.disabled = false
          state.btn.text = `发送验证码`
          return
        }
        count--
        state.btn.text = `${count}s后重发`
      }, 1000)
      return
    }
    showFailToast('发送失败,稍后再试')
  } catch (_) {}
}
onUnmounted(() => clearInterval(timer))

/* 登录提交 */
const submit = async () => {
  try {
    await formIns.value.validate()
    let { code, token } = await API.userLogin(state.phone, state.code)
    if (+code !== 0) {
      showFailToast('登录失败,请稍后再试')
      return
    }
    // 登录成功:存储Token、获取登录者信息、提示、跳转
    utils.storage.set('TK', token)
    await baseStore.queryProfile()
    showSuccessToast('登录成功')
    router.push('/')
  } catch (_) {}
}
</script>

<template>
  <nav-back title="登录/注册" />
  <van-form ref="formIns" validate-first>
    <van-cell-group inset>
      <van-field
        center
        label="手机号"
        label-width="50px"
        name="phone"
        v-model.trim="state.phone"
        :rules="[
          { required: true, message: '手机号是必填项' },
          { pattern: /^(?:(?:\+|00)86)?1\d{10}$/, message: '手机号格式不正确' }
        ]"
      >
        <template #button>
          <button-again
            class="form-btn"
            size="small"
            type="primary"
            loading-text="处理中"
            :disabled="state.btn.disabled"
            @click="handleSendCode"
          >
            {{ state.btn.text }}
          </button-again>
        </template>
      </van-field>

      <van-field
        label="验证码"
        label-width="50px"
        name="code"
        v-model.trim="state.code"
        :rules="[
          { required: true, message: '验证码是必填项' },
          { pattern: /^\d{6}$/, message: '验证码格式不正确' }
        ]"
      />
    </van-cell-group>

    <div style="margin: 20px 40px">
      <ButtonAgain round block type="primary" loading-text="正在处理中..." @click="submit">
        立即登录/注册
      </ButtonAgain>
    </div>
  </van-form>
</template>

<style lang="less" scoped>
.van-form {
  margin-top: 30px;

  .form-btn {
    width: 78px;
  }
}
</style>
提交表单信息
  1. 对表单进行校验。
  2. 发送请求。
  3. 登录成功:存储token、进行提示。
  4. 获取登录者信息、进行页面的跳转。
获取登录者信息
  1. 从服务器获取登录者信息。
    • 一般是在pinia中创建出来的。
      • src/stores/base.js

        import { defineStore } from 'pinia'
        import { ref } from 'vue'
        import API from '@/api'
        
        const useBaseStore = defineStore('base', () => {
          // 定义公共状态。
          const profile = ref(null)
        
          // 修改公共状态。
          // 
          const queryProfile = async () => {
            let info = null
            try {
              let { code, data } = await API.userInfo()
              if (code === 0) {
                info = data
                profile.value = info
              }
            } catch (error) {
              console.log(`error:-->`, error)
            }
            return info
          }
          // 暴露给外面用。
          return {
            profile,
            queryProfile
          }
        })
        export default useBaseStore
        

登录态校验

  • src/router/index.js

    import { createRouter, createWebHashHistory } from 'vue-router'
    import routes from './routes'
    import useBaseStore from '@/stores/base'
    import { showFailToast } from 'vant'
    
    
    const router = createRouter({
      history: createWebHashHistory(),
      routes
    })
    
    // 全局前置守卫:登录态校验
    const checkList = ['/person', '/store', '/update']//用于判断那些页面需要登录态校验。
    router.beforeEach(async (to, from, next) => {
      const base = useBaseStore()//用于拿到个人信息。
      let profile = base.profile
      if (checkList.includes(to.path) && !profile?.value) {
        let info = await base.queryProfile()
        if (!info) {
          // 真的没登录过。
          showFailToast('您还未登录,请先登录')
          next({
            path: '/login',
            query: {
              target: to.fullPath
            }
          })
          return
        }
      }
    
      next()
    })
    // 全局后置守卫
    router.beforeEach((to, from) => {
    
    })
    export default router
    
  • src/views/Login.vue

    <script setup>
    import useBaseStore from '@/stores/base'
    import useAutoImport from '@/useAutoImport'
    const { reactive, ref, onUnmounted, API, router, route, showSuccessToast, showFailToast, utils } =
      useAutoImport()
    
    const baseStore = useBaseStore()
    console.log(`baseStore-->`, baseStore)
    
    /* 定义状态 */
    const formIns = ref(null)
    const state = reactive({
      phone: '',
      code: '',
      btn: {
        disabled: false,
        text: '发送验证码'
      }
    })
    /* 登录提交 */
    const submit = async () => {
      try {
        await formIns.value.validate()
        let { code, token } = await API.userLogin(state.phone, state.code)
        if (+code !== 0) {
          showFailToast('登录失败,请稍后再试')
          return
        }
        // 登录成功:存储Token、获取登录者信息、提示、跳转
        utils.storage.set('TK', token)
        await baseStore.queryProfile()
        showSuccessToast('登录成功')
    
        let target = route.query.target
        target ? router.replace(target) : router.push('/')
      } catch (_) {}
    }
    </script>
    
    <template>
        <div style="margin: 20px 40px">
          <ButtonAgain round block type="primary" loading-text="正在处理中..." @click="submit">
            立即登录/注册
          </ButtonAgain>
        </div>
      </van-form>
    </template>
    
    <script setup>
    /* 登录提交 */
    const submit = async () => {
      try {
        showSuccessToast('登录成功')
    
        let target = route.query.target
        target ? router.replace(target) : router.push('/')
      } catch (_) {}
    }
    </script>
    
    
  • 会有一个问题-路由错乱的问题。

登录页的跳转
  1. 让登录页中可以直接跳转回来源页面。
  • src/views/Login.vue
<script setup>
/* 登录提交 */
const submit = async () => {
  try {
    showSuccessToast('登录成功')

    let target = route.query.target
    target ? router.replace(target) : router.push('/')
  } catch (_) {}
}
</script>
<script setup>
import useBaseStore from '@/stores/base'
import useAutoImport from '@/useAutoImport'
const { reactive, ref, onUnmounted, API, router, route, showSuccessToast, showFailToast, utils } =
  useAutoImport()

const baseStore = useBaseStore()
console.log(`baseStore-->`, baseStore)

/* 定义状态 */
const formIns = ref(null)
const state = reactive({
  phone: '',
  code: '',
  btn: {
    disabled: false,
    text: '发送验证码'
  }
})
/* 登录提交 */
const submit = async () => {
  try {
    await formIns.value.validate()
    let { code, token } = await API.userLogin(state.phone, state.code)
    if (+code !== 0) {
      showFailToast('登录失败,请稍后再试')
      return
    }
    // 登录成功:存储Token、获取登录者信息、提示、跳转
    utils.storage.set('TK', token)
    await baseStore.queryProfile()
    showSuccessToast('登录成功')

    let target = route.query.target
    target ? router.replace(target) : router.push('/')
  } catch (_) {}
}
</script>

<template>
    <div style="margin: 20px 40px">
      <ButtonAgain round block type="primary" loading-text="正在处理中..." @click="submit">
        立即登录/注册
      </ButtonAgain>
    </div>
  </van-form>
</template>
返回上一页功能
  1. 单独做一个组件,专门来处理返回逻辑。
  • src/components/NavBack.vue
<script setup>
import useAutoImport from '@/useAutoImport'

const { router, route } = useAutoImport()

const back = () => {
  router.go(-1)
}
</script>

<template>
  <van-nav-bar title="个人中心" left-text="返回" left-arrow @click-left="back" />
</template>

<style lang="less" scoped>
:deep(.van-icon),
:deep(.van-nav-bar__text) {
  color: #000;
}
</style>
函数式调用组件的封装
  1. 先写一个主组件。主组件可以用模板组件,也可以用jsx组件

    • src/components/overlay/Index.vue 模板组件

      <script setup></script>
      <template>
        <van-overlay show>
          <van-loading color="#0094ff" vertical> 努力加载中,请稍后 </van-loading>
        </van-overlay>
      </template>
      <style lang="less" scoped>
      .van-overlay {
        display: flex;
        align-items: center;
        justify-content: center;
      }
      </style>
      
    • src/App.vue 在根视图中先看模板组件效果

      <script setup>
      import OverlayVue from '@/components/overlay/Index.vue'
      </script>
      
      <template>
        <OverlayVue></OverlayVue>
        <router-view v-slot="{ Component }">
          <keep-alive include="Home">
            <component :is="Component" />
          </keep-alive>
        </router-view>
      </template>
      
  2. 写一个js函数,用于在全局中渲染组件和移除主组件。

    • src/components/overlay/Index.vue 主组件

      <script setup></script>
      <template>
        <van-overlay show>
          <van-loading color="#0094ff" vertical> 努力加载中,请稍后 </van-loading>
        </van-overlay>
      </template>
      <style lang="less" scoped>
      .van-overlay {
        display: flex;
        align-items: center;
        justify-content: center;
      }
      </style>
      
    • src/components/overlay/index.js 在js文件中用js方式来在全局中插件入调用。

      import { createVNode, render } from 'vue'
      import Index from './Index.vue'
      
      export default function showOverlayLoading() {
        // 创建虚拟DOM。
        const vnode = createVNode(Index)
        // 渲染虚拟DOM
        console.log(`vnode-->`, vnode);
      
        const frag = document.createDocumentFragment()
        render(vnode, frag)
        document.body.appendChild(vnode.el, frag)
      
        return function hiddenOverlayLoading() {
          render(null, frag)
        }
      }
      
    • src/App.vue 根组件中尝试调用

      <script setup>
      import showOverlayLoading from '@/components/overlay'
      let hiddenOverlayLoading = showOverlayLoading()
      setTimeout(() => {
        console.log(`根视图组件移除`)
      
        hiddenOverlayLoading?.()
      }, 3000)
      </script>
      
      <template>
        <router-view v-slot="{ Component }">
          <keep-alive include="Home">
            <component :is="Component" />
          </keep-alive>
        </router-view>
      </template>
      
      <style lang="less">
      @import './assets/reset.min.css';
      
      .van-button {
        border-radius: 0 !important;
      }
      
      html,
      body,
      #app {
        min-height: 100vh;
        overflow-x: hidden;
        background: #f4f4f4;
      }
      
      #app {
        margin: 0 auto;
        background: @CR_W;
      }
      
      .van-skeleton {
        padding: 30px 15px;
      }
      </style>
      
路由中进行loading
  • src/components/overlay/Index.vue 全局loading模板组件
<script setup></script>
<template>
  <van-overlay show>
    <van-loading color="#0094ff" vertical> 努力加载中,请稍后 </van-loading>
  </van-overlay>
</template>
<style lang="less" scoped>
.van-overlay {
  display: flex;
  align-items: center;
  justify-content: center;
}
</style>
  • src/components/overlay/index.js 函数式调用全局loading模板组件的方法
import { createVNode, render } from 'vue'
import Index from './Index.vue'

export default function showOverlayLoading() {
  // 创建虚拟DOM。
  const vnode = createVNode(Index)
  // 渲染虚拟DOM
  console.log(`vnode-->`, vnode);

  const frag = document.createDocumentFragment()
  render(vnode, frag)
  document.body.appendChild(vnode.el, frag)

  return function hiddenOverlayLoading() {
    render(null, frag)
  }
}
  • src/router/index.js
import showOverlayLoading from '@/components/overlay'


// 全局前置守卫:登录态校验
const checkList = ['/person', '/store', '/update']//用于判断那些页面需要登录态校验。
let hiddenOverlayLoading = null//用于遮罩层
router.beforeEach(async (to, from, next) => {
  if (需要进行登录但没个人信息时) {
    hiddenOverlayLoading = showOverlayLoading()//开启遮罩层
    let info = await base.queryProfile()//异步用token拿到个人信息。
    if (!info) {
      // 真的没登录过。
      showFailToast('您还未登录,请先登录')
      next({
        path: '/login',
        query: {
          target: to.fullPath
        }
      })
      hiddenOverlayLoading?.()//移除遮罩层-用户真的没登录时。
      return
    }
  }

  next()
})
// 全局后置守卫
router.beforeEach((to, from) => {
  hiddenOverlayLoading?.()//移除遮罩层-其它情况,如用户已登录或者是无需个人信息页的情况。
})
export default router
import { createRouter, createWebHashHistory } from 'vue-router'
import routes from './routes'
import useBaseStore from '@/stores/base'
import { showFailToast } from 'vant'
import showOverlayLoading from '@/components/overlay'


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

// 全局前置守卫:登录态校验
const checkList = ['/person', '/store', '/update']//用于判断那些页面需要登录态校验。
let hiddenOverlayLoading = null//用于遮罩层
router.beforeEach(async (to, from, next) => {
  const base = useBaseStore()//用于拿到个人信息。
  let profile = base.profile
  if (checkList.includes(to.path) && !profile) {
    hiddenOverlayLoading = showOverlayLoading()//开启遮罩层
    let info = await base.queryProfile()
    if (!info) {
      // 真的没登录过。
      showFailToast('您还未登录,请先登录')
      next({
        path: '/login',
        query: {
          target: to.fullPath
        }
      })
      hiddenOverlayLoading?.()//移除遮罩层
      return
    }
  }

  next()
})
// 全局后置守卫
router.beforeEach((to, from) => {
  hiddenOverlayLoading?.()//移除遮罩层

  let title = to.meta?.title
  document.title = !title ? '知乎日报' : `${title} - 知乎日报`
})
export default router
路由跳转时修改标签页标题
  • src/router/index.js

    import { createRouter, createWebHashHistory } from 'vue-router'
    import routes from './routes'
    
    
    const router = createRouter({
      history: createWebHashHistory(),
      routes
    })
    // 全局后置守卫
    router.beforeEach((to, from) => {
    
      let title = to.meta?.title
      document.title = !title ? '知乎日报' : `${title} - 知乎日报`
    })
    export default router
    
    import { createRouter, createWebHashHistory } from 'vue-router'
    import routes from './routes'
    import useBaseStore from '@/stores/base'
    import { showFailToast } from 'vant'
    import showOverlayLoading from '@/components/overlay'
    
    
    const router = createRouter({
      history: createWebHashHistory(),
      routes
    })
    
    // 全局前置守卫:登录态校验
    const checkList = ['/person', '/store', '/update']//用于判断那些页面需要登录态校验。
    let hiddenOverlayLoading = null//用于遮罩层
    router.beforeEach(async (to, from, next) => {
      const base = useBaseStore()//用于拿到个人信息。
      let profile = base.profile
      if (checkList.includes(to.path) && !profile?.value) {
        hiddenOverlayLoading = showOverlayLoading()//开启遮罩层
        let info = await base.queryProfile()
        if (!info) {
          // 真的没登录过。
          showFailToast('您还未登录,请先登录')
          next({
            path: '/login',
            query: {
              target: to.fullPath
            }
          })
          hiddenOverlayLoading?.()//移除遮罩层
          return
        }
      }
    
      next()
    })
    // 全局后置守卫
    router.beforeEach((to, from) => {
      hiddenOverlayLoading?.()//移除遮罩层
    
      let title = to.meta?.title
      document.title = !title ? '知乎日报' : `${title} - 知乎日报`
    })
    export default router
    
  • src/router/routes.js

    import Home from '@/views/Home.vue'
    const routes = [{
        path: '/',
        name: 'home',
        meta: { title: '首页' },
        component: Home
    }, {
        path: '/detail/:id',
        name: 'detail',
        meta: { title: '详情页' },
        component: () => import('@/views/Detail.vue')
    }, {
        path: '/login',
        name: 'login',
        meta: { title: '登录/注册页' },
        component: () => import('@/views/Login.vue')
    }, {
        path: '/person',
        name: 'person',
        meta: { title: '个人中心' },
        component: () => import('@/views/Person.vue')
    }, {
        path: '/store',
        name: 'store',
        meta: { title: '我的收藏' },
        component: () => import('@/views/Store.vue')
    }, {
        path: '/update',
        name: 'update',
        meta: { title: '更改信息' },
        component: () => import('@/views/Update.vue')
    }, {
        path: '/:pathMatch(.*)*',
        redirect: '/'
    }]
    export default routes
    
详情页收藏按钮
  1. 不用传统的登录态校验,但一些区域或功能需要用到个人信息。
  2. 所以需要优化个人信息的处理。
  3. 所有的涉及收藏的状态及操作和前后端数据交互,都放在全局公共状态里。
  4. 在需要用到收藏相关的状态及操作,都要调用全局公共状态方法。
优化个人信息的处理
  • src/router/index.js
import { createRouter, createWebHashHistory } from 'vue-router'
import routes from './routes'
import useBaseStore from '@/stores/base'
import { showFailToast } from 'vant'
import showOverlayLoading from '@/components/overlay'


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

// 全局前置守卫:登录态校验
const checkList = ['/person', '/store', '/update']//用于判断那些页面需要登录态校验。
let hiddenOverlayLoading = null//用于遮罩层
router.beforeEach(async (to, from, next) => {
  const base = useBaseStore()//用于拿到个人信息。
  let profile = base.profile//个人信息。
  // 除登录页之外,其余所有页面在没有存储登录者信息的情况下,都需要从服务器获取登录者信息进行存储。
  if (!profile && to.path !== '/login') {
    hiddenOverlayLoading = showOverlayLoading()//开启遮罩层
    let info = await base.queryProfile()
    // 如果是需要登录态校验的三个页面,再进行登录校验和跳转。
    if (checkList.includes(to.path) && !info) {
      // 真的没登录过。
      showFailToast('您还未登录,请先登录')
      next({
        path: '/login',
        query: {
          target: to.fullPath
        }
      })
      hiddenOverlayLoading?.()//移除遮罩层
      return
    }
  }
  next()
})
// 全局后置守卫
router.beforeEach((to, from) => {
  hiddenOverlayLoading?.()//移除遮罩层

  let title = to.meta?.title
  document.title = !title ? '知乎日报' : `${title} - 知乎日报`
})
export default router

收藏功能

基础pinia模板
  • src/stores/collect.js
import { defineStore } from 'pinia'
import { ref } from 'vue'
import API from '@/api'

const useCollectStore = defineStore('collect', () => {
  // 定义公共状态。

  // 派发的方法。

  // 暴露给外面用。
  return {

  }
})
export default useCollectStore
import { defineStore } from 'pinia'
import { ref } from 'vue'
import API from '@/api'

const useCollectStore = defineStore('collect', () => {
  // 定义公共状态。
  const collectList = ref(null)

  // 派发的方法。
  const queryCollectList = async () => {

  }
  const removeCollectList = async () => {

  }

  // 暴露给外面用。
  return {
    collectList,
    queryCollectList,
    removeCollectList,
  }
})
export default useCollectStore
收藏模块全局状态
  • 示例代码:

    • src/stores/collect.js 收藏相关的接口都用来源于这里的文件。

      import { defineStore } from 'pinia'
      import { ref } from 'vue'
      import API from '@/api'
      import { showFailToast, showSuccessToast } from 'vant'
      
      const useCollectStore = defineStore('collect', () => {
        // 定义公共状态。
        const collectList = ref(null)//用于保存收藏列表。
      
        // 派发的方法。
        // 查询收藏列表。
        const queryCollectList = async () => {
          let list = null
          try {
            let { code, data } = await API.storeList()
            if (+code === 0) {
              list = data
              collectList.value = list
            }
          } catch (error) {
            console.log(`error:-->`, error)
          }
          return list
      
        }
        // 删除收藏。
        // id为收藏id。
        const removeCollectList = async (id) => {
          if (!collectList?.value) {
            return
          }
          try {
            let { code } = await API.storeRemove(id)
            if (+code !== 0) {
              showFailToast('移除收藏失败')
              return
            }
            showSuccessToast(`移除收藏成功`)
            collectList.value = collectList.value.filter(item => {
              return +item.id !== +id
            })
          } catch (error) {
            console.log(`error:-->`, error)
          }
        }
        // 新增收藏。
        const insertCollectList = async (newsId) => {
          try {
            let { code } = await API.storeAdd(newsId)
            if (+code !== 0) {
              showFailToast('收藏失败')
              return
            }
            await queryCollectList()
            showSuccessToast(`收藏成功`)
          } catch (error) {
            console.log(`error:-->`, error)
          }
        }
      
        // 暴露给外面用。
        return {
          collectList,
          queryCollectList,
          removeCollectList,
          insertCollectList,
        }
      })
      export default useCollectStore
      
    • src/views/Detail.vue 详情页

      1. 由于没有登录而进入到登录页,不能直接用push(),因为会添加一条记录,导致登录成功后重新跳转回详情页之后,会新增一条详情记录。此时登录成功后点击返回,依旧是在详情页。所以这里只能使用replace()进登录页,用target字段标识,则在登录成功后,退回到详情页。
        • 这个在登录页中做特殊处理,如果有target字段标识,则在登录成功后,跳转到target字段对应的路径中。
      2. 而用replace(),也会丢失历史记录。在登录页中点击我们写的后退组件,不是返回详情页,而是回到详情页的上一条历史记录。即在详情页用replace()进登录页之后,从登录页点击后退,会跳转回首页
        • 这个需要在我们写的后退组件中做特殊处理。
      <script setup>
      import useCollectStore from '@/stores/collect'
      import useBaseStore from '@/stores/base'
      import useAutoImport from '@/useAutoImport'
      const { reactive, onBeforeMount, onUnmounted, nextTick, router, route, API } = useAutoImport()
      const { computed, showFailToast } = useAutoImport()
      
      const base = useBaseStore()
      const collect = useCollectStore()
      
      /* 定义状态 */
      const newsId = route.params.id
      const state = reactive({
        info: null,
        extra: null
      })
      
      /* 第一次渲染之前:从服务器获取新闻详情和额外的信息 */
      let link = null
      const handleInfoStyle = () => {
        let css = state.info?.css?.[0]
        if (!css) return
        link = document.createElement('link')
        link.rel = 'stylesheet'
        link.href = css
        document.head.appendChild(link)
      }
      const handleHeaderImage = () => {
        const holderBox = document.querySelector('.img-place-holder')
        if (!holderBox) return
        const imgTemp = new Image()
        imgTemp.src = state.info.image
        imgTemp.onload = () => holderBox.appendChild(imgTemp)
        imgTemp.onerror = () => {
          const p = holderBox.parentNode
          p.parentNode.removeChild(p)
        }
      }
      onBeforeMount(async () => {
        try {
          let data = await API.queryNewsInfo(newsId)
          state.info = Object.freeze(data)
          // 处理样式:无需等待视图更新完毕
          handleInfoStyle()
          // 处理头图:需要等待组件更新完毕
          nextTick(handleHeaderImage)
        } catch (_) {}
      })
      onBeforeMount(async () => {
        try {
          let data = await API.queryStoryExtra(newsId)
          state.extra = Object.freeze(data)
        } catch (_) {}
      })
      
      /* 组件销毁后:把创建的样式移除掉 */
      onUnmounted(() => {
        if (link) document.head.removeChild(link)
      })
      
      // ----------------------------------
      // 第一次渲染页面之前:如果用户登录了,且没有收藏记录,则需要获取。
      onBeforeMount(() => {
        if (base.profile && !collect.collectList) {
          collect.queryCollectList()
        }
      })
      // 根据收藏记录,来计算此文章用户是否收藏过。
      const collectItem = computed(() => {
        let collectList = collect.collectList || []
        return collectList.find((item) => {
          return String(item.news.id) === String(newsId)
        })
      })
      // 收藏的相关操作
      const handleCollect = () => {
        if (!base.profile) {
          showFailToast(`请你先登录`)
          router.replace({
            path: '/login',
            query: {
              target: route.fullPath
            }
          })
          return
        }
        if (collectItem.value) {
          // 当前是已收藏,则移除收藏
          collect.removeCollectList(collectItem.value.id)
          return
        }
        // 当前是未收藏:则进行收藏。
        collect.insertCollectList(newsId)
      }
      </script>
      
      <template>
        <van-skeleton title :row="5" v-if="!state.info" />
        <div class="contentMy" v-else v-html="state.info.body"></div>
      
        <div class="nav-box">
          <van-icon name="arrow-left" @click="router.go(-1)"></van-icon>
          <template v-if="state.extra">
            <van-icon name="comment-o" :badge="state.extra.comments"></van-icon>
            <van-icon name="good-job-o" :badge="state.extra.popularity"></van-icon>
            <van-icon
              name="star-o"
              :color="collectItem ? `#1989fa` : ``"
              @click="handleCollect"
            ></van-icon>
            <van-icon name="share-o" color="#ccc"></van-icon>
          </template>
        </div>
      </template>
      
      <style lang="less" scoped>
      .contentMy {
        background: @CR_W;
        padding-bottom: 50px;
        margin: 0;
      
        :deep(.img-place-holder) {
          height: 375px;
          overflow: hidden;
      
          img {
            display: block;
            margin: 0;
            width: 100%;
            min-height: 100%;
          }
        }
      }
      
      .van-skeleton {
        padding: 30px 15px;
      }
      
      .nav-box {
        position: fixed;
        bottom: 0;
        left: 0;
        display: flex;
        justify-content: space-between;
        align-items: center;
        box-sizing: border-box;
        padding: 0 15px;
        width: 100%;
        height: 50px;
        background: #f4f4f4;
        font-size: 22px;
      
        .van-icon:nth-child(1) {
          position: relative;
      
          &::after {
            position: absolute;
            top: -10%;
            right: -15px;
            content: '';
            width: 1px;
            height: 120%;
            background: #d5d5d5;
          }
        }
      
        :deep(.van-badge) {
          background-color: transparent;
          border: none;
          color: #000;
          right: -5px;
        }
      }
      </style>
      
    • src/components/NavBack.vue

      <script setup>
      import useAutoImport from '@/useAutoImport'
      
      const { router, route } = useAutoImport()
      
      const back = () => {
        // 特殊情况:当前是登录页,而且来源是详情页,需要基于replace的方式,回到详情页。
        if (route.path === '/login') {
          let target = route.query.target || ''
          if (/^\/detail\//.test(target)) {
            router.replace(target)
            return
          }
        }
        router.go(-1)
      }
      </script>
      
      <template>
        <van-nav-bar title="个人中心" left-text="返回" left-arrow @click-left="back" />
      </template>
      
      <style lang="less" scoped>
      :deep(.van-icon),
      :deep(.van-nav-bar__text) {
        color: #000;
      }
      </style>
      
    • src/views/Login.vue

      <script setup>
      import useBaseStore from '@/stores/base'
      import useAutoImport from '@/useAutoImport'
      const { reactive, ref, onUnmounted, API, router, route, showSuccessToast, showFailToast, utils } =
        useAutoImport()
      
      const baseStore = useBaseStore()
      console.log(`baseStore-->`, baseStore)
      
      /* 定义状态 */
      const formIns = ref(null)
      const state = reactive({
        phone: '',
        code: '',
        btn: {
          disabled: false,
          text: '发送验证码'
        }
      })
      
      /* 发送验证码 */
      let timer = null,
        count = 30
      const handleSendCode = async () => {
        try {
          // 先对手机号进行校验
          await formIns.value.validate('phone')
          // 向服务器发送请求
          let { code } = await API.userSendCode(state.phone)
          if (+code === 0) {
            // 开启倒计时
            state.btn.disabled = true
            state.btn.text = `30s后重发`
            timer = setInterval(() => {
              if (count === 1) {
                clearInterval(timer)
                count = 30
                state.btn.disabled = false
                state.btn.text = `发送验证码`
                return
              }
              count--
              state.btn.text = `${count}s后重发`
            }, 1000)
            return
          }
          showFailToast('发送失败,稍后再试')
        } catch (_) {}
      }
      onUnmounted(() => clearInterval(timer))
      
      /* 登录提交 */
      const submit = async () => {
        try {
          await formIns.value.validate()
          let { code, token } = await API.userLogin(state.phone, state.code)
          if (+code !== 0) {
            showFailToast('登录失败,请稍后再试')
            return
          }
          // 登录成功:存储Token、获取登录者信息、提示、跳转
          utils.storage.set('TK', token)
          await baseStore.queryProfile()
          showSuccessToast('登录成功')
      
          let target = route.query.target
          target ? router.replace(target) : router.push('/')
        } catch (_) {}
      }
      </script>
      
      <template>
        <nav-back title="登录/注册" />
        <van-form ref="formIns" validate-first>
          <van-cell-group inset>
            <van-field
              center
              label="手机号"
              label-width="50px"
              name="phone"
              v-model.trim="state.phone"
              :rules="[
                { required: true, message: '手机号是必填项' },
                { pattern: /^(?:(?:\+|00)86)?1\d{10}$/, message: '手机号格式不正确' }
              ]"
            >
              <template #button>
                <button-again
                  class="form-btn"
                  size="small"
                  type="primary"
                  loading-text="处理中"
                  :disabled="state.btn.disabled"
                  @click="handleSendCode"
                >
                  {{ state.btn.text }}
                </button-again>
              </template>
            </van-field>
      
            <van-field
              label="验证码"
              label-width="50px"
              name="code"
              v-model.trim="state.code"
              :rules="[
                { required: true, message: '验证码是必填项' },
                { pattern: /^\d{6}$/, message: '验证码格式不正确' }
              ]"
            />
          </van-cell-group>
      
          <div style="margin: 20px 40px">
            <ButtonAgain round block type="primary" loading-text="正在处理中..." @click="submit">
              立即登录/注册
            </ButtonAgain>
          </div>
        </van-form>
      </template>
      
      <style lang="less" scoped>
      .van-form {
        margin-top: 30px;
      
        .form-btn {
          width: 78px;
        }
      }
      </style>
      
  • 关于没登录跳转到登录页的核心处理代码:

    • src/views/Detail.vue 详情页

      1. 由于没有登录而进入到登录页,不能直接用push(),因为会添加一条记录,导致登录成功后重新跳转回详情页之后,会新增一条详情记录。此时登录成功后点击返回,依旧是在详情页。所以这里只能使用replace()进登录页,用target字段标识,则在登录成功后,退回到详情页。
        • 这个在登录页中做特殊处理,如果有target字段标识,则在登录成功后,跳转到target字段对应的路径中。
      2. 而用replace(),也会丢失历史记录。在登录页中点击我们写的后退组件,不是返回详情页,而是回到详情页的上一条历史记录。即在详情页用replace()进登录页之后,从登录页点击后退,会跳转回首页
        • 这个需要在我们写的后退组件中做特殊处理。
      <script setup>
      import useCollectStore from '@/stores/collect'
      import useBaseStore from '@/stores/base'
      import useAutoImport from '@/useAutoImport'
      const { reactive, onBeforeMount, onUnmounted, nextTick, router, route, API } = useAutoImport()
      const { computed, showFailToast } = useAutoImport()
      
      const base = useBaseStore()
      const collect = useCollectStore()
      // 收藏的相关操作
      const handleCollect = () => {
        if (!base.profile) {
          showFailToast(`请你先登录`)
          router.replace({
            path: '/login',
            query: {
              target: route.fullPath
            }
          })
          return
        }
        if (collectItem.value) {
          // 当前是已收藏,则移除收藏
          collect.removeCollectList(collectItem.value.id)
          return
        }
        // 当前是未收藏:则进行收藏。
        collect.insertCollectList(newsId)
      }
      </script>
      
      <template>
        <div class="nav-box">
          <template v-if="state.extra">
            <van-icon
              name="star-o"
              :color="collectItem ? `#1989fa` : ``"
              @click="handleCollect"
            ></van-icon>
          </template>
        </div>
      </template>
      
    • src/components/NavBack.vue

      <script setup>
      import useAutoImport from '@/useAutoImport'
      
      const { router, route } = useAutoImport()
      
      const back = () => {
        // 特殊情况:当前是登录页,而且来源是详情页,需要基于replace的方式,回到详情页。
        if (route.path === '/login') {
          let target = route.query.target || ''
          if (/^\/detail\//.test(target)) {
            router.replace(target)
            return
          }
        }
        router.go(-1)
      }
      </script>
      
      <template>
        <van-nav-bar title="个人中心" left-text="返回" left-arrow @click-left="back" />
      </template>
      
    • src/views/Login.vue

      <script setup>
      
      /* 登录提交 */
      const submit = async () => {
        try {
          //...
          showSuccessToast('登录成功')
      
          let target = route.query.target
          target ? router.replace(target) : router.push('/')
        } catch (_) {}
      }
      </script>
      
      <template>
      
          <div style="margin: 20px 40px">
            <ButtonAgain round block type="primary" loading-text="正在处理中..." @click="submit">
              立即登录/注册
            </ButtonAgain>
          </div>
        </van-form>
      </template>
      

打包

  1. vite按需导入插件vite-plugin-impvant@4的按需导入插件有冲突,会导致vant4中的函数调用式组件会导入与实际vant组件用到的样式文件地址不同的路径。
  • 示例

    • vite.config.js

      import { fileURLToPath, URL } from 'node:url'
      import { defineConfig } from 'vite'
      import vue from '@vitejs/plugin-vue'
      import vueJsx from '@vitejs/plugin-vue-jsx'
      import viteImp from 'vite-plugin-imp'
      import Components from 'unplugin-vue-components/vite'
      import { VantResolver } from 'unplugin-vue-components/resolvers'
      import pxtorem from 'postcss-pxtorem'
      
      /* https://vitejs.dev/config/ */
      export default defineConfig({
        base: './',
        plugins: [
          vue(),
          vueJsx(),
          /* // 按需导入插件 https://github.com/onebay/vite-plugin-imp
          // 与vant4的按需导入有冲突。
          viteImp({
            libList: [
              {
                libName: 'lodash',
                libDirectory: '',
                camel2DashComponentName: false
              }
            ]
          }), */
          // vant@4的按需导入
          Components({
            resolvers: [
              VantResolver()
            ]
          })
        ],
        resolve: {
          alias: {
            '@': fileURLToPath(new URL('./src', import.meta.url))
          }
        },
        /* 服务配置 */
        server: {
          host: '127.0.0.1',
          proxy: {
            '/api': {
              target: 'http://127.0.0.1:7100',
              changeOrigin: true,
              rewrite: path => path.replace(/^\/api/, '')
            }
          }
        },
        /* 生产环境 */
        build: {
          assetsInlineLimit: 1024 * 10,
          minify: 'terser',
          terserOptions: {
            compress: {
              drop_console: true,
              drop_debugger: true
            }
          },
          rollupOptions: {
            external: ['']
          }
        },
        /* CSS样式 */
        css: {
          postcss: {
            plugins: [
              pxtorem({
                rootValue: 37.5,
                propList: ['*']
              })
            ]
          },
          preprocessorOptions: {
            less: {
              additionalData: `@import "@/assets/var.less";`
            }
          }
        }
      })
      
  • 核心:

    • vite.config.js

      import viteImp from 'vite-plugin-imp'
      export default defineConfig({
        plugins: [
          // 按需导入插件 https://github.com/onebay/vite-plugin-imp
          // 与vant4的按需导入有冲突。
          viteImp({
            libList: [
              {
                libName: 'lodash',
                libDirectory: '',
                camel2DashComponentName: false
              }
            ]
          }),
        ],
      })
      

      不兼容

      import Components from 'unplugin-vue-components/vite'
      import { VantResolver } from 'unplugin-vue-components/resolvers'
      export default defineConfig({
        plugins: [
          Components({
            resolvers: [
              VantResolver()
            ]
          })
        ],
      })
      

TS

  • 主要就是为了开发时限定类型,让代码更严谨。
    1. 开发时用ts代替js,用tsx代替jsx
  1. 类型 对各种变量/值,进行类型限制
  2. 类型断言
  3. 在函数中使用各种声明和限制
  4. 在类中的处理 public/private/protected

与es5及es6的关系

类型的限定

  1. 对各种变量/值,进行类型限制

常见类型

/*
 let/const 变量:类型限定 = 值
   + 变量不能是已经被 lib.dom.d.ts 声明的,例如:name
     但可以把当前文件变为一个模块 “ 加入 export 导出 ”,这样在这里声明的变量都是私有的了
   + 类型限定可以是小写和大写
     + 一般用小写
     + 大写类型可以描述实例
     + 大写的 Object 不用,因为所有值都是其实例;想要笼统表示对象类型,需要用 object !
   + 数组的限定
     let arr:number/string[]
     let arr:(number|string)[]
     let arr:Array<string> 泛型
     ...
   + TS中的元祖:类型和长度固定
     let tuple:[string, number] = ['abc', 10]
     可基于数组的方法操作元祖
   + TS中的枚举
     enum USER_ROLE {
        ADMIN,
        USER
     }
   + null 和 undefined 只能赋予本身的值
   + void 用于函数的返回
     function fn():void{ ... }
     function fn():void | null{ ... }
   + never 不可能出现的值「任何类型的子类型」
     function fn():never{ 
        // 报错 OR 死循环 等
     }
   + any 任意类型
 */

类型断言

  1. 一定小心使用,相关于程序员用人格保证了,就是不是,ts编译器也会把该值当成是断言的类型。
/*
 类型断言:
 @1 声明变量,没有给类型限定,没有赋值的时候,默认类型是any
 @2 如果最开始声明的时候赋值了,则会按照此时值的类型自动推导
 @3 联合类型
   let name:string | number
   + 在没有赋值之前,只能使用联合类型规定的类型,进行相关的操作
   + 不能在变量赋值之前调用其方法
   + !. 断言变量一定有值
   + as 认定是啥类型的值
   (name! as number).toFixed()
 @4 字面量类型
   let direction:'top'|'right'|'down'|'left' 赋的值只能是这四个中的一个{限定了值}
   可以基于 type (类型别名)优化
     let Direction = 'top'|'right'|'down'|'left'
     let direction:Direction = ...
 */

函数类型

  1. 在函数中使用各种声明和限制。
/*
 函数的玩法
   普通函数:声明参数和返回值类型
     function fn(x:number,y:number):number{...}
   函数表达式:在普通函数基础上,对赋值的函数做类型校验 
     type Fn = (x:number,y?:number) => number
     let fn:Fn = function(x,y){...}
 */

类的类型

高级类型与联合类型

接口

接口与type

泛型

ts的应用

  1. @vue/cli中使用
  2. vite中使用

在项目根目录中配置

  • Vue3进阶/knowledge/env.d.ts 这个很重要,要不在.vue后缀类型文件中会有报错。

    /// <reference types="vite/client" />
    
    // 声明导入 .vue 文件的类型「防止波浪线报错」
    declare module '*.vue' {
      import type { DefineComponent } from 'vue'
      const component: DefineComponent<{}, {}, any>
      export default component
    }
    
  • Vue3进阶/knowledge/tsconfig.app.json

    {
      "extends": "@vue/tsconfig/tsconfig.dom.json",
      "include": [
        "env.d.ts",
        "src/**/*",
        "src/**/*.vue"
      ],
      "exclude": [
        "src/**/__tests__/*"
      ],
      "compilerOptions": {
        "composite": true,
        "baseUrl": ".",
        "paths": {
          "@/*": [
            "./src/*"
          ]
        }
      }
    }
    
  • Vue3进阶/knowledge/tsconfig.json 看对应的pdf文档

    {
      "files": [],
      "references": [
        {
          "path": "./tsconfig.node.json"
        },
        {
          "path": "./tsconfig.app.json"
        }
      ]
    }
    
  • Vue3进阶/knowledge/tsconfig.node.json

    {
      "extends": "@tsconfig/node18/tsconfig.json",
      "include": [
        "vite.config.*",
        "vitest.config.*",
        "cypress.config.*",
        "nightwatch.conf.*",
        "playwright.config.*"
      ],
      "compilerOptions": {
        "composite": true,
        "module": "ESNext",
        "types": [
          "node"
        ]
      }
    }
    

简历

注意细节

  1. 先有面试再说后面的事。
  2. 先有word版写好,后面再复制到网站的模板上。
  3. 先全员海投,有面试机会再看具体信息。(无脑投)
  4. BOSS直聘一天100个左右,其它投到上限。(用一个小时左右投)
  5. 先看到有的,后面去试,可以准备给朋友。(可以记录下要面试的题)。
  6. 面试时,一般就说在我之前的项目中…而不要八股文。(个人真实就好了)
  7. 带上笔和本-面试遇到不会的问题,是面试的开始,而不是结束。
    • 当上不会的题或东西,当着面试官的面来记,再说后面再查,晚上回去再查。这个也要真查,因为提到的可能就是新的主流东西。
      • 再问对方写代码了多少年,夸奖面试官。多少年之后,比自己少的,夸对方厉害。比自己多,不愧是xx年工作经验的。

招聘平台

  • 招聘平台
    • BOSS直聘(主要)
      • 基于聊天去投递简历,要准备好简历和聊天用语。
    • 51job(前程无忧)
    • 拉钩
    • 猎聘网

投递时间

  • 投的时间:周一到周六,每天9:30开始、下午14:00开始(不要睡懒觉)。

    • 剩下的时间要复习。
    • 整理好css、js,之后是vue和react,并写页面。
      • 进阶学一些算法。
    • 面试之后要录音,电脑面试也要录音,而现场面试时进公司就录音。
    • 面试之后要再整理面试题,如果记不清,则要听录音。同时再总结出最佳的面试题回答。
    • 早睡早起:早上不要晚于8:30、晚上不要晚于12:00、在此期间不要玩游戏。
  • 老家或北京之类的都投。

个人预期

  • 学习完ts和uniapp。

职业规划和离职原因

  • 职业规划

    • 随意一些,按个人真实的来。
    • 走技术,学习全栈。
      • 学会后端知识点如:node。
      • 学习uniapp。
      • 学习taro。
      • 看vue3源码和react源码和UI框架源码,如:
        • element-ui源码。
        • antd源码。
        • vant源码。
      • 学习前端算法。
    • 走管理,熟悉公司的业务,会培训带领新人,写文档。会和后端进行交互,
  • 离职原因

    • 不要说上家公司坏话,如技术栈不新、公司抠门、领导差之类的。
    • 尽量多写客观原因:
      • 可以说公司业绩不太好-公司暗示要解散项目组。
        • 公司倒闭了,但压了自己的工资,老大那边压力也大,后面帮做最后一个项目里,结束最后一个业务后,就结束了。
      • 要结婚之类的。

进阶参考

  1. ts中文网
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值