导航


HTML

CSS

JavaScript

浏览器 & 网络

版本管理

框架

构建工具

TypeScript

性能优化

软实力

算法

UI、组件库

Node

业务技能

针对性攻坚

AI


项目搭建与配置

项目初始化

? Check the features needed for your project: Choose Vue version, Babel, Router, Vuex, CSS Pre-process
ors, Linter
? Choose a version of Vue.js that you want to start the project with 3.x
? Use history mode for router? (Requires proper server setup for index fallback in production) No
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): Sass/SCSS
 (with node-sass)
? Pick a linter / formatter config: Basic
? Pick additional lint features: Lint on save
? Where do you prefer placing config for Babel, ESLint, etc.? In package.json
? Save this as a preset for future projects? No

src 目录结构改造

.
├── App.vue
├── assets
├── components
├── main.js
├── router
│   └── index.js
├── store
│   └── index.js
└── views
.
├── css
│   ├── border.css # 移动端1px边框
│   └── resets.css # 标签重置
├── img
│   ├── error.jpg
│   ├── 中秋节.jpg
│   ├── 元旦.jpg
│   ├── 劳动节.jpg
│   ├── 国庆节.jpg
│   ├── 春节.jpg
│   ├── 清明节.jpg
│   ├── 端午节.jpg
│   └── 除夕.jpg
└── js
    ├── common.js # 初始化 fastclick;消除 touchmove 警告;移动端 rem 适配
    └── fastclick.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

import '@/assets/css/resets.css';
import '@/assets/css/border.css';
import '@/assets/js/common.js';

createApp(App).use(store).use(router).mount('#app')

vue.config.js 配置

module.exports = {
  devServer: {
    overlay: { // 报错页面蒙层关闭
      warning: false,
      errors: false
    },
    proxy: {
      '/api': { // 遇到接口为 /api 开头的,做以下代理配置
        target: '<http://v.juhe.cn/>',
        changeOrigin: true, // 改变源 进行跨域
        ws: true, // 开启 websocket
        secure: false, // https 检查关闭
        pathRewrite: { // 路径重写
          '^/api/': '', // 把 /api/myAPI/path 变为 myAPI/path, 然后代理到 <http://localhost:8080> 这个代理服务器下边
        }
      }
    }
  },
  lintOnSave: false, // 关闭 eslint 检查
}

关闭 eslint 检查

eslint 检查还得关闭 package.json 下的以下属性:

"eslintConfig": {
  "root": false, // 设置 false
  "env": {
    "node": false // 设置 false
  },
  ...
}

安装 axios 和 qs

npm i axios qs -S

创建三个 tabbar 页面

.
├── Day.vue
├── Month.vue
└── Year.vue

配置路由

import { createRouter, createWebHashHistory } from 'vue-router'
import DayPage from '@/views/Day';
const routes = [
  {
    path: '/',
    name: 'DayPage',
    component: DayPage
  },
  {
    path: '/month',
    name: 'MonthPage',
    component: () => import('@/views/Month.vue')
  },
  {
    path: '/year',
    name: 'YearPage',
    component: () => import('@/views/Day.vue')
  }
]

const router = createRouter({
  history: createWebHashHistory(),
  routes
})

export default router

全局配置 configs

配置 keys.js 存储 聚合 API key

const JUHE_APP_KEY = 'your api key';

export {
  JUHE_APP_KEY
}

封装数据请求与接口方法

src/libs/http.js 封装axios

import axios from 'axios';
import qs from 'qs';
import { JUHE_APP_KEY } from '../../configs/keys';

function get(options) {
  axios({
    method: 'GET',
    url: options.url,
    params: {
      ...options.params,
      key: JUHE_APP_KEY
    }
  }).then(res => {
    options.success(res.data);
  }, err => {
    options.error(err);
  });
}

function post(options) {
  axios({
    method: 'POST',
    url: options.url,
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded'
    },
    data: qs.stringify({
      ...options.data,
      key: JUHE_APP_KEY
    })
  }).then(res => {
    options.success(res.data);
  }, err => {
    options.error(err);
  });
}

export {
  get,
  post
}

