day-121-one-hundred-and-twenty-one-20230726-vue3项目实战-知乎日报第3天-TS-简历
vue3项目实战
-知乎日报第3天
封装按钮组件
jsx函数式组件
- 只能做静态页面,内部没有方法让它自动更新。
封装第三方按钮-非计算属性版
- 封装第三方按钮-不使用计算属性
- 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
封装第三方按钮计算属性版
- 封装第三方按钮-使用计算属性。
- 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>
提交表单信息
- 对表单进行校验。
- 发送请求。
- 登录成功:存储token、进行提示。
- 获取登录者信息、进行页面的跳转。
获取登录者信息
- 从服务器获取登录者信息。
- 一般是在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
-
- 一般是在pinia中创建出来的。
登录态校验
-
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>
-
会有一个问题-路由错乱的问题。
登录页的跳转
- 让登录页中可以直接跳转回来源页面。
- 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>
返回上一页功能
- 单独做一个组件,专门来处理返回逻辑。
- 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>
函数式调用组件的封装
-
先写一个主组件。主组件可以用模板组件,也可以用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>
-
-
写一个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
详情页收藏按钮
- 不用传统的登录态校验,但一些区域或功能需要用到个人信息。
- 所以需要优化个人信息的处理。
- 所有的涉及收藏的状态及操作和前后端数据交互,都放在全局公共状态里。
- 在需要用到收藏相关的状态及操作,都要调用全局公共状态方法。
优化个人信息的处理
- 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 详情页
- 由于没有登录而进入到登录页,不能直接用push(),因为会添加一条记录,导致登录成功后重新跳转回详情页之后,会新增一条详情记录。此时登录成功后点击返回,依旧是在详情页。所以这里只能使用
replace()
进登录页,用target字段
标识,则在登录成功后
,退回到详情页。- 这个在登录页中做特殊处理,如果有
target字段
标识,则在登录成功后
,跳转到target字段
对应的路径中。
- 这个在登录页中做特殊处理,如果有
- 而用
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>
- 由于没有登录而进入到登录页,不能直接用push(),因为会添加一条记录,导致登录成功后重新跳转回详情页之后,会新增一条详情记录。此时登录成功后点击返回,依旧是在详情页。所以这里只能使用
-
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 详情页
- 由于没有登录而进入到登录页,不能直接用push(),因为会添加一条记录,导致登录成功后重新跳转回详情页之后,会新增一条详情记录。此时登录成功后点击返回,依旧是在详情页。所以这里只能使用
replace()
进登录页,用target字段
标识,则在登录成功后
,退回到详情页。- 这个在登录页中做特殊处理,如果有
target字段
标识,则在登录成功后
,跳转到target字段
对应的路径中。
- 这个在登录页中做特殊处理,如果有
- 而用
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>
- 由于没有登录而进入到登录页,不能直接用push(),因为会添加一条记录,导致登录成功后重新跳转回详情页之后,会新增一条详情记录。此时登录成功后点击返回,依旧是在详情页。所以这里只能使用
-
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>
-
打包
vite按需导入插件vite-plugin-imp
与vant@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
- 主要就是为了开发时限定类型,让代码更严谨。
- 开发时用
ts
代替js
,用tsx
代替jsx
。
- 开发时用
- 类型 对各种变量/值,进行类型限制
- 类型断言
- 在函数中使用各种声明和限制
- 在类中的处理 public/private/protected
与es5及es6的关系
类型的限定
- 对各种变量/值,进行类型限制
常见类型
/*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 任意类型*/
类型断言
- 一定小心使用,相关于程序员用
人格保证
了,就是不是,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 = ...*/
函数类型
- 在函数中使用各种声明和限制。
/*函数的玩法普通函数:声明参数和返回值类型function fn(x:number,y:number):number{...}函数表达式:在普通函数基础上,对赋值的函数做类型校验 type Fn = (x:number,y?:number) => numberlet fn:Fn = function(x,y){...}*/
类的类型
高级类型与联合类型
接口
接口与type
泛型
ts的应用
- 在
@vue/cli
中使用 - 在
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"]} }
简历
注意细节
- 先有面试再说后面的事。
- 先有word版写好,后面再复制到网站的模板上。
- 先全员海投,有面试机会再看具体信息。(无脑投)
- 在
BOSS直聘
一天100个左右,其它投到上限。(用一个小时左右投) - 先看到有的,后面去试,可以准备给朋友。(可以记录下要面试的题)。
- 面试时,一般就说在我之前的项目中…而不要八股文。(个人真实就好了)
- 带上笔和本-面试遇到不会的问题,是面试的开始,而不是结束。
- 当上不会的题或东西,当着面试官的面来记,再说后面再查,晚上回去再查。这个也要真查,因为提到的可能就是新的主流东西。
- 再问对方写代码了多少年,夸奖面试官。多少年之后,比自己少的,夸对方厉害。比自己多,不愧是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源码。
- 学习前端算法。
- 走管理,熟悉公司的业务,会培训带领新人,写文档。会和后端进行交互,
-
离职原因
- 不要说上家公司坏话,如技术栈不新、公司抠门、领导差之类的。
- 尽量多写客观原因:
- 可以说公司业绩不太好-公司暗示要解散项目组。
- 公司倒闭了,但压了自己的工资,老大那边压力也大,后面帮做最后一个项目里,结束最后一个业务后,就结束了。
- 要结婚之类的。
- 可以说公司业绩不太好-公司暗示要解散项目组。
进阶参考
- ts中文网