20230726----重返学习-vue3项目实战-知乎日报第3天-TS-简历
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 routerimport { 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中文网
相关文章:
20230726----重返学习-vue3项目实战-知乎日报第3天-TS-简历
day-121-one-hundred-and-twenty-one-20230726-vue3项目实战-知乎日报第3天-TS-简历 vue3项目实战-知乎日报第3天 封装按钮组件 jsx函数式组件 只能做静态页面,内部没有方法让它自动更新。 封装第三方按钮-非计算属性版 封装第三方按钮-不使用计算属性 src/c…...
TypeScript 在前端开发中的应用实践
TypeScript 在前端开发中的应用实践 TypeScript 已经成为前端开发领域越来越多开发者的首选工具。它是一种静态类型的超集,由 Microsoft 推出,为开发者提供了强大的静态类型检查、面向对象编程和模块化开发的特性,解决了 JavaScript 的动态类…...
商业密码应用安全性评估量化评估规则2023版更新点
《商用密码应用安全性评估量化评估规则》(2023版)已于2023年7月发布,将在8月1日正式执行。相比较2021版,新版本有多处内容更新,具体包括5处微调和5处较大更新。 微调部分(5处) 序号2021版本202…...
【软件测试】单元测试工具---Junit详解
1.junit 1.1 junit是什么 JUnit是一个Java语言的单元测试框架。 虽然我们已经学习了selenium测试框架,但是有的时候测试用例很多,我们需要一个测试工具来管理这些测试用例,Junit就是一个很好的管理工具,简单来说Junit是一个针对…...
【算法基础:搜索与图论】3.4 求最短路算法(Dijkstrabellman-fordspfaFloyd)
文章目录 求最短路算法总览Dijkstra朴素 Dijkstra 算法(⭐原理讲解!⭐重要!)(用于稠密图)例题:849. Dijkstra求最短路 I代码1——使用邻接表代码2——使用邻接矩阵 补充:稠密图和稀疏…...
【Matlab】基于卷积神经网络的数据分类预测(Excel可直接替换数据)
【Matlab】基于卷积神经网络的数据分类预测(Excel可直接替换数据) 1.模型原理2.数学公式3.文件结构4.Excel数据5.分块代码6.完整代码7.运行结果1.模型原理 基于卷积神经网络(Convolutional Neural Network,CNN)的数据分类预测是一种常见的深度学习方法,广泛应用于图像识…...
【C++ 重要知识点总结】自定义类型-枚举和联合
复杂类型 除了类之外还有Union、Enum连个特殊的类型。 Union 概念 union即为联合,它是一种特殊的类。通过关键字union进行定义,一个union可以有多个数据成员。 union Token{char cval;int ival;double dval; };用法 互斥赋值。在任意时刻,…...
Centos MySql安装,手动安装保姆级教程
1.删除原有的mariadb,不然mysql装不进去 查询MAriaDB命令 rpm -qa|grep mariadb 删除 rpm -e --nodeps mariadb-libs-5.5.60-1.el7_5.x86_64 (yum -y remove mysql 如需要清除服务器上以前安装过的MySQL可执行此命令,执行前一…...
电脑C盘空间大小调整 --- 扩容(扩大/缩小)--磁盘分区大小调整/移动
概述: 此方法适合C盘右边没有可分配空间(空闲空间)的情况,D盘有数据不方便删除D盘分区的情况下,可以使用傲梅分区助手软件进行跨分区调整分区大小,不会损坏数据。反之可直接使用系统的磁盘管理工具进行调整…...
centos7设置网桥网卡
安装bridge-utils yum install bridge-utils修改ens33 网卡 TYPEEthernet BOOTPROTOnone DEFROUTEyes IPV4_FAILURE_FATALno IPV6INITyes IPV6_AUTOCONFyes IPV6_DEFROUTEyes IPV6_FAILURE_FATALno NAMEens33 UUID04b97484-25c8-45c7-8c8c-e335e8080e10 DEVICEens33 ONBOOTye…...
TCP模型和工作沟通方式
我们如何与客户沟通?理科生和技术人员可能在沟通技巧方面有所欠缺。 那么我们如何理解和掌握沟通的原则和技巧呢?我发现TCP网络交互模型很好的描述了沟通的原则和要点。下面我们就从TCP来讲沟通的过程。 TCP的客户端就像客户(甲方ÿ…...
Langchain 的 ConversationSummaryBufferMemory
Langchain 的 ConversationSummaryBufferMemory ConversationSummaryBufferMemory 在内存中保留最近交互的缓冲区,但不仅仅是完全刷新旧的交互,而是将它们编译成摘要并使用两者。但与之前的实现不同的是,它使用令牌长度而不是交互次数来确定何…...
【Rust 基础篇】Rust 通道实现单个消费者多个生产者模式
导言 在 Rust 中,我们可以使用通道(Channel)来实现单个消费者多个生产者模式,简称为 MPMC。MPMC 是一种常见的并发模式,适用于多个线程同时向一个通道发送数据,而另一个线程从通道中消费数据的场景。本篇博…...
HTTP协议各版本介绍
HTTP协议是一种用于传输Web页面和其他资源的协议。 下面详细介绍一下HTTP的各个版本: 1.HTTP/0.9 这是最早的HTTP版本,于1991年发布。它非常简单,只能传输HTML格式的文本,并且不支持其他类型的资源、请求头和状态码。 2.HTTP/1…...
玩转ChatGPT:Custom instructions (vol. 1)
一、写在前面 据说GPT-4又被削了,前几天让TA改代码,来来回回好几次才成功。 可以看到之前3小时25条的限制,现在改成了3小时50条,可不可以理解为:以前一个指令能完成的任务,现在得两条指令? 可…...
黄东旭:The Future of Database,掀开 TiDB Serverless 的引擎盖
在 PingCAP 用户峰会 2023 上, PingCAP 联合创始人兼 CTO 黄东旭 分享了“The Future of Database”为主题的演讲, 介绍了 TiDB Serverless 作为未来一代数据库的核心设计理念。黄东旭 通过分享个人经历和示例,强调了数据库的服务化而非服务化…...
Linux环境搭建(XShell+云服务器)
好久不见啊,放假也有一周左右了,简单休息了下(就是玩了几天~~),最近也是在学习Linux,现在正在初步的学习阶段,本篇将会简单的介绍一下Linux操作系统和介绍Linux环境的安装与配置,来帮…...
-bash: /bin/rm: Argument list too long
有套数据库环境,.aud文件太多导致/u01分区使用率过高,rm清理时发现报错如下 [rootdb1 audit]# rm -rf ASM1_ora_*202*.aud -bash: /bin/rm: Argument list too long [rootdb1 audit]# rm -rf ASM1_ora_*20200*.aud -bash: /bin/rm: Argument list too…...
5个步骤完成Linux 搭建Jdk1.8环境
1:首先,在Linux系统中创建一个目录,用于存放JDK文件。可以选择在/opt目录下创建一个新的文件夹,例如/opt/jdk。 sudo mkdir /opt/jdk 2:将下载的jdk-8u381-linux-x64.tar.gz文件复制到新创建的目录中。 sudo cp jdk…...
【JAVASE】运算符
⭐ 作者:小胡_不糊涂 🌱 作者主页:小胡_不糊涂的个人主页 📀 收录专栏:浅谈Java 💖 持续更文,关注博主少走弯路,谢谢大家支持 💖 运算符 1. 什么是运算符2. 算术运算符3.…...
Python实现prophet 理论及参数优化
文章目录 Prophet理论及模型参数介绍Python代码完整实现prophet 添加外部数据进行模型优化 之前初步学习prophet的时候,写过一篇简单实现,后期随着对该模型的深入研究,本次记录涉及到prophet 的公式以及参数调优,从公式可以更直观…...
鸿蒙中用HarmonyOS SDK应用服务 HarmonyOS5开发一个生活电费的缴纳和查询小程序
一、项目初始化与配置 1. 创建项目 ohpm init harmony/utility-payment-app 2. 配置权限 // module.json5 {"requestPermissions": [{"name": "ohos.permission.INTERNET"},{"name": "ohos.permission.GET_NETWORK_INFO"…...
AI,如何重构理解、匹配与决策?
AI 时代,我们如何理解消费? 作者|王彬 封面|Unplash 人们通过信息理解世界。 曾几何时,PC 与移动互联网重塑了人们的购物路径:信息变得唾手可得,商品决策变得高度依赖内容。 但 AI 时代的来…...
A2A JS SDK 完整教程:快速入门指南
目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库ÿ…...
【Linux系统】Linux环境变量:系统配置的隐形指挥官
。# Linux系列 文章目录 前言一、环境变量的概念二、常见的环境变量三、环境变量特点及其相关指令3.1 环境变量的全局性3.2、环境变量的生命周期 四、环境变量的组织方式五、C语言对环境变量的操作5.1 设置环境变量:setenv5.2 删除环境变量:unsetenv5.3 遍历所有环境…...
django blank 与 null的区别
1.blank blank控制表单验证时是否允许字段为空 2.null null控制数据库层面是否为空 但是,要注意以下几点: Django的表单验证与null无关:null参数控制的是数据库层面字段是否可以为NULL,而blank参数控制的是Django表单验证时字…...
Java详解LeetCode 热题 100(26):LeetCode 142. 环形链表 II(Linked List Cycle II)详解
文章目录 1. 题目描述1.1 链表节点定义 2. 理解题目2.1 问题可视化2.2 核心挑战 3. 解法一:HashSet 标记访问法3.1 算法思路3.2 Java代码实现3.3 详细执行过程演示3.4 执行结果示例3.5 复杂度分析3.6 优缺点分析 4. 解法二:Floyd 快慢指针法(…...
React从基础入门到高级实战:React 实战项目 - 项目五:微前端与模块化架构
React 实战项目:微前端与模块化架构 欢迎来到 React 开发教程专栏 的第 30 篇!在前 29 篇文章中,我们从 React 的基础概念逐步深入到高级技巧,涵盖了组件设计、状态管理、路由配置、性能优化和企业级应用等核心内容。这一次&…...
前端调试HTTP状态码
1xx(信息类状态码) 这类状态码表示临时响应,需要客户端继续处理请求。 100 Continue 服务器已收到请求的初始部分,客户端应继续发送剩余部分。 2xx(成功类状态码) 表示请求已成功被服务器接收、理解并处…...
Java后端检查空条件查询
通过抛出运行异常:throw new RuntimeException("请输入查询条件!");BranchWarehouseServiceImpl.java // 查询试剂交易(入库/出库)记录Overridepublic List<BranchWarehouseTransactions> queryForReagent(Branch…...