services/request.js

⚪️ 接口编写

import { get, post } from '../libs/http';

function getDayData(date) {
  return new Promise((resolve, reject) => {
    post({
      url: '/api/calendar/day',
      data: { date },
      success(data) {
        resolve(data);
      },
      error(err) {
        reject(err);
      }
    })
  });
}

function getMonthData(yearMonth) {
  return new Promise((resolve, reject) => {
    post({
      url: '/api/calendar/month',
      data: { 'year-month': yearMonth },
      success(data) {
        resolve(data);
      },
      error(err) {
        reject(err);
      }
    })
  });
}

function getYearData(year) {
  return new Promise((resolve, reject) => {
    get({
      url: '/api/calendar/year',
      params: { year },
      success(data) {
        resolve(data);
      },
      error(err) {
        reject(err);
      }
    })
  });
}

export {
  getDayData,
  getMonthData,
  getYearData
}

⚪️ 封装数据获取

import {
  getDayData,
  getMonthData,
  getYearData
} from './request';

export default async (field, date) => {
  let data = null;
  switch (field) {
    case 'day':
      data = await getDayData(date);
      break;
    case 'month':
      data = await getMonthData(date);
      break;
    case 'year':
      data = await getYearData(date);
    default:
      break;
  }
  return data;
}

⚪️ Day.vue 使用

<template>
  <div class="container">
    Day Page
  </div>
</template>

<script>
import { onMounted } from 'vue';
import getData from '@/services';
export default {
  name: 'DayPage',
  setup() {
    onMounted(async () => {
      const data = await getData('day', '2022-3-16');
      console.log(data);
    });
  }
}
</script>

<style scoped>

</style>

组件编写

Header 组件

components/Header/index.vue

<template>
  <header class="header">
    <h1>
      <slot></slot>
    </h1>
  </header>
</template>
<script>
export default {
  name: 'MyHeader',
}
</script>
<style lang="scss" scoped>
.header {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 1;
  width: 100%;
  height: .44rem;
  background-color: #ed4040;
  text-align: center;
  line-height: .44rem;

  h1 {
    font-size: .18rem;
    color: #fff;
  }
}
</style>

vuex改造

.
├── actions.js
├── index.js
├── mutations.js
└── state.js

state.js

export default {
  headerTitle: '当天信息'
}

mutations.js

export default {
  setHeaderTitle(state, routeName) { // 根据路由名称设置 header-title
    switch (routeName) {
      case 'day':
        state.headerTitle = '当天信息';
        break;
      case 'month':
        state.headerTitle = '近期假期';
        break;
      case 'year':
        state.headerTitle = '当年假期';
        break;
      default:
        state.headerTitle = '当天信息';
        break;
    }
  }
}

App 中使用

<template>
  <div id="app">
    <my-header>{{ headerTitle }}</my-header>
    <router-view/>
  </div>
</template>

<script>
import { useStore } from 'vuex';
import { useRouter } from 'vue-router';
import { computed } from 'vue';
import MyHeader from '@/components/Header';
export default {
  name: 'App',
  components: {
    MyHeader
  },
  setup() {
    // use[] 的方式获取 store 和 router
    const store = useStore(),
          state = store.state,
          router = useRouter();
    router.push('/');

    return computed(() => state).value;
  }
}
</script>
<style lang="scss" scoped>
.container {
  padding: .82rem 0 .44rem 0;
  box-sizing: border-box;
}
</style>

监听 router 改变动态设置 headerTitle

<template>...</template>
<script>
export default {
  name: 'App',
  components: {
    MyHeader,
    Tab
  },
  setup() {
    // use[] 的方式获取 store 和 router
    const store = useStore(),
          state = store.state,
          router = useRouter();
    router.push('/');

    // 监听路由变化,动态设置 headerTitle
    watch(() => {
      return router.currentRoute.value.name
    }, (newVal) => {
      store.commit('setHeaderTitle', newVal);
    });

    return computed(() => state).value;
  }
}
</script>
<style lang="scss" scoped>
.container {
  padding: .82rem 0 .44rem 0;
  box-sizing: border-box;
}
</style>

