闲来无事,趁假期来加强一波技能学习!不废话直接上流程上图;
超级简洁和极致的本地开发体验!!!
来个项目简介吧;
<!--
* @Description: Vue 3 + Typescript + Vite2.0 +vant3 + vue-i18n@next国际化 搭建移动端项目简介
* @Version: 2.0
* @Autor: lhl
* @Date: 2021-04-03 23:18:54
* @LastEditors: lhl
* @LastEditTime: 2021-04-11 22:39:02
-->
# Vue 3 + Typescript + Vite
#### 项目初始化 yarn or cnpm or npm 安装 【本项目为了速度一律 cnpm】
cnpm init @vitejs/app or yarn create @vitejs/app 或者快捷命令 cnpm init @vitejs/app my-vue-app --template vue-ts Node.js: - 版本最好大于 12.0.0 yarn > npm > cnpm: - 包管理工具
### 安装依赖
cnpm i
### 安装路由
cnpm i vue-router@4 -S 【--save】
### 安装 vuex
cnpm i vuex@next -S 【--save】
### 安装国际化
cnpm i vue-i18n@next -S cnpm i js-cookie -S cnpm i @types/js-cookie -D
console.log(import.meta.env.VITE_APP_BASE_URL, 'VITE_APP_BASE_URL')
console.log(process.env.NODE_ENV)
### 启动项目
cnpm run dev
### 代码规范 vscode 需要安装的相关插件 Eslint Prettier Prettier Eslint 三个即可
cnpm i eslint -D 根目录下创建 .eslintrc.js 文件 eslint 官方配置文档:https://eslint.org/docs/user-guide/configuring/configuration-files#using-configuration-files
node 环境的类型检查 cnpm i @types/node -D
cnpm i prettier -D 根目录下创建 .prettierrc.js 文件 prettier 官方配置文档:https://prettier.io/docs/en/
安装相关依赖
cnpm i @typescript-eslint/eslint-plugin -D
cnpm i @typescript-eslint/parser -D
cnpm i eslint-config-prettier -D
cnpm i eslint-plugin-prettier -D
cnpm i eslint-plugin-vue -D
cnpm i eslint-define-config -D
### git 代码提交
cnpm i husky lint-staged -D 【git 代码提交规范】 package.json 文件中配置下
cnpm i eslint-plugin-vue@latest @typescript-eslint/eslint-plugin@latest @typescript-eslint/parser@latest babel-eslint -D cnpm i eslint-config-standard eslint-friendly-formatter eslint-plugin-import eslint-plugin-standard eslint-plugin-promise -D
### 移动端适配问题
先说特别的 iPhonex 的适配 iOS11 新增特性,Webkit 的一个 CSS 函数,用于设定安全区域与边界的距离,有四个预定义的变量: safe-area-inset-left:安全区域距离左边边界距离 safe-area-inset-right:安全区域距离右边边界距离 safe-area-inset-top:安全区域距离顶部边界距离 safe-area-inset-bottom:安全区域距离底部边界距离只有设置了 viewport-fit=cover,才能使用 constant 函数
<meta name=“viewport” content=“width=device-width, viewport-fit=cover”>
body {
padding-bottom: constant(safe-area-inset-bottom);
}
fixed 元素的适配
{
padding-bottom: constant(safe-area-inset-bottom);
}
或者直接设置 body{ padding: env(safe-area-inset-left,20px) env(safe-area-inset-right,20px) env(safe-area-inset-top,20px) env(safe-area-inset-bottom,20px) }
再或者媒体查询 /_兼容 iphoneX_/ /_判断是否是 iphoneX,使用@media 媒体查询尺寸_/ @media only screen and (device- 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) { body { top: constant(safe-area-inset-top); bottom: constant(safe-area-inset-bottom); left: constant(safe-area-inset-left); right: constant(safe-area-inset-right); } } ios11 webview 状态栏问题 【设置了固定定位页面滚动过程中两边留白】如果是纯色背景的话,可以通过给 body 设置 background 来实现填充两边的留白
scss 函数 $designWidth: 750;
@function px2vm($size){ @return #{100*$size / $designWidth}vw } 调用 px2vm(50)
### vm vw 适配方案
cnpm i postcss-px-to-viewport -D
### 第三方 UI 库 vant
cnpm i vant@next -S 官方文档:https://vant-contrib.gitee.io/vant/v3/#/zh-CN
"browserslist": [
"defaults", // 默认 "last 2 versions",
"last 2 versions", // 兼容主流浏览器的最近两个版本 "> 1%",
"> 1%", // 兼容主流浏览器的最近两个版本 "> 1%",
"iOS 7", // 使用的浏览器需要在市场上的份额大于 1 "iOS 7",
"last 3 iOS versions" // 兼容 ios 的最新 3 个版本
]
组件样式按需加载配置
cnpm i vite-plugin-style-import -D
import styleImport from 'vite-plugin-style-import'
css:{ preprocessorOptions:{ less:{ modifyVars:{}, javascriptEnabled: true } } }, plugins:[ styleImport({ libs:[ { libraryName: 'ant-design-vue', esModule: true, resolveStyle: name => `ant-design-vue/es/${name}/style/index` } ] }) ]
### 自动添加 css 前缀插件
cnpm i autoprefixer -D
### SASS 预处理器
cnpm i node-sass sass-loader sass -D
### 生产环境生成 .gz 文件
cnpm i vite-plugin-compression -D 参考文档:https://github.com/anncwb/vite-plugin-compression
### package.json 文件配置打包命令
环境变量 VITE_ 开头
"build:dev": "vue-tsc --noEmit && vite build --mode development",
"build:test": "vue-tsc --noEmit && vite build --mode test",
"build:prod": "vue-tsc --noEmit && vite build --mode production"
### PWA 配置
cnpm i vite-plugin-pwa -D
import { VitePWA } from 'vite-plugin-pwa'
参考文档:https://github.com/antfu/vite-plugin-pwa
plugins:[
VitePWA({
manifest: {},
workbox: { skipWaiting: true, clientsClaim: true }
})
]
再来个vite.config.ts常用配置
/*
* @Description: vite.config.ts vite2.0配置
* @Version: 2.0
* @Autor: lhl
* @Date: 2021-04-03 23:18:54
* @LastEditors: lhl
* @LastEditTime: 2021-04-11 20:43:27
*/
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import viteCompression from 'vite-plugin-compression'
import path from 'path'
const resolve = (dir: string) => path.join(__dirname, dir)
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
// 生产环境生成 .gz 文件
viteCompression({
verbose: true,
disable: false,
threshold: 10240,
algorithm: 'gzip',
ext: '.gz'
})
],
base: './', // 打包路径
resolve: {
// 设置别名
alias: {
'@': resolve('src'),
// 解决vue-i18n警告You are running the esm-bundler build of vue-i18n. It is recommended to configure your bundler to explicitly replace feature flag globals with boolean literals to get proper tree-shaking in the final bundle.
'vue-i18n': 'vue-i18n/dist/vue-i18n.cjs.js'
}
},
server: {
host: '0.0.0.0',
https: false,
port: 4000, // 启动端口
open: true,
// proxy: {
// // 选项写法
// '/api': 'http://xxxx'// 代理网址
// },
cors: true
},
build: {
// 生产环境移除 console
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
}
}
})
接着main.ts
/*
* @Description: 入口文件
* @Version: 2.0
* @Autor: lhl
* @Date: 2021-04-03 23:18:54
* @LastEditors: lhl
* @LastEditTime: 2021-04-11 22:41:30
*/
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import i18n from './locales/index'
import Vant from 'vant'
import 'vant/lib/index.css' // 全局引入样式
// vite版本不需要配置组件的按需加载,因为Vant 3.0 内部所有模块都是基于 ESM 编写的,天然具备按需引入的能力
console.log(import.meta.env.VITE_APP_BASE_URL, 'VITE_APP_BASE_URL')
console.log(process.env.NODE_ENV)
createApp(App).use(i18n).use(Vant).use(router).use(store).mount('#app')
接着是国际化的配置
/*
* @Description: vscode自带注释
* @Version: 2.0
* @Autor: lhl
* @Date: 2021-04-11 11:29:47
* @LastEditors: lhl
* @LastEditTime: 2021-04-11 12:11:08
*/
import { createI18n } from 'vue-i18n'
import { getLanguage } from '../utils/cookies'
// Vant built-in lang
import { Locale } from 'vant'
import enUS from 'vant/es/locale/lang/en-US'
import zhCN from 'vant/es/locale/lang/zh-CN'
import zhTW from 'vant/es/locale/lang/zh-TW'
import jaJP from 'vant/es/locale/lang/ja-JP'
// User defined lang
import enUsLocale from './en_US/index'
import zhCnLocale from './zh_CN/index'
import zhTwLocale from './zh_TW/index'
import jaJpLocale from './ja_JP/index'
const messages: any = {
'zh-CN': {
...zhCN,
...zhCnLocale
},
'zh-TW': {
...zhTW,
...zhTwLocale
},
'en-US': {
...enUS,
...enUsLocale
},
'ja-JP': {
...jaJP,
...jaJpLocale
}
}
export const getLocale = () => {
const cookieLanguage = getLanguage()
if (cookieLanguage) {
document.documentElement.lang = cookieLanguage
return cookieLanguage
}
const language = navigator.language.toLowerCase()
const locales = Object.keys(messages)
for (const locale of locales) {
if (language.indexOf(locale) > -1) {
document.documentElement.lang = locale
return locale
}
}
// Default language is english
return 'en-US'
}
const CURRENT_LANG = getLocale()
// first entry
Locale.use(CURRENT_LANG, messages[CURRENT_LANG])
const i18n = createI18n({
locale: CURRENT_LANG,
messages
})
export default i18n
/*
* @Description: vscode自带注释
* @Version: 2.0
* @Autor: lhl
* @Date: 2021-04-11 11:40:24
* @LastEditors: lhl
* @LastEditTime: 2021-04-11 11:43:09
*/
import Cookies from 'js-cookie'
// App
const languageKey = 'language'
export const getLanguage = () => Cookies.get(languageKey)
export const setLanguage = (language: string) => Cookies.set(languageKey, language)
新建四个ts文件存放四种语言;这里只放一种作为演示
/*
* @Description: 中文
* @Version: 2.0
* @Autor: lhl
* @Date: 2021-04-11 11:52:33
* @LastEditors: lhl
* @LastEditTime: 2021-04-11 14:31:08
*/
const zh_CN = {
appHeader: {
title: 'Vue App',
selectLanguage: '语言选择'
},
goBack: {
text: '返回'
},
tabBarItem: {
home: '首页',
product: '产品页',
vue3Study: 'vue3Study',
myCenter: '个人中心'
},
langSelect: {
pickerTitle: '当前语言'
}
}
export default zh_CN
兴趣来了还封装了一个语言选择组件vant3 popup+piker组件合成
<!--
* @Description: vscode自带注释
* @Version: 2.0
* @Autor: lhl
* @Date: 2021-04-10 21:56:40
* @LastEditors: lhl
* @LastEditTime: 2021-04-11 14:08:24
-->
<template>
<van-popup v-model:show="showPicker" v-bind="popupConfig">
<van-picker
show-toolbar
swipe-duration="300"
:title="$t('langSelect.pickerTitle')"
:columns="langs"
:default-index="defaultIndex"
@confirm="onConfirm"
@cancel="onClose"
/>
</van-popup>
</template>
<script>
import { defineComponent, toRefs, reactive, onMounted, getCurrentInstance, computed } from 'vue'
import { useStore } from 'vuex'
import { Locale } from 'vant'
import { setLanguage } from '@/utils/cookies'
export default defineComponent({
name: 'LangSelect',
props: {
popupConfig: {
type: Object,
default: () => ({
overlay: true,
position: 'bottom',
duration: 0.3,
closeOnPopstate: true,
transitionAppear: true,
safeAreaInsetBottom: true
})
}
},
setup() {
const state = reactive({
langs: [
{
text: '中文(简体)',
value: 'zh-CN'
},
{
text: '中文(繁体)',
value: 'zh-TW'
},
{
text: 'English',
value: 'en-US'
},
{
text: '日本語',
value: 'ja-JP'
}
]
})
const store = useStore()
const { proxy } = getCurrentInstance()
console.log(store, 'store', getCurrentInstance(), 'getCurrentInstance')
const computedData = {
showPicker: computed(() => store.getters.langPicker),
defaultIndex: computed(
() => state.langs.findIndex((item) => item.value === proxy.$i18n.locale) || 0
)
}
const methods = {
onConfirm({ value }) {
// Vant basic
Locale.use(value, proxy.$i18n.messages[value])
// Business component
proxy.$i18n.locale = value
// Cookie
setLanguage(value)
store.dispatch('changeShowPicker', false)
},
onClose() {
store.dispatch('changeShowPicker', false)
}
}
return {
...toRefs(state),
...methods,
...computedData
}
}
})
</script>
<style lang="scss" scoped></style>
最后给个文件目录和效果图吧
再底部tabbar组件
<!--
* @Descripttion: 底部tabbar
* @version:
* @Author: lhl
* @Date: 2021-04-06 16:29:07
* @LastEditors: lhl
* @LastEditTime: 2021-04-14 10:47:34
-->
<template>
<div>
<van-tabbar v-model="active">
<template v-for="(item, index) in tabbars" :key="index">
<van-tabbar-item :icon="item.icon" :to="item.path">{{ $t(item.title) }}</van-tabbar-item>
</template>
</van-tabbar>
</div>
</template>
<script lang="ts">
// 注明v-for中的国际化需要自动注意 直接数组上 t(''message.menuItme.home') 不会更新
import { defineComponent, onMounted, toRefs, reactive, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { Toast } from 'vant'
import { useRoute } from 'vue-router'
export default defineComponent({
name: 'Tabbar',
setup() {
const { t } = useI18n()
const active = ref(0)
const state = reactive({
// active: 0,
//函数接收一个普通对象,返回一个响应式的数据对象
tabbars: [
{
path: '/home',
title: 'message.menuItme.home',
icon: 'home-o'
},
{
path: '/product',
title: 'message.menuItme.product',
icon: 'coupon-o'
},
{
path: '/vue3Grammer',
title: 'message.menuItme.study',
icon: 'hot-o'
},
{
path: '/my',
title: 'message.menuItme.my',
icon: 'friends-o'
}
]
})
const route = useRoute()
const methods = {
initActive() {
state.tabbars.map((item, index) => {
if (item.path === route.path) {
active.value = index
}
})
}
}
// watch的使用
watch(
() => route.path,
(value) => {
console.log('value改变', value)
if (value) {
let vIndex = state.tabbars.findIndex((item) => {
return item.path == value
})
console.log(vIndex, 'vIndex')
if (vIndex > -1) {
active.value = vIndex
// Toast.success(t('message.menuItme.study'))
}
}
}
)
onMounted(() => {
methods.initActive()
})
return {
active,
...methods,
...toRefs(state)
}
}
})
</script>
<style lang="less"></style>
再贴一个vuex配置
/*
* @Descripttion: vuex配置入口
*/
import { createStore } from 'vuex'
const store = createStore({
state: {
langPicker: false,
loading: false
},
mutations: {
// 语言选择框
handleShowPicker(state) {
state.langPicker = !state.langPicker
},
// 显示loading
showLoading(state) {
state.loading = true
},
// 隐藏loading
hideLoading(state) {
state.loading = false
}
},
getters: {
langPicker: (state) => state.langPicker
},
actions: {
changeShowPicker(context, value) {
context.commit('handleShowPicker', value)
}
},
modules: {}
})
export default store
再贴一个路由配置
/*
* @Descripttion: 路由配置文件 参考文档:https://next.router.vuejs.org/zh/introduction.html
*/
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
const routes: Array<RouteRecordRaw> = [
{
path: '/',
redirect: '/home',
name: '/',
component: () => import('@/components/layout/index.vue'),
children: [
{
path: '/home',
// name: 'home',
meta: {
title: '首页',
i18Title: 'message.menuItme.home'
},
component: () => import('@/pages/home/index.vue')
},
{
path: '/carDetail',
name: 'carDetail',
meta: {
title: '购物车详情',
i18Title: 'message.menuItme.carDetail'
},
component: () => import('@/pages/home/carDetail.vue')
},
{
path: '/product',
name: 'product',
meta: {
title: '产品列表',
i18Title: 'message.menuItme.product'
},
component: () => import('@/pages/product/index.vue')
},
{
path: '/vue3Grammer',
name: 'vue3Grammer',
meta: {
title: 'vue3语法',
i18Title: 'message.menuItme.study'
},
component: () => import('@/pages/vue3Grammer/index.vue')
},
{
path: '/my',
name: 'my',
meta: {
title: '个人中心',
i18Title: 'message.menuItme.my'
},
component: () => import('@/pages/my/index.vue')
}
]
},
{
path: '/404',
name: '404',
component: () => import('@/pages/notFound/index.vue')
},
{
path: '/:pathMatch(.*)', // 和以前配置有所不一样 or /:catchAll(.*)
redirect: '/404'
}
]
const router = createRouter({
history: createWebHashHistory(),
routes
})
console.log(router, 'router')
// 路由前置钩子
router.beforeEach((to, from, next) => {
const title = to.meta && (to.meta.title as string)
if (title) {
document.title = title
}
next()
})
router.afterEach((to, from) => {
console.log(to, 'to', from, 'from')
})
// 路由配置上定义 路由独享的守卫
// beforeEnter: (to, from) => {
// // reject the navigation
// return false
// },
// 导航守卫
// onBeforeRouteLeave, onBeforeRouteUpdate
export default router
再贴一个vue3基础用法
<!--
* @Descripttion: vue3语法 --tsx写法(就是react 的jsx风格) ts写法 .vue写法 options API Composition API写法都可以
-->
<template>
<div class="vue3-grammer">
<div class="num-box">{{ num }}----{{ newNum }}----{{ refData }}</div>
<div class="change-num-btn">
<van-button size="large" type="primary" @click="handleChange">改变数据</van-button>
</div>
<div class="from-box">
<van-form @submit="onSubmit">
<van-field v-model="username" name="username" label="用户名" placeholder="用户名" :rules="[{ required: true, message: '请填写用户名' }]" />
<van-field v-model="password" type="password" name="password" label="密码" placeholder="密码" :rules="[{ required: true, message: '请填写密码' }]" />
<child @reciveChildData="reciveFromChildData" />
<div style="margin: 16px">
<van-button round block type="primary" native-type="submit"> 提交表单 </van-button>
</div>
</van-form>
</div>
<!-- vant 组件使用 -->
<van-button size="large" type="success" @click="goNextPage">路由跳转</van-button>
</div>
</template>
<script lang="ts">
// defineComponent最重要的是:在TypeScript下,给予了组件 正确的参数类型推断
import { defineComponent, toRefs, reactive, getCurrentInstance, watch, computed, ref } from 'vue'
// beforeCreate created -->用 setup 代替
import { onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import child from './child.vue'
export default defineComponent({
name: 'vue3Grammer',
components: { child },
setup(props, context) {
console.log(props, 'props', context, 'context')
//这里的ctx 类似于vue2的this
// 会报错 且开发环境可以生产环境会报错
// const { ctx: any } = getCurrentInstance()
// console.log(ctx, 'ctx')
// 获取组件实例 用于高阶用法或库的开发
// internalInstance.appContext.config.globalProperties // 访问 globalProperties
const internalInstance = getCurrentInstance()
console.log(internalInstance, 'internalInstance') // 访问 globalProperties
const refData = ref(12) //ref包裹 变为响应式对象
// 个人觉着还是这样写舒服一点 类似于vue2中的data
const state = reactive({
//函数接收一个普通对象,返回一个响应式的数据对象
num: 0,
username: '',
password: ''
})
//计算属性 个人喜欢写在对象中 因为看得更清晰一下 防止计算属性方法等混在一起不好区分
const computedData = {
// 计算属性写法 别忘记引入 computed
newNum: computed(() => state.num * 2)
}
const router = useRouter()
console.log(router, 'this.$router')
const route = useRoute()
console.log(route, 'this.$route', route.meta)
const methods = {
// 改变数据
handleChange: () => {
state.num++
// ref包裹的数据 必须用.value获取
refData.value++
},
// 提交表单
onSubmit: (values: object) => {
console.log('submit', values)
},
// 跳转
goNextPage() {
//路由跳转
router.push({
name: '/'
})
},
// 父组件接收子组件的值
reciveFromChildData(val: any) {
console.log(val, 'val-来自子组件的值')
state.username = val.username
state.password = val.password
}
//网络请求
// main.js传入的封装axios
// async getUser() {
// try {
// let { data } = await userApi()
// console.log(data)
// } catch (error) {
// console.log(error)
// }
// }
}
onBeforeMount(() => {
console.log('生命周期---等同于vue2的 beforeMount')
})
onMounted(() => {
// methods.getUser()
console.log('生命周期---等同于vue2的 mounted')
})
onBeforeUpdate(() => {
console.log('生命周期---等同于vue2的 beforeUpdate')
})
onUpdated(() => {
console.log('生命周期---等同于vue2的 updated')
})
onBeforeUnmount(() => {
console.log('生命周期---等同于vue2的 beforeUnmount')
})
onUnmounted(() => {
console.log('生命周期---等同于vue2的 unmounted ')
})
// watch的使用
watch(
() => state.num,
(value) => {
console.log('num改变', value)
}
)
return {
...toRefs(state), // 将响应式的对象变为普通对象 使用时不需要state.num 直接num即可使用
...methods, // 解构赋值
...computedData,
refData
}
}
})
</script>
<style lang="less" scoped>
.num-box {
background: #000;
color: #fff;
font-size: 16px;
text-align: center;
padding: 10px 0;
}
.change-num-btn {
margin: 8px 0;
}
</style>
【子组件】
<!--
* @Descripttion: 子组件---vant3表单常用组件组合
-->
<template>
<div class="child">
<van-divider :style="{ color: '#1989fa', borderColor: '#1989fa', padding: '0 16px' }"> 子组件表单 </van-divider>
<!-- 开关 -->
<van-field name="switch" label="开关">
<template #input>
<van-switch v-model="checked" size="20" />
</template>
</van-field>
<!-- 复选框组 -->
<van-field name="checkboxGroup" label="学习框架">
<template #input>
<van-checkbox-group v-model="groupChecked" direction="horizontal">
<van-checkbox name="1" shape="square">react</van-checkbox>
<van-checkbox name="2" shape="square">vue</van-checkbox>
</van-checkbox-group>
</template>
</van-field>
<!-- 单选框 -->
<van-field name="radio" label="多端">
<template #input>
<van-radio-group v-model="checkedradio1" direction="horizontal">
<van-radio name="1">taro</van-radio>
<van-radio name="2">uni-app</van-radio>
</van-radio-group>
</template>
</van-field>
<van-field name="radio" label="跨平台">
<template #input>
<van-radio-group v-model="checkedradio2" direction="horizontal">
<van-radio name="3">Flutter</van-radio>
<van-radio name="4">React native</van-radio>
</van-radio-group>
</template>
</van-field>
<!-- 步进器 -->
<van-field name="stepper" label="步进器">
<template #input>
<van-stepper v-model="count" />
</template>
</van-field>
<!-- 评分 -->
<van-field name="rate" label="评分">
<template #input>
<van-rate v-model="rateVal" />
</template>
</van-field>
<!-- 滑块 -->
<van-field name="slider" label="滑块">
<template #input>
<van-slider v-model="sliderVal" />
</template>
</van-field>
<!-- 文件上传 -->
<van-field name="uploader" label="文件上传">
<template #input>
<van-uploader v-model="uploadVal" />
</template>
</van-field>
<!-- Picker 组件 -->
<van-field v-model="pickerVal" readonly clickable name="picker" label="选择器" placeholder="点击选择城市" @click="showPicker = true" />
<van-popup v-model:show="showPicker" position="bottom">
<van-picker :columns="columns" @confirm="onConfirm" @cancel="showPicker = false" />
</van-popup>
<!-- DatetimePicker 组件 -->
<van-field v-model="datetimePickerVal" readonly clickable name="datetimePicker" label="时间选择" placeholder="点击选择时间" @click="showDatetimePicker = true" />
<van-popup v-model:show="showDatetimePicker" position="bottom">
<van-datetime-picker type="time" @confirm="onDatetimePickerConfirm" @cancel="showDatetimePicker = false" />
</van-popup>
<!-- 省市区选择器 Area 组件 -->
<van-field v-model="areaVal" readonly clickable name="area" label="地区选择" placeholder="点击选择省市区" @click="showArea = true" />
<van-popup v-model:show="showArea" position="bottom">
<van-area :area-list="areaList" @confirm="onAreaConfirm" @cancel="showArea = false" />
</van-popup>
<!-- 日历 Calendar 组件 -->
<van-field v-model="calendarVal" readonly clickable name="calendar" label="日历" placeholder="点击选择日期" @click="showCalendar = true" />
<van-calendar v-model:show="showCalendar" @confirm="onCalendarConfirm" />
</div>
</template>
<script lang="ts">
//用到地区组件需要加载数据或者接口数据去拿
// yarn add @vant/area-data cnpm i @vant/area-data -S
import { defineComponent, reactive, toRefs } from 'vue'
import { areaList } from '@vant/area-data'
export default defineComponent({
name: 'child',
setup(props, context) {
console.log(props, context)
const state = reactive({
username: '111',
password: '222',
checked: false,
groupChecked: [],
checkedradio1: '1',
checkedradio2: '3',
count: 1, // 最小就是1了
rateVal: 1,
sliderVal: 10,
uploadVal: [],
pickerVal: '',
showPicker: false,
columns: ['北京', '上海', '广州', '深圳'],
datetimePickerVal: '',
showDatetimePicker: false,
areaVal: '',
showArea: false,
areaList, // 数据格式见 Area 组件文档
calendarVal: '',
showCalendar: false
})
// 自定义事件
context.emit('reciveChildData', state)
const methods = {
onConfirm: (value: any) => {
console.log(value, 'Picker组件')
state.pickerVal = value
state.showPicker = false
},
onDatetimePickerConfirm: (value: any) => {
console.log(value, '时间')
state.datetimePickerVal = value
state.showDatetimePicker = false
},
onAreaConfirm: (value: any) => {
console.log(value, '地区')
state.showArea = false
state.areaVal = value
.filter((item: any) => !!item)
.map((item: any) => item.name)
.join('/')
},
onCalendarConfirm: (date: any) => {
console.log(date, '日期')
state.calendarVal = `${date.getMonth() + 1}/${date.getDate()}`
state.showCalendar = false
}
}
return {
...toRefs(state),
...methods
}
}
})
</script>
<style lang="less" scoped></style>
再贴一个axios封装
/*
* @Descripttion: api统一管理入口
*/
import * as testApi from './testApi'
export default {
testApi
}
/*
* @Descripttion: 单独api单独配置 RESTful 风格 api
* RESTful解释 参考文档:http://www.ruanyifeng.com/blog/2011/09/restful.html
*/
import request from './request'
/**
* 封装get请求
* @param {string} url 请求连接
* @param {Object} params 请求参数
* @param {Object} header 请求需要设置的header头
*/
export const getTest = (params: object, header: {}) => {
return request({
url: `/posts`,
method: 'get',
params: params,
headers: header
})
}
/**
* 封装post请求
* @param {string} url 请求连接
* @param {Object} data 请求参数
* @param {Object} header 请求的header头
*/
export const postTest = (data: object, header: {}) => {
return request({
url: `/posts`,
method: 'post',
data: data,
headers: header
})
}
/**
* 封装put请求
* @param {string} url 请求连接
* @param {Object} data 请求参数
* @param {Object} header 请求设置的header头
*/
export const putTest = (data: object, header: {}) => {
return request({
url: `/posts/1`,
method: 'put',
data: data,
headers: header
})
}
/**
* 封装delete请求
* 如果服务端将参数作为java对象来封装接受 (data入参)
* 如果服务端将参数作为url参数来接受,则请求的url为:www.xxx/url?a=1&b=2形式 (params入参)
* @param {string} url 请求连接
* @param {Object} params 请求参数
* @param {Object} header 请求设置的header头
*/
export const deleteTest = (params: object, header: {}) => {
return request({
url: `/posts/1`,
method: 'delete',
data: params,
headers: header
})
}
/*
* @Descripttion: aioxs二次封装
*/
import axios, { AxiosRequestConfig, AxiosResponse, Method } from 'axios'
import store from '../store'
import { Toast } from 'vant'
// 定义接口
interface PendingType {
url?: string
method?: Method
params: any
data: any
cancel: Function
}
// 取消重复请求
const repeatRequstList: Array<PendingType> = []
// 使用 cancel token 取消请求
const CancelToken = axios.CancelToken
console.log(import.meta.env.VITE_APP_BASE_URL, 'axios')
const instance = axios.create({
// baseURL: <string>import.meta.env.VITE_APP_BASE_URL, // 给类型 不然报错
baseURL: import.meta.env.VITE_APP_BASE_URL as string, // 断言 不然报错
// 指定请求超时的毫秒数(0 表示无超时时间)
timeout: 10000,
// 表示跨域请求时是否需要使用凭证
withCredentials: true
})
// let loadingInstance: ElLoadingComponent
// 移除重复请求
const removePending = (config: AxiosRequestConfig) => {
for (const key in repeatRequstList) {
console.log(key, 'key')
const item: number = +key
const list: PendingType = repeatRequstList[key]
// 当前请求在数组中存在时执行函数体
if (list.url === config.url && list.method === config.method && JSON.stringify(list.params) === JSON.stringify(config.params) && JSON.stringify(list.data) === JSON.stringify(config.data)) {
// 执行取消操作
list.cancel('操作太频繁,请稍后再试')
// 从数组中移除记录
repeatRequstList.splice(item, 1)
}
}
}
// 添加请求拦截器
instance.interceptors.request.use(
(config) => {
store.commit('showLoading')
removePending(config)
config.cancelToken = new CancelToken((c) => {
repeatRequstList.push({ url: config.url, method: config.method, params: config.params, data: config.data, cancel: c })
})
return config
},
(error: any) => {
store.commit('hideLoading')
return Promise.reject(error)
}
)
// 添加响应拦截器
instance.interceptors.response.use(
(response: AxiosResponse) => {
store.commit('hideLoading')
removePending(response.config)
return response.data
},
(error: any) => {
store.commit('hideLoading')
const { response } = error
// 根据返回的 http 状态码做不同的处理 ?.语法需要最新的谷歌浏览器才支持目前
// switch (response?.status) {
// case 401:
// // token失效
// break
// case 403:
// // 没有权限
// break
// case 404:
// // 请求的资源不存在
// break
// case 500:
// // 服务端错误
// Toast('提示内容')
// break
// default:
// break
// }
// 超时重新请求
const config = error.config
// 全局的请求次数,请求的间隙
const [RETRY_COUNT, RETRY_DELAY] = [3, 1000]
if (config && RETRY_COUNT) {
// 设置用于跟踪重试计数的变量
config.__retryCount = config.__retryCount || 0
// 检查是否已经把重试的总数用完
if (config.__retryCount >= RETRY_COUNT) {
return Promise.reject(response || { message: error.message })
}
// 增加重试计数
config.__retryCount++
// 创造新的Promise来处理指数后退
// 在typescript中定义promise返回类型 首先要在tsconfig.json中配置ES2015.promise的lib 不然ts无法支持promise
const backClose = new Promise((resolve) => {
setTimeout(() => {
// 写上 null or undefined 不然ts下面会报错
resolve(null)
}, RETRY_DELAY || 1)
})
// instance重试请求的Promise
return backClose.then(() => {
return instance(config)
})
}
// eslint-disable-next-line
return Promise.reject(response || { message: error.message })
}
)
export default instance
/*
* @Descripttion:
* @version:
* @Author: lhl
* @Date: 2021-04-02 15:03:55
* @LastEditors: lhl
* @LastEditTime: 2021-04-14 18:13:04
*/
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import Vant from 'vant'
import Vconsole from 'vconsole'
import 'vant/lib/index.css'
import i18n from './language/index'
import API from './api/index'
import 'normalize.css/normalize.css'
import './assets/style/index.less'
console.log(import.meta.env.VITE_APP_BASE_URL, '环境变量')
const isProd = process.env.NODE_ENV === 'production'
if (!isProd) {
new Vconsole()
}
const app = createApp(App)
app.config.globalProperties.API = API
console.log(app.config.globalProperties.API, 'app.config.globalProperties.API')
app.use(i18n).use(router).use(store).use(Vant).mount('#app')
再贴个vscode的个人配置
{
"workbench.colorTheme": "One Dark Pro",
"fileheader.customMade": {
"Description": "vscode自带注释",
"Version": "2.0",
"Autor": "lhl",
"Date": "Do not edit",
"LastEditors": "lhl",
"LastEditTime": "Do not edit"
},
"fileheader.cursorMode": {
"description":"",
"param": "",
"return": "",
"author":"lhl"
},
"[javascript]": {
"editor.defaultFormatter": "HookyQR.beautify"
},
// "[jsonc]": {
// "editor.defaultFormatter": "HookyQR.beautify"
// },
"javascript.updateImportsOnFileMove.enabled": "always",
// "emmet.excludeLanguages": [
// "markdown"
// ],
"emmet.includeLanguages": {
"javascript": "javascriptreact"
},
"emmet.triggerExpansionOnTab": true,
"eslint.codeAction.showDocumentation": {
"enable": true
},
//===========================================
//============= Editor ======================
//===========================================
"explorer.openEditors.visible": 0,
"editor.tabSize": 2,
"editor.renderControlCharacters": true,
"editor.minimap.renderCharacters": false,
"editor.minimap.maxColumn": 300,
"editor.minimap.showSlider": "always",
"editor.cursorBlinking": "phase",
"editor.cursorSmoothCaretAnimation": true,
"editor.detectIndentation": false,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"diffEditor.ignoreTrimWhitespace": false,
"javascript.format.insertSpaceBeforeFunctionParenthesis": true,
"editor.suggestSelection": "first",
"editor.trimAutoWhitespace": true,
"editor.quickSuggestions": {
"other": true,
"comments": true,
"strings": true
},
//===========================================
//============= Other =======================
//===========================================
"breadcrumbs.enabled": true,
"open-in-browser.default": "chrome",
//===========================================
//============= emmet =======================
//===========================================
"emmet.showAbbreviationSuggestions": true,
"emmet.showExpandedAbbreviation": "always",
"emmet.syntaxProfiles": {
"vue-html": "html",
"vue": "html",
"xml": {
"attr_quotes": "single"
}
},
//===========================================
//============= files =======================
//===========================================
"files.trimTrailingWhitespace": true,
"files.insertFinalNewline": true,
"files.trimFinalNewlines": true,
"files.eol": "
",
"search.exclude": {
"**/node_modules": true,
"**/*.log": true,
"**/*.log*": true,
"**/bower_components": true,
"**/dist": true,
"**/elehukouben": true,
"**/.git": true,
"**/.gitignore": true,
"**/.svn": true,
"**/.DS_Store": true,
"**/.idea": true,
"**/.vscode": false,
"**/yarn.lock": true,
"**/tmp": true,
"out": true,
"dist": true,
"node_modules": true,
"CHANGELOG.md": true,
"examples": true,
"res": true,
"screenshots": true
},
"files.exclude": {
"**/bower_components": true,
"**/.idea": true,
"**/tmp": true,
"**/.git": true,
"**/.svn": true,
"**/.hg": true,
"**/CVS": true,
"**/.DS_Store": true
},
"files.watcherExclude": {
"**/.git/objects/**": true,
"**/.git/subtree-cache/**": true,
"**/.vscode/**": true,
"**/node_modules/**": true,
"**/tmp/**": true,
"**/bower_components/**": true,
"**/dist/**": true,
"**/yarn.lock": true
},
// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
// ===========================================
// ================ Eslint ===================
// ===========================================
"eslint.alwaysShowStatus": true,
"eslint.options": {
"plugins": ["html", "vue", "javascript", "jsx", "typescript"],
"extensions": [".js", ".jsx", ".ts", ".tsx", ".vue"]
},
"eslint.validate": [
"javascript",
"typescript",
"reacttypescript",
"reactjavascript",
"html",
"vue"
],
// ===========================================
// ================ Vetur ====================
// ===========================================
"vetur.experimental.templateInterpolationService": true,
"vetur.format.options.tabSize": 2,
"vetur.format.defaultFormatter.html": "js-beautify-html",
"vetur.format.defaultFormatter.scss": "prettier",
"vetur.format.defaultFormatter.css": "prettier",
"vetur.format.defaultFormatter.ts": "prettier-tslint",
"vetur.format.defaultFormatter.js": "prettier",
"vetur.languageFeatures.codeActions": false,
"vetur.format.defaultFormatterOptions": {
"js-beautify-html": {
"wrap_attributes": "force-expand-multiline"
},
"prettier": {
"eslintIntegration": true,
"arrowParens": "always",
"semi": false,
"singleQuote": true
}
},
"terminal.integrated.rendererType": "dom",
"telemetry.enableCrashReporter": false,
"telemetry.enableTelemetry": false,
"workbench.settings.enableNaturalLanguageSearch": false,
"path-intellisense.mappings": {
"/@/": "${workspaceRoot}/src"
},
"prettier.requireConfig": true,
"typescript.updateImportsOnFileMove.enabled": "always",
"workbench.sideBar.location": "left",
"[javascriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[less]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[scss]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
// "[markdown]": {
// "editor.defaultFormatter": "esbenp.prettier-vscode"
// },
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"[vue]": {
"editor.codeActionsOnSave": {
"source.fixAll.eslint": false
}
},
"terminal.integrated.automationShell.windows": "F:\Git\bin\bash.exe",
"terminal.integrated.shell.windows": "F:\Git\bin\bash.exe",
"editor.formatOnSave": true,
"editor.formatOnPaste": true,
"editor.formatOnType": true,
"files.autoSave": "afterDelay",
}
好嘞尝鲜到此结束~明天继续搬砖ing!!!!内容为自己总结原创,未经同意,请勿随意转载~谢谢合作~~