vue3开发移动端星座物语
技术选型:Vue3.0 + axios + sass
后台API接口:聚合API 星座物语
API请求路径: http://web.juhe.cn/constellation/getAll
使用vue-cli脚手架搭建项目:vue create constellation-pro
安装依赖:npm i -S axios js-cookie
module.exports = {
publicPath: './',
devServer: {
proxy: {
"/api": {
target: "http://web.juhe.cn/",
changeOrigin: true,
ws: true,
secure: false,
pathRewrite: {
'^/api': ''
}
},
}
},
lintOnSave: false
}
聚合API同一个接口每天只能请求50次,通过本地服务器来拦截请求并存入内存,下次访问同一个接口就会从内存中获取,不会重复请求接口,从而解决接口次数限制问题
请求地址中需要携带唯一key
keys.js
export JUHE_APPKEY = "..."
export {
JUHE_APPKEY
}
libs/http.js
import axios from 'axios'
import { JUHE_APPKEY } from '@/config/keys'
function axiosGet(options) {
// 拼接key,后续请求就不用每次都拼接key了
axios(options.url + "&key=" + JUHE_APPKEY) // 返回一个Promise对象
.then(res => { // 使用then接收
options.success(res)
})
.catch(err => {
options.error(err)
})
}
export {
axiosGet
}
services/request.js
import { axiosGet } from '@/libs/http'
function getData(consName, type) {
// 为了使用async await 需要返回一个promise
return new Promise((resolve, reject) => {
axiosGet({
url: `/api/contellation/getAll?consName=${consName}&type=${type}`,
success(data) {
resolve(data)
},
error(err) {
reject(err)
}
})
})
}
export {
getData
}
Today.vue:
import { onMounted } from 'vue'
import { getData } from '@/services/request'
export default {
name: 'todayPage',
setup() {
// 在生命周期onMounted钩子函数中请求数据
onMounted(() => {
getData('双子座', 'today') // 拿到数据
})
}
}
store/state.js:
export default {
consName: '双子座',
field: 'today'
}
store/mutations.js:
export default {
setConsName(state, consName) {
state.consName = consName
},
setField(state, field) {
state.field = field
}
}
store/index.js:
import { createStore } from 'vuex'
import state from './state'
import mutations from './mutations'
export default createStore({
state,
mutations
})
services/index.js:
import { getData } from './request'
// 返回一个异步函数
export default async (store) => {
const consName = store.state.consName,
field = store.state.field,
data = await getData(consName, field)
console.log(data)
}
Today.vue
store在vue文件里通过vuex中的useStore
钩子来取:
import { useStore } from 'vuex'
import getData from '@/services'
const store = useStore() // 拿到store
onMounted(() => {
getData(store) // 同样可拿到数据
})
errorCode: 0,
setErrorCode(state, errorCode) {
state.errorCode = errorCode
},
services/index.js:
import { getData } from './request'
export default async (store) => {
const consName = store.state.consName,
field = store.state.field,
data = await getData(consName, field);
console.log(data)
if(data.data.error_code) {
store.commit("setErrorCode", data.data.error_code)
return
}
}
配置state:
today: {},
tomorrow: {},
week: {},
month: {},
year: {}
配置mutations:
// 因为filed就代表的state中的today、tomorrow、week...
setData(state, data) {
// 动态修改state
state[state.field] = data
}
触发mutations:
services/index.js:
store.commit("setData", data)
Header/index.vue:
<template>
<div class="app-header">
<img src="@/assets/img/cons.png" alt="">
<span>
<slot></slot>
</span>
</div>
</template>
<script>
export default {
name: "MyHeader"
}
</script>
<style lang="scss" scoped>
.app-header {
width: 100%;
height: 44px;
background: #e57e94;
position: fixed;
top: 0;
display: flex;
justify-content: center;
align-items: center;
color: white;
font-weight: 600;
z-index: 1;
img {
width: 30px;
height: 30px;
margin-right: 10px;
}
}
</style>
Tab/icon.vue:
<template>
<router-link
:to="path"
class="tab-icon"
>
<i class="icon">{{ iconText }}</i>
<p class="text">
<slot></slot>
</p>
</router-link>
</template>
<script>
export default {
name: "TabIcon",
props: {
iconText: String,
path: String
}
}
</script>
<style lang='scss' scoped>
.tab-icon {
text-decoration: none;
display: flex;
flex-direction: column;
align-items: center;
.icon {
display: flex;
align-items: center;
justify-content: center;
width: 25px;
height: 25px;
border-radius: 50%;
background-color: #dddddd;
color: #999999;
font-size: 12px;
font-style: inherit;
text-align: center;
margin-top: 2px;
transition: color .5s;
}
&.router-link-active {
.icon{
background-color: #DB7093;
color: #ffffff;
}
.text {
color: #DB7093;
}
}
.text {
margin: 0;
font-size: 14px;
color: #999999;
}
}
</style>
Tab/index.vue:
js
App.vue:
<template>
<my-header>星座物语</my-header>
<tab></tab>
<router-view/>
</template>
<script>
import MyHeader from '@/components/Header'
import Tab from '@/components/Tab'
export default ({
components: {
MyHeader,
Tab
},
setup() {
},
})
</script>
<style lang="scss">
* {
margin: 0;
padding: 0;
}
</style>
使用vue中的watch
来监听路由的改变,来修改store中的field:
// App.vue
import { watch } from 'vue'
import { useRouter } from 'vue-router'
import { useStore } from 'vuex'
setup() {
const store = useStore(),
state = store.state,
router = useRouter()
// 监听路由的切换并且同步修改store中的field
watch(() => {
return router.currentRoute.value.name
}, (value) => {
store.commit('setField', value)
})
},
避免在切换路由的时候都请求接口,我们可以缓存组件,减少不需要的请求次数:
// App.vue
<router-view v-slot="{ Component }">
<!-- 缓存所有组件 -->
<keep-alive>
<component :is="Component"/>
</keep-alive>
</router-view>
效果如下:
https://s1.328888.xyz/2022/04/12/fD1oB.gif
新建组件:
index.vue:
<template>
<div class="nav-bar">
<div class="scroll-wrapper">
<div class="nav-wrapper" :style="`width:${navData.length * 20}vw`">
<nav-item
v-for="(item, index) in navData" :key="index"
:item='item'
:curIdx='curIdx'
:index='index'
@navClick="navClick"
/>
</div>
</div>
</div>
</template>
<script>
import navData from '@/datas/nav'
import NavItem from './item'
import getData from '@/services'
import { reactive, ref, toRefs } from 'vue'
import { useStore } from 'vuex'
export default {
name: 'NavBar',
components: {
NavItem
},
setup () {
const curIdx = ref(0),
state = reactive({
navData
}),
store = useStore()
const navClick = (index) => {
curIdx.value = index
const consName = state.navData[curIdx.value]
store.commit("setConsName", consName)
getData(store)
}
return {
...toRefs(state),
navClick,
curIdx
}
}
}
</script>
<style lang='scss'>
.nav-bar{
width: 100%;
height: 38px;
border-bottom: 1px solid #dddddd;
box-sizing: border-box;
background-color: #ffffff;
overflow: hidden;
.scroll-wrapper {
height: 44px;
overflow-x: auto;
.nav-wrapper {
display: flex;
flex-direction: row;
height: 42px;
}
}
}
</style>
item.vue:
<template>
<div
:class="['nav-item', { 'nav-current': index === curIdx }]"
@click="navClick(index)"
>
{{ item }}
</div>
</template>
<script>
export default {
name: 'NavItem',
props: {
item: String,
curIdx: Number,
index: Number
},
setup(props, { emit }) { // ctx 或者解构(emit)
const navClick = (index) => {
// ctx.emit('navClick', index)
emit('navClick', index)
}
return {
navClick
}
}
}
</script>
<style lang='scss'>
.nav-item {
width: 75px;
height: 100%;
font-size: 14px;
line-height: 40px;
text-align: center;
color: #666666;
display: inline-block;
&.nav-current {
color: #DB7093;
font-weight: bold;
}
}
</style>
NavBar
组件中删除不必要的步骤:NavBar
item.vue
<template>
<div
class="nav-item"
>
{{ item }}
</div>
</template>
<script>
export default {
name: 'NavItem',
props: {
item: String,
}
}
</script>
index.vue
<template>
<div class="nav-bar">
<div class="scroll-wrapper">
<div class="nav-wrapper" :style="`width:${navData.length * 20}vw`">
<nav-item
v-for="(item, index) in navData" :key="index"
:item='item'
/>
</div>
</div>
</div>
</template>
<script>
import navData from '@/datas/nav'
import NavItem from './item'
import { ref } from 'vue'
export default {
name: 'NavBar',
components: {
NavItem
},
setup () {
const curIdx = ref(0)
return {
curIdx,
navData
}
}
}
</script>
directives
index.js
// 自定义指令出口文件
import navCurrent from './navCurrent'
export {
navCurrent
}
navCurrent.js
export default {
// 固定写法
mounted(el, binding) {},
updated(el, binding) {}
}
NavBar/index.vue
<template>
<div
class="nav-bar"
v-nav-current="{
className: 'nav-item',
activeClass: 'nav-current',
curIdx
}"
>
<div class="scroll-wrapper">
<div class="nav-wrapper" :style="`width:${navData.length * 20}vw`">
<nav-item
v-for="(item, index) in navData"
:key="index"
:item='item'
/>
</div>
</div>
</div>
</template>
<script>
import navData from '@/datas/nav'
import NavItem from './item'
import { ref } from 'vue'
import { navCurrent } from '@/directives'
export default {
name: 'NavBar',
components: {
NavItem
},
directives: {
navCurrent
},
setup () {
const curIdx = ref(0)
return {
curIdx,
navData,
}
}
}
</script>
到现在为止,可以在navCurrent.js中看到参数el里有我们配置进去的信息了:
el: 给设置了自定义指令的DOM结构
binding: 包含v-nav-current指令的配置信息
完善自定义指令开发:
mounted(el, binding) {
const { className, curIdx, activeClass } = binding.value,
oNavItems = el.getElementsByClassName(className)
oNavItems[curIdx].className += ` ${activeClass}`
},
到现在为止,我们已经完成了当前项高亮。
接下来实现切换导航,目标导航高亮,上一个导航高亮消失:
我们需要设置点击事件,拿到当前项索引,然后赋值
这就用到了H5的接口,在子组件item中配置索引,:data-index="index"
// item.vue
<template>
<div
class="nav-item"
:data-index="index"
>
{{ item }}
</div>
</template>
<script>
export default {
name: 'NavItem',
props: {
item: String,
index: Number
}
}
</script>
在父组件中配置事件委托,配置索引:
// index.vue
<template>
<div
class="nav-bar"
v-nav-current="{
className: 'nav-item',
activeClass: 'nav-current',
curIdx
}"
@click="navClick($event)"
>
<div class="scroll-wrapper">
<div class="nav-wrapper" :style="`width:${navData.length * 20}vw`">
<nav-item
v-for="(item, index) in navData"
:key="index"
:item='item'
:index='index'
/>
</div>
</div>
</div>
</template>
setup () {
const curIdx = ref(0)
const navClick = (e) => {
const className = e.target.className
if(className == 'nav-item') {
const idx = e.target.dataset.index
curIdx.value = idx
}
}
return {
curIdx,
navData,
navClick
}
}
最后,实现切换导航实现高亮:
navCurrent.js:
updated(el, binding) {
const { className, curIdx, activeClass } = binding.value,
oOptions = binding.oldValue,
oNavItems = el.getElementsByClassName(className)
oNavItems[curIdx].className += ` ${activeClass}`
oNavItems[oOptions.curIdx].className = className
}
NavBar/index.vue:
const curIdx = ref(0),
store = useStore()
const navClick = (e) => {
const className = e.target.className
if(className == 'nav-item') {
const tar = e.target,
idx = tar.dataset.index,
consName = tar.innerText
curIdx.value = idx
store.commit('setConsName', consName)
}
}
components/common/Card.vue:
<template>
<div class="card">
<div class="wrapper">
<img
:src="require(`@/assets/img/${ name || '白羊座'}.jpeg`)"
:alt="name"
/>
<div class="mask">
<span>{{ name || '白羊座' }}</span>
<p v-if="allIndex">综合指数: {{ allIndex || 0 }}</p>
</div>
</div>
</div>
</template>
<script>
export default {
name: "ConsCard",
props: {
name: String,
allIndex: [String, Number]
}
}
</script>
<style lang="scss" scoped>
.card {
padding: 10px;
box-sizing: border-box;
.wrapper {
position: relative;
border-radius: 5px;
overflow: hidden;
img {
width: 100%;
vertical-align: bottom;
}
.mask {
display: flex;
flex-direction: column;
justify-content: center;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.4);
text-align: center;
span {
color: #FFD7F5;
font-size: 48px;
font-weight: none;
line-height: 2em;
}
p {
font-size: 16px;
color: #ffffff;
}
}
}
}
</style>
使用:
<ConsCard
:name="todayData.name"
:allIndex="todayData.all"
/>
setup() {
const store = useStore(),
state = store.state
onMounted(() => {
getData(store)
})
return {
todayData: computed(() => state.today)
}
}
components/common/index.js:
import ConsCard from '@/components/common/Card'
const MyPlugin = {}
MyPlugin.install = function(Vue) {
Vue.component(ConsCard.name, ConsCard)
}
export default MyPlugin
mainjs中导入:
import MyPlugin from './components/common'
createApp(App).use(store).use(router).use(MyPlugin).mount('#app')