Tabbar 组件

index.vue

<template>
  <div class="tab">
    <div class="tab-item"
      v-for="(item, idx) of tabData"
      :key="idx"
    >
      <tab-icon
        :iconText="item.iconText"
        :path="item.path"
      >{{ item.tabText }}</tab-icon>
    </div>
  </div>
</template>
<script>
import TabIcon from './Icon';
import tabData from '../../data/tab';

import { reactive } from 'vue';
export default {
  name: 'Tab',
  components: {
    TabIcon
  },
  setup() {
    const state = reactive({
      tabData
    })
    return {
      ...state
    }
  }
}
</script>
<style lang="scss" scoped>
.tab {
  position: fixed;
  left: 0;
  bottom: 0;
  width: 100%;
  height: .44rem;
  display: flex;
  flex-direction: row;
  border-top: 1px solid #ddd;
  background-color: #fff;

  .tab-item {
    display: flex;
    justify-content: center;
    align-items: center;
    width: 33.33%;
    height: 100%;
  }
}
</style>

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: {
      type: String,
      default: ''
    },
    path: {
      type: String,
      default: '/'
    }
  }
}
</script>
<style lang="scss" scoped>
.tab-icon {
  display: inline-block;

  .icon {
    display: flex;
    justify-content: center;
    align-items: center;
    width: 0.25rem;
    height: 0.25rem;
    border-radius: 50%;
    background-color: #ddd;
    color: #999;
    font-size: .12rem;
    transition: all .5s;
  }

  .text {
    font-size: .12rem;
    text-align: center;
    margin-top: .02rem;
    color: #999;
    transition: color .5s;
  }

  &.router-link-active {
    .icon {
      background-color: #ed4040;
      color: #fff;
    }
    .text {
      color: #ed4040;
    }
  }
}
</style>

tab.js tabbar 底部数据

import { getIconDate } from '@/libs/utils';

export default [
  {
    iconText: getIconDate('day'),
    tabText: '当天',
    path: '/'
  },
  {
    iconText: getIconDate('month'),
    tabText: '近期',
    path: '/month'
  },
  {
    iconText: getIconDate('year'),
    tabText: '当年',
    path: '/year'
  }
]
function _addZero(value) {
  return value < 10 ? ('0' + value) : value;
}
function getIconDate(type) {
  const date = new Date();
  switch (type) {
    case 'day':
      return _addZero(date.getDate().toString());
    case 'month':
      return _addZero((date.getMonth() + 1).toString());
    case 'year':
      return _addZero(date.getFullYear().toString().substring(2));
    default:
      return _addZero(date.getDay().toString());
  }
}

export {
  getIconDate
}

App.vue 使用 tabbar

<template>
  <div id="app">
    <my-header>{{ headerTitle }}</my-header>
    <router-view/>
    <tab />
  </div>
</template>

<script>
import { useStore } from 'vuex';
import { useRouter } from 'vue-router';
import { computed } from 'vue';
import MyHeader from '@/components/Header';
import Tab from '@/components/Tab';
export default {
  name: 'App',
  components: {
    MyHeader,
    Tab
  },
  setup() {
    // use[] 的方式获取 store 和 router
    const store = useStore(),
          state = store.state,
          router = useRouter();
    router.push('/');

    return computed(() => state).value;
  }
}
</script>
<style lang="scss" scoped>
.container {
  padding: .82rem 0 .44rem 0;
  box-sizing: border-box;
}
</style>

SearchInput 搜索组件

index.vue

<template>
  <div class="search-wrap">
    <input
      type="text"
      :placeholder="placeholder"
      :maxlength="maxlength"
      :value="inputValue"
      @input="searchData($event)"
    >
  </div>
