创建项目
使用 create-vue 脚手架创建项目
pnpm create vue
选择步骤
✔ Project name: … patients-h5
✔ Add TypeScript? … No / `Yes`
✔ Add JSX Support? … `No` / Yes
✔ Add Vue Router for Single Page Application development? … No / `Yes`
✔ Add Pinia for state management? … No / `Yes`
✔ Add Vitest for Unit Testing? … `No` / Yes
✔ Add Cypress for both Unit and End-to-End testing? … `No` / Yes
✔ Add ESLint for code quality? … No / `Yes`
✔ Add Prettier for code formatting? … No / `Yes`
Scaffolding project in /Users/zhousg/Desktop/patient-h5-100...
Done. Now run:
cd patient-h5-100
pnpm install
pnpm lint
pnpm dev
eslint配置
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
root: true,
extends: [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-typescript',
'@vue/eslint-config-prettier/skip-formatting'
],
parserOptions: {
ecmaVersion: 'latest'
},
rules: {
'prettier/prettier': [
'warn',
{
singleQuote: true,
semi: false,
printWidth: 80,
trailingComma: 'none',
endOfLine: 'auto'
}
],
'vue/multi-word-component-names': [
'warn',
{
ignores: ['index']
}
],
'vue/no-setup-props-destructure': ['off']
}
}
创建路由
import { createRouter, createWebHistory } from 'vue-router'
// createRouter 创建路由实例,===> new VueRouter()
// history 是路由模式,hash模式,history模式
// createWebHistory() 是开启history模块 http://xxx/user
// createWebHashHistory() 是开启hash模式 http://xxx/#/user
// vite 的配置 import.meta.env.BASE_URL 是路由的基准地址,默认是 ’/‘
// https://vitejs.dev/guide/build.html#public-base-path
// 如果将来你部署的域名路径是:http://xxx/my-path/user
// vite.config.ts 添加配置 base: my-path,路由这就会加上 my-path 前缀了
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: []
})
export default router
安装vant组件库
pnpm add vant
按需引入组件样式
pnpm add unplugin-vue-components -D
在 vite.config.js 文件中配置插件:
import vue from '@vitejs/plugin-vue';
import Components from 'unplugin-vue-components/vite';
import { VantResolver } from 'unplugin-vue-components/resolvers';
export default {
plugins: [
vue(),
Components({
resolvers: [VantResolver()],
}),
],
};
移动端适配
postcss-px-to-viewport 是一款 PostCSS 插件,用于将 px 单位转化为 vw/vh 单位。
pnpm add -D postcss-px-to-viewport
配置 postcss.config.js:
// eslint-disable-next-line no-undef
module.exports = {
plugins: {
'postcss-px-to-viewport': {
// 设备宽度375计算vw的值
viewportWidth: 375,
},
},
};
css变量主题定制
styles/main.scss
// :root:可以在一个CSS文件中定义全局变量,供多个属性引用
// var:读取变量
:root {
--cp-primary: #16C2A3;
--cp-plain: #EAF8F6;
--cp-orange: #FCA21C;
--cp-text1: #121826;
--cp-text2: #3C3E42;
--cp-text3: #6F6F6F;
--cp-tag: #848484;
--cp-dark: #979797;
--cp-tip: #C3C3C5;
--cp-disable: #D9DBDE;
--cp-line: #EDEDED;
--cp-bg: #F6F7F9;
--cp-price: #EB5757;
// 覆盖vant的变量
--van-primary-color: var(--cp-primary);
// 单元格上下间距
--van-cell-vertical-padding: 14px;
// 复选框大小
--van-checkbox-size: 14px;
// 默认按钮文字大小
--van-button-normal-font-size: 16px;
}
// 全局样式
body {
font-size: 14px;
color: var(--cp-text1);
}
a {
color: var(--cp-text2);
}
h1,h2,h3,h4,h5,h6,p,ul,ol {
margin: 0;
padding: 0;
}
#nprogress .bar {
background-color: var(--cp-primary) !important;
}
// 全局覆盖van-tab样式
.van-tabs {
.van-tabs__nav {
padding: 0 0 15px 0;
}
.van-tabs__line {
width: 20px;
background-color: var(--cp-primary);
}
.van-tab {
padding: 0 15px;
}
}
在main.ts文件引入
// 全局样式
import '@/styles/main.scss'
安装pinia
// pinia-plugin-persistedstate插件可以实现pinia仓库状态持久化
pnpm add pinia pinia-plugin-persistedstate
main.ts
import persist from 'pinia-plugin-persistedstate'
const app = createApp(App)
app.use(createPinia().use(persist))
需要在defineStore的第三个参数添加persist:true
import type { User } from '@/types/user'
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useUserStore = defineStore(
'user',
() => {
// 用户信息
const user = ref<User>()
// 设置用户,登录后使用
const setUser = (u: User) => {
user.value = u
}
// 清空用户,退出后使用
const delUser = () => {
user.value = undefined
}
return { user, setUser, delUser }
},
{
persist: true
}
)
测试 App.vue
<script setup lang="ts">
import { useUserStore } from './stores/user'
const store = useUserStore()
</script>
<template>
<p>{{ store.user }}</p>
<button @click="store.setUser({ id: '1', mobile: '1', account: '1', avatar: '1', token: '1' })">
登录
</button>
<button @click="store.delUser()">退出</button>
</template>
stores统一导出
stores/index
import { createPinia } from 'pinia'
import persist from 'pinia-plugin-persistedstate'
// 创建pinia实例
const pinia = createPinia()
// 使用pinia插件
pinia.use(persist)
// 导出pinia实例,给main使用
export default pinia
import { createApp } from 'vue'
import App from './App.vue'
import pinia from './stores'
import router from './router'
import './styles/main.scss'
const app = createApp(App)
app.use(pinia)
app.use(router)
app.mount('#app')
stores/index
// 统一导出,代码简洁,入口唯一
export * from './modules/user'
App.vue
- import { useUserStore } from './stores/user'
+ import { useUserStore } from './stores'
安装axios
pnpm add axios
封装axios
import { useUserStore } from '@/stores'
import router from '@/router'
import axios, { type Method } from 'axios'
import { showFailToast } from 'vant'
type Data<T> = {
code: number
message: string
data: T
}
const baseURL = 'https://consult-api.itheima.net/'
// 1. 新axios实例,基础配置
const instance = axios.create({
baseURL,
timeout: 10000
})
// 2. 请求拦截器,携带token
instance.interceptors.request.use(
(config) => {
const store = useUserStore()
if (store.user?.token && config.headers) {
config.headers['Authorization'] = `Bearer ${store.user?.token}`
}
return config
},
(err) => Promise.reject(err)
)
// 3. 响应拦截器,剥离无效数据,401拦截
instance.interceptors.response.use(
(res) => {
// 后台约定,响应成功,但是code不是10000,是业务逻辑失败
if (res.data?.code !== 10000) {
showFailToast(res.data?.message || '业务失败')
return Promise.reject(res.data)
}
// 业务逻辑成功,返回响应数据,作为axios成功的结果
return res.data
},
(err) => {
if (err.response.status === 401) {
// 删除用户信息
const store = useUserStore()
store.delUser()
// 跳转登录,带上接口失效所在页面的地址,登录完成后回跳使用
router.push({
path: '/login',
query: { returnUrl: router.currentRoute.value.fullPath }
})
}
return Promise.reject(err)
}
)
// 这个需要替换axsio.request默认的响应成功后的结果类型
// 之前是:传 { name: string } 然后res是 res = { data: { name: string } }
// 但现在:在响应拦截器中返回了 res.data 也就是将来响应成功后的结果,和上面的类型一致吗?
// 所以要:request<数据类型,数据类型>() 这样才指定了 res.data 的类型
const request = <T>(
url: string,
method: Method = 'get',
submitData?: object
) => {
return instance.request<T, Data<T>>({
url,
method,
[method.toLowerCase() === 'get' ? 'params' : 'data']: submitData
})
}
export { baseURL, request }