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') returnprops[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 = truetry {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') returnprops[key] = attrs[key]})return props})// 自己控制loading效果const loading = ref(false)const handle = async (ev) => {loading.value = truetry {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 = truestate.btn.text = `30s后重发`timer = setInterval(() => {if (count === 1) {clearInterval(timer)count = 30state.btn.disabled = falsestate.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-fieldcenterlabel="手机号"label-width="50px"name="phone"v-model.trim="state.phone":rules="[{ required: true, message: '手机号是必填项' },{ pattern: /^(?:(?:\+|00)86)?1\d{10}$/, message: '手机号格式不正确' }]"><template #button><button-againclass="form-btn"size="small"type="primary"loading-text="处理中":disabled="state.btn.disabled"@click="handleSendCode">{{ state.btn.text }}</button-again></template></van-field><van-fieldlabel="验证码"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 = nulltry {let { code, data } = await API.userInfo()if (code === 0) {info = dataprofile.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.profileif (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.targettarget ? 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.targettarget ? router.replace(target) : router.push('/')} catch (_) {}
    }
    </script>
  • 会有一个问题-路由错乱的问题。

登录页的跳转

  1. 让登录页中可以直接跳转回来源页面。
  • src/views/Login.vue
<script setup>
/* 登录提交 */
const submit = async () => {try {showSuccessToast('登录成功')let target = route.query.targettarget ? 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.targettarget ? 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)// 渲染虚拟DOMconsole.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)// 渲染虚拟DOMconsole.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.profileif (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?.titledocument.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?.titledocument.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.profileif (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?.titledocument.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?.titledocument.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 = nulltry {let { code, data } = await API.storeList()if (+code === 0) {list = datacollectList.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) returnlink = document.createElement('link')link.rel = 'stylesheet'link.href = cssdocument.head.appendChild(link)
      }
      const handleHeaderImage = () => {const holderBox = document.querySelector('.img-place-holder')if (!holderBox) returnconst imgTemp = new Image()imgTemp.src = state.info.imageimgTemp.onload = () => holderBox.appendChild(imgTemp)imgTemp.onerror = () => {const p = holderBox.parentNodep.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-iconname="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 = truestate.btn.text = `30s后重发`timer = setInterval(() => {if (count === 1) {clearInterval(timer)count = 30state.btn.disabled = falsestate.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.targettarget ? router.replace(target) : router.push('/')} catch (_) {}
      }
      </script><template><nav-back title="登录/注册" /><van-form ref="formIns" validate-first><van-cell-group inset><van-fieldcenterlabel="手机号"label-width="50px"name="phone"v-model.trim="state.phone":rules="[{ required: true, message: '手机号是必填项' },{ pattern: /^(?:(?:\+|00)86)?1\d{10}$/, message: '手机号格式不正确' }]"><template #button><button-againclass="form-btn"size="small"type="primary"loading-text="处理中":disabled="state.btn.disabled"@click="handleSendCode">{{ state.btn.text }}</button-again></template></van-field><van-fieldlabel="验证码"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-iconname="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.targettarget ? 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) => numberlet 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中文网

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/15688.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

netty 错误

项目场景&#xff1a; 提示&#xff1a;这里简述项目相关背景&#xff1a; 问题描述 提示&#xff1a;这里描述项目中遇到的问题&#xff1a; 七月 28, 2023 11:12:49 上午 org.jboss.netty.channel.SimpleChannelHandler 警告: EXCEPTION, please implement cn.com.onlinet…

完美解决Qt error C2664: 无法将参数 1 从“const char []”转换为“char *

问题原因 C新标准不再允许将常量字符串&#xff0c;作为字符指针使用。对此&#xff0c;msvc编译环境&#xff0c;会报错。对此&#xff0c;可以在工程属性中配置“多字节字符集”来解决这个问题。 目前&#xff0c;使用QtCreator&#xff0c;同样也可以在Qt工程文件中去配置“…

Zookeeper学习笔记

0、ZooKeeper安装与集群安装 略。。。 1、Zookeeper介绍 Zookeeper是一个开源的分布式的&#xff0c;为分布式框架提供协调服务的Apache项目。 1.1、Zookeeper工作机制 Zookeeper从设计模式的角度来理解&#xff1a;是一个基于观察者模式设计的分布式服务管理框架&#xf…

从Arweave开始:4EVERLAND存储签入挑战开始

嗨&#xff0c;4evers&#xff0c; 今天&#xff0c;我们热烈欢迎您参加 Galxe 上的 4EVERLAND “Arweave 入门”活动。这是一项长期的重头活动&#xff0c;所有参与的用户都有机会获得相应的奖励。 Arweave 是一种革命性的去中心化存储协议&#xff0c;为寻求安全可靠的有价…

【Linux】进程轻松入门

目录 一&#xff0c; 冯* 诺依曼体系结构 1&#xff0c;存储结构 ​编辑 二&#xff0c; 操作系统 1&#xff0c;概念 2&#xff0c;设计OS的目的 3&#xff0c;定位 4&#xff0c;如何理解 "管理" 5&#xff0c; 总结 三&#xff0c;进程 1. 概念 那么…

26 用lsqnonlin求解最小二乘问题(matlab程序)

1.简述 函数语法 x lsqnonlin(fun,x0) 函数用于&#xff1a; 解决非线性最小二乘(非线性数据拟合)问题 解决非线性最小二乘曲线拟合问题的形式 变量x的约束上下限为ub和lb&#xff0c; x lsqnonlin(fun,x0)从x0点开始&#xff0c;找到fun中描述的函数的最小平方和。函数fu…

zore-shot,迁移学习和多模态学习

1.zero-shot 定义&#xff1a;在ZSL中&#xff0c;某一类别在训练样本中未出现&#xff0c;但是我们知道这个类别的特征&#xff0c;然后通过语料知识库&#xff0c;便可以将这个类别识别出来。概括来说&#xff0c;就是已知描述&#xff0c;对未知类别&#xff08;未在训练集中…

前端Vue入门-day05-自定义指令、插槽、路由入门

(创作不易&#xff0c;感谢有你&#xff0c;你的支持&#xff0c;就是我前行的最大动力&#xff0c;如果看完对你有帮助&#xff0c;请留下您的足迹&#xff09; 目录 自定义指令 基本语法 (全局&局部注册) 全局注册 局部注册 指令的值 v-loading 指令封装 插槽 …

OmegaConf

OmegaConf OmegaConf 是一个用于处理配置文件和命令行参数的Python库&#xff0c;它支持YAML和JSON格式的配置文件。OmegaConf 提供了一些高级功能&#xff0c;如配置合并、类型安全的配置访问、环境变量插值等。OmegaConf.load() 是这个库中的一个函数&#xff0c;用于加载和…

【Linux】TCP协议

​&#x1f320; 作者&#xff1a;阿亮joy. &#x1f386;专栏&#xff1a;《学会Linux》 &#x1f387; 座右铭&#xff1a;每个优秀的人都有一段沉默的时光&#xff0c;那段时光是付出了很多努力却得不到结果的日子&#xff0c;我们把它叫做扎根 目录 &#x1f449;TCP协议&…

第2章 JavaScript语法

准备工作 编写js需要准备一个编译器和游览器&#xff0c;js必须通过HTML/XHTML文档编写 js的编写位置 <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Docume…

【C++】类和对象-C++运算符重载

运算符重载 1.加号运算符重载 代码&#xff1a; #include <iostream> using namespace std; /******************************************/ //加号运算符重载class Person { public:int m_A;int m_B;//1、成员函数重载号(不能与下面方式2同时存在&#xff0c;否则代码报…

flag{网鼎杯之java代码审计入门} - file-in-java[ctf]

一、赛题截图 二、接口测试 我们先上传文件抓包&#xff0c;发送到repeter 响应如下 我们使用下载接口去下载一个不存在的文件&#xff0c;回显“资源被删除” - 说明系统可能去查找了这个文件&#xff0c;那我们能不能去下载/etc/passwd文件&#xff0c;但是还不知道相对…

【使用机器学习和深度学习对城市声音进行分类】基于两种技术(ML和DL)对音频数据(城市声音)进行分类(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

AttributeError: ‘DataFrame‘ object has no attribute ‘iteritems‘解决方案

大家好,我是爱编程的喵喵。双985硕士毕业,现担任全栈工程师一职,热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。现为CSDN博客专家、人工智能领域优质创作者。喜欢通过博客创作的方式对所学的…

程序设计 算法基础

✅作者简介&#xff1a;人工智能专业本科在读&#xff0c;喜欢计算机与编程&#xff0c;写博客记录自己的学习历程。 &#x1f34e;个人主页&#xff1a;小嗷犬的个人主页 &#x1f34a;个人网站&#xff1a;小嗷犬的技术小站 &#x1f96d;个人信条&#xff1a;为天地立心&…

纯JS+Vue实现一个仪表盘

在使用canvas的时候发现数值变化&#xff0c;每次都要重新渲染&#xff0c;值都从0开始&#xff0c;这和我的需求冲突。 1. 先绘制基本的圆环背景&#xff0c;利用border-color和border-radius将正方形变成基本的圆环。 <div class"circle"><div class&qu…

vue3如何封装框架

在Vue 3中&#xff0c;你可以通过创建一个基础的框架来封装一些常用的功能、组件和样式&#xff0c;以便在不同的项目中重复使用。下面是一个简单的步骤来封装一个Vue 3框架&#xff1a; 创建一个新的Vue项目&#xff1a;首先&#xff0c;使用Vue CLI创建一个新的Vue项目。 v…

Bash配置文件

当Bash以登录Shell启动的时候&#xff0c;会首先读取并执行文件“/etc/profile”中的命令。 接着&#xff0c;Bash会依次查找文件“~/.bash_profile”&#xff0c;“~/.bash_login”&#xff0c;“~/.profile”&#xff0c;读取并执行找到的第一个文件中的命令。也就是说&…

强烈推荐!考完PMP之后不要错过软考!

对于刚刚考完PMP的同学来说&#xff0c;趁热打铁继续学习软考&#xff0c;考一个软考证书是性价比极高的选择&#xff0c;不只是因为软考本身具备的高价值&#xff0c;更因为PMP软考能够达到11&#xff1e;2的效果&#xff01; 下面&#xff0c;老师就为大家详细讲解考完PMP之…