</template>
<script>
import { ref } from "vue";
export default {
  name: 'SearchInput',
  props: {
    placeholder: String,
    maxlength: Number
  },
  setup() {
    const inputValue = ref('');
    const searchData = (e) => {

    }
    return {
      inputValue,
      searchData
    }
  }
}
</script>
<style lang="scss" scoped>
.search-wrap {
  position: fixed;
  top: .44rem;
  left: 0;
  z-index: 1;
  width: 100%;
  height: 0.38rem;
  padding: .03rem .1rem;
  border-bottom: 1px solid #ddd;
  box-sizing: border-box;
  background-color: #fff;

  input {
    width: 100%;
    height: 100%;
    font-size: .14rem;
    text-indent: 1rem;
    border-radius: .03rem;

    &:focus {
      border-color: #ed4040;
      box-shadow: 0 0 .02rem #ed4040;
      transition: all .3s;
    }
  }
}
</style>
<template>
  <div id="app">
    <my-header>{{ headerTitle }}</my-header>
    <search-input
      :placeholder="placeholder"
      :maxLength="maxLength"
    ></search-input>
    <router-view/>
    <tab />
  </div>
</template>

<script>
import { useStore } from 'vuex';
import { useRouter } from 'vue-router';
import { computed, watch } from 'vue';
import MyHeader from '@/components/Header';
import Tab from '@/components/Tab';
import SearchInput from '@/components/SearchInput';
export default {
  name: 'App',
  components: {
    MyHeader,
    Tab,
    SearchInput
  },
  setup() {
    // use[] 的方式获取 store 和 router
    const store = useStore(),
          state = store.state,
          router = useRouter();
    router.push('/');

    // 监听路由变化,动态设置 headerTitle
    watch(() => {
      return router.currentRoute.value.name
    }, (newVal) => {
      store.commit('setHeaderTitle', newVal);
      store.commit('setMaxLength', newVal);
      store.commit('setPlaceholder', newVal);
    });

    return computed(() => state).value;
  }
}
</script>
<style lang="scss" scoped>
.container {
  padding: .82rem 0 .44rem 0;
  box-sizing: border-box;
}
</style>

首页大图数据编写

DayPage/Card.vue

<template>
  <div class="card-wrapper">
    <div class="card">
      <div class="header">
        <span class="weekday">{{ data.weekday }}</span>
      </div>
      <h1 class="text lunar">{{ data.lunar }}</h1>
      <h2 class="text date">{{ data.date }}</h2>
    </div>
  </div>
</template>
<script>
export default {
  name: 'DayCard',
  props: {
    data: Object
  }
}
</script>
<style lang="scss" scoped>
.card-wrapper {
  padding: 0.15rem;
  box-sizing: border-box;

  .card {
    background: url('~@/assets/img/春节.jpg') no-repeat center/cover;
    border-radius: .15rem;
    overflow: hidden;
    border: 1px solid #ddd;
    color: #fff;

    .header {
      height: 0.35rem;
      padding: 0.1rem;
      box-sizing: border-box;
      font-size: .14rem;
      background-color: rgba(0, 0, 0, .3);
    }

    .text {
      text-align: center;
      margin: 0.3rem 0;
      text-shadow: .02rem .05rem .05rem #666;

      &.lunar {
        color: #f75555;
        font-size: .5rem;
        font-weight: bold;
      }

      &.date {
        font-size: .2rem;
      }
    }
  }
}
</style>

utils 工具函数编写

为了格式化聚合api数据扩充 utils 方法:

...

function formatChsDate(date, type) {
  const _arr = date.split('-'),
        [year, month, day] =_arr;

  switch (type) {
    case 'day':
      return `${year}年${month}月${day}日`;
    case 'month':
      return `${year}年${month}月`;
    case 'year':
      return `${year}年`;
    default:
      return `${year}年${month}月${day}日`;
  }
}

function mapForChsDate(data, key) {
  return data.map(item => {
    item[key] = formatChsDate(item[key]);
    return item;
  });
}

function getNowDate(field) {
  const date = new Date();

  let year = date.getFullYear(),
      month = date.getMonth() + 1,
      day = date.getDate();

  switch (field) {
    case 'day':
      return `${year}-${month}-${day}`;
    case 'month':
      return `${year}-${month}`;
    case 'year':
      return `${year}`;
    default:
      return `${year}-${month}-${day}`;
  }
}

export {
  getIconDate,
  formatChsDate,
  mapForChsDate,
  getNowDate
}

完善 vuex