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') 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
封装第三方按钮计算属性版
- 封装第三方按钮-使用计算属性。
- 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>
提交表单信息
- 对表单进行校验。
- 发送请求。
- 登录成功:存储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 = 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
-
- 一般是在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.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>
-
会有一个问题-路由错乱的问题。
登录页的跳转
- 让登录页中可以直接跳转回来源页面。
- 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>
返回上一页功能
- 单独做一个组件,专门来处理返回逻辑。
- 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) // 渲染虚拟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
详情页收藏按钮
- 不用传统的登录态校验,但一些区域或功能需要用到个人信息。
- 所以需要优化个人信息的处理。
- 所有的涉及收藏的状态及操作和前后端数据交互,都放在全局公共状态里。
- 在需要用到收藏相关的状态及操作,都要调用全局公共状态方法。
优化个人信息的处理
- 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 详情页
- 由于没有登录而进入到登录页,不能直接用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) 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>
- 由于没有登录而进入到登录页,不能直接用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 = 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 详情页
- 由于没有登录而进入到登录页,不能直接用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-icon name="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.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>
-
打包
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) => number
let 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源码。
- 学习前端算法。
- 走管理,熟悉公司的业务,会培训带领新人,写文档。会和后端进行交互,
-
离职原因
- 不要说上家公司坏话,如技术栈不新、公司抠门、领导差之类的。
- 尽量多写客观原因:
- 可以说公司业绩不太好-公司暗示要解散项目组。
- 公司倒闭了,但压了自己的工资,老大那边压力也大,后面帮做最后一个项目里,结束最后一个业务后,就结束了。
- 要结婚之类的。
- 可以说公司业绩不太好-公司暗示要解散项目组。