【Vue3.0 移动端项目开发】星座物语(持续更新~)

文章目录

  • 星座物语
    • 项目介绍及初始化
      • 1、项目搭建
      • 2、项目结构划分
      • 3、跨域配置
      • 4、搭建本地server缓存聚合接口数据
    • 项目开发
      • 1、唯一key的配置
      • 2、axios封装
      • 3、接口测试
      • 4、配置store
      • 5、 入参从store中动态获取
      • 6、数据的存储和error_code的配置
      • 7、Header组件、Tab组件的开发
      • 8、Header组件、Tab组件的使用
      • 8、切换Tab的时候,将store中的field同步
      • 9、使用keep-alive来缓存组件
      • 10、顶部滑动NavBar组件的开发
      • 11、导航切换逻辑自定义指令的开发
        • 第一步: 我们在`NavBar`组件中删除不必要的步骤:
        • 第二步: 在src下新建自定义指令:
        • 第三步: 在NavBar组件中编写自定义组件v-nav-current
      • 12、切换Nav导航后同步store中的consName
      • 13、Card组件开发
      • 14、公共组件全局注册(card组件全局注册)

星座物语

项目介绍及初始化

vue3开发移动端星座物语

【Vue3.0 移动端项目开发】星座物语(持续更新~)_第1张图片

技术选型:Vue3.0 + axios + sass
后台API接口:聚合API 星座物语

API请求路径: http://web.juhe.cn/constellation/getAll

1、项目搭建

使用vue-cli脚手架搭建项目:vue create constellation-pro

安装依赖:npm i -S axios js-cookie

2、项目结构划分

  • src
    • assets
      • css
      • img
      • js
    • components
    • configs
      • keys.js (存放唯一key)
    • datas 前端需要循环遍历的数据
      • nav.js (头部导航数据)
      • tab.js (底部导航栏数据)
    • libs 数据请求第一层
      • http.js (axios请求函数封装)
    • router
      • index.js (路由)
    • services 数据请求第二层提取
      • index.js (使用request.js 从store中获取数据)
      • request.js (基于http.js封装get、post请求)
    • store (vuex)
      • index.js
      • mutations.js
      • state.js
    • views (页面)
      • Month.vue
      • Today.vue
      • Tomorrow.vue
      • Week.vue
      • Year.vue
    • App.vue (项目根组件)
    • main.js (项目入口)

3、跨域配置

  • vue.config.js
module.exports = {
  publicPath: './',
  devServer: {
    proxy: {
      "/api": {
        target: "http://web.juhe.cn/",
        changeOrigin: true,
        ws: true,
        secure: false,
        pathRewrite: {
          '^/api': ''
        }
      },
    }
  },
  lintOnSave: false
}

4、搭建本地server缓存聚合接口数据

聚合API同一个接口每天只能请求50次,通过本地服务器来拦截请求并存入内存,下次访问同一个接口就会从内存中获取,不会重复请求接口,从而解决接口次数限制问题

项目开发

1、唯一key的配置

请求地址中需要携带唯一key

  • keys.js

    export JUHE_APPKEY = "..."
    
    export {
    	JUHE_APPKEY
    }
    

2、axios封装

  1. 请求的第一层封装

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
}
  1. 请求的第二层封装

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
}

3、接口测试

Today.vue:

import { onMounted } from 'vue'
import { getData } from '@/services/request'

export default {
	name: 'todayPage',
	setup() {
		// 在生命周期onMounted钩子函数中请求数据
		onMounted(() => {
			getData('双子座', 'today') // 拿到数据
		})
	}
}

4、配置store

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
})

5、 入参从store中动态获取

  1. 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)
}
  1. Today.vue

store在vue文件里通过vuex中的useStore钩子来取:

import { useStore } from 'vuex'
import getData from '@/services'

const store = useStore() // 拿到store
onMounted(() => {
	getData(store) // 同样可拿到数据
})

6、数据的存储和error_code的配置

  1. 将error_code放入state中

errorCode: 0,

  1. 配置error_code的mutations来修改state中的error_code
setErrorCode(state, errorCode) {
    state.errorCode = errorCode
},
  1. 在拿到数据后触发errorCode的mutation

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
  }
}
  1. 数据的存储

配置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)

7、Header组件、Tab组件的开发

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

8、Header组件、Tab组件的使用

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>

8、切换Tab的时候,将store中的field同步

使用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)
    })
  },

9、使用keep-alive来缓存组件

避免在切换路由的时候都请求接口,我们可以缓存组件,减少不需要的请求次数:

// App.vue
<router-view v-slot="{ Component }">
  <!-- 缓存所有组件 -->
  <keep-alive>
    <component :is="Component"/>
  </keep-alive>
</router-view>

10、顶部滑动NavBar组件的开发

效果如下:
https://s1.328888.xyz/2022/04/12/fD1oB.gif

新建组件:

  • NavBar
    • index.vue
    • item.vue

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>

11、导航切换逻辑自定义指令的开发

第一步: 我们在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>
      

第二步: 在src下新建自定义指令:

  • directives

    • index.js

      // 自定义指令出口文件
      import navCurrent from './navCurrent'
      
      export {
        navCurrent
      }
      
    • navCurrent.js

      export default {
      	// 固定写法
        mounted(el, binding) {},
        updated(el, binding) {}
      }
      

第三步: 在NavBar组件中编写自定义组件v-nav-current

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
  }

12、切换Nav导航后同步store中的consName

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)
  }
}

13、Card组件开发

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)
  }
}

14、公共组件全局注册(card组件全局注册)

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')

你可能感兴趣的:(Vue3.0,Vue3.0,实战项目)