vue3 + vant3.x 项目 更新vant typescript---记录总结
vue3 + vant3.x 搭建项目
下载 vant
yarn add vant
引入组件
yarn 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()],
}),
],
};
Rem 适配
yarn add postcss-pxtorem lib-flexible
postcss.config.js
module.exports = {
plugins: {
// postcss-pxtorem 插件的版本需要 >= 5.0.0
'postcss-pxtorem': {
rootValue({ file }) {
return file.indexOf('vant') !== -1 ? 37.5 : 75;
},
propList: ['*'],
},
},
};
注意 main.js 引入 lib-flexible
import "lib-flexible/flexible.js";
package.json
删除 “type”: “module”, 或者改为 “type”:“commjs”
router
yarn add vue-router@4
src/router/index.js
import { createRouter, createWebHashHistory, createWebHistory } from "vue-router";
import home from "./home";
const test = [
{
path: "/test",
name: "test",
component: () => import("@/views/test/Test.vue"),
meta: {
title: "测试",
requiresAuth: false,
},
},
];
const notFound = [
{
path: "/:pathMatch(.*)*",
name: "NotFound",
component: () => import("@/views/error/NotFound.vue"),
},
];
const routes = [...home, ...test, ...notFound];
const router = createRouter({
history: createWebHistory(),
routes,
});
router.beforeEach(async (to, from) => {
if (to.meta.title) document.title = to.meta.title;
else document.title = "xx";
return true;
});
export default router;
main.js
import router from "@/router";
createApp(App).use(pinia).use(router).mount("#app");
app.vue
<template>
<router-view></router-view>
</template>
pinia
yarn add pinia
yarn add pinia-plugin-persist
src/store/common.js
import { defineStore, createPinia } from "pinia";
const id = "@@common";
const initialState = {
keepAlive: [],
};
export const useCommonStore = defineStore(id, {
state: () => ({ ...initialState }),
getters: {},
actions: {},
persist: {
enabled: true,
},
});
export function useCommonStoreWithOut() {
return useCommonStore(createPinia());
}
main.js
import { createPinia } from "pinia";
import piniaPersist from "pinia-plugin-persist";
const pinia = createPinia();
pinia.use(piniaPersist);
createApp(App).use(pinia).use(router).mount("#app");
axios
src/utils/http.js
import axios from "axios";
import { getLocalData, removeLocalData } from "@/utils/utils";
import { Toast } from "vant";
const noLogin = ["/login"];
const instance = axios.create({
baseURL: import.meta.env.VITE_BASE_API,
timeout: 50000,
headers: { "Content-Type": "application/json" },
});
instance.interceptors.request.use(
(config) => {
if (!noLogin.includes(config?.url)) {
const token = getLocalData("token");
if (true) {
config.headers = {
...config.headers,
Authorization: `Bearer ${token}`,
};
}
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
instance.interceptors.response.use(
(response) => {
console.log("response", response.data);
if (response.data?.code !== 200) {
if (response.data?.message) Toast(response.data?.message);
return response.data;
} else {
return response.data;
}
},
(error) => {
console.log("error", error);
if (error?.response?.data?.code == 599) {
removeLocalData("token");
} else {
Toast(error?.response?.data?.message || error?.message || "服务器异常,请稍后重试");
return new Promise(() => ({}));
}
// return Promise.reject(error);
}
);
export default instance;
src/servers/common.js
import services from "@/utils/http";
export const getName = () => services.get(`/api/getName`);
上传文件
// 上传
export const upload = async (file: File) => {
const formData = new FormData();
formData.append("file", file);
const res: AxiosData<UploadData> = await services.post("/api/upload/image", formData, {
headers: { "Content-Type": "multipart/form-data" },
});
return res;
};
配置别名
vite.config.js
resolve: {
// 路径别名配置
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
// vscode 路径提示
jsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"jsx": "preserve"
},
"exclude": ["node_modules", "dist"]
}
normalize.css
下载
yarn add normalize.css
main.js
import "normalize.css";
eslint prettier, husky lint-staged, @commitlint/config-conventional @commitlint/cli
npm init @eslint/config
.eslintrc.js 修改
module.exports = {
env: {
browser: true,
es2021: true,
},
extends: [
// "eslint:recommended",
// "plugin:vue/vue3-essential",
"plugin:vue/vue3-recommended",
"plugin:@typescript-eslint/recommended",
],
overrides: [],
parser: "vue-eslint-parser",
parserOptions: {
parser: "@typescript-eslint/parser",
ecmaVersion: "latest",
sourceType: "module",
},
plugins: ["vue", "@typescript-eslint"],
rules: {
// indent: 'off',
// 'vue/script-indent': ['error', 2, { baseIndent: 1 }],
// 'linebreak-style': ['error', 'unix'],
// quotes: ['error', 'single'],
// semi: ['error', 'always'],
// 'vue/no-multiple-template-root': 'off',
// 'vue/no-v-model-argument': 'off',
},
};
常用命令
检测文件
npx eslint “src/App.vue”
npx eslint --ext .js,vue src
npx eslint “src/**/*.{js,vue}”
格式化文件
npx eslint “src/**/*.{js,vue}” --fix
添加 .eslintignore
/node_modules
/public
/dist
.vscode
.husky
components.d.ts
auto-imports.d.ts
Prettier
如果您使用 ESLint,请安装 eslint-config-prettier 以使 ESLint 和 Prettier 彼此配合得很好。它会关闭所有不必要的或可能与 Prettier 冲突的 ESLint 规则。
pnpm add eslint-config-prettier prettier -D
.eslintrc.js 修改
module.exports = {
extends: [
...
"prettier"
],
};
添加 .prettierrc.json
{
"singleQuote": true,
"printWidth": 80,
"tabWidth": 4,
"endOfLine": "lf",
"semi": true,
"trailingComma": "all",
"vueIndentScriptAndStyle": true,
"htmlWhitespaceSensitivity": "ignore"
}
添加 .prettierignore
/node_modules
/public
/dist
.vscode
.husky
components.d.ts
auto-imports.d.ts
格式化代码
npx prettier --write .
vscode 插件 ESLint Prettier - Code formatter
husky lint-staged
pnpm add husky lint-staged -D
npm pkg set scripts.prepare=“husky install”
package.json 生成命令
{
"scripts": {
...
"prepare": "husky install"
}
}
npm run prepare
根目录生成 .husky 文件夹
npx husky add .husky/pre-commit “npx lint-staged”
.husky 文件夹 生成 pre-commit
pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# 提交 git 时会自动执行此命令
npx lint-staged
package.json
{
...
"lint-staged": {
"*.{js,jsx,ts,tsx,vue}": [
"eslint --cache --fix",
"prettier --write"
]
}
}
@commitlint/config-conventional @commitlint/cli
下载
pnpm add @commitlint/config-conventional @commitlint/cli -D
添加 commitlint.config.js
注意 如果是命令创建注意修改编码为 utf-8
echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js
使用 Husky 的 commit-msg 钩子
npx husky install
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit ${1}'
commitlint.config.js
1:0 error Parsing error: Invalid character
echo "module.exports 编码不是utf-8
把 commitlint.config.js 编码格式改为 utf-8
CRLF 和 LF
在你使用git拉取代码的时候,git会自动将代码当中与你当前系统不同的换行方式转化成你当前系统的换行方式,从而造成这种冲突。
解决方案 vscode
-
在设置里Eol 选\n
-
修改git全局配置,禁止git自动将lf转换成crlf, 命令 然后重新克隆项目
git config --global core.autocrlf false
自定义的全局变量没有覆盖上 vant 全局变量
:root {
--van-primary-color: #00418e !important;
}
vueuse
useElementSize 获取不到元素的边距,可以嵌套一层给内层元素添加边距
<header ref="headerRef"></header>
<div :style="{ height: swiperHeight }"></div>
import { useElementSize } from "@vueuse/core";
const headerRef = ref(null);
const { height: headerHeight } = useElementSize(headerRef);
const swiperHeight = computed(() => {
return `calc(100vh - ${headerHeight.value}px)`;
});
calc
calc 写法
.hot{
width: calc((100% - 150px) / 3);
}
calc 写入 css 变量
.root {
height: calc(100vh - var(--van-nav-bar-height) - var(--van-tabbar-height));
}
calc 写入响应式变量
import { useDebounceFn, useElementSize } from "@vueuse/core";
const header = ref(null);
const { height } = useElementSize(header);
const styles = computed(() => {
return `min-height: calc(100vh - ${height.value}px`;
});
<div :style="styles"></div>
下拉刷新、上拉加载更多
<van-tab title="我收到的" :dot="isDot">
<div class="tabs">
<van-pull-refresh v-model="refreshLoading" @refresh="onRefresh">
<van-list
v-model:loading="state.list[1].loading"
:finished="state.list[1].finished"
finished-text="没有更多了"
@load="onLoad"
:immediate-check="false"
>
</van-list>
<template v-else><van-empty description="暂无内容" /></template>
</van-pull-refresh>
</div>
</van-tab>
const active = ref(0);
const state = reactive({
list: [
{
page: 1,
limit: 10,
data: [],
loading: true,
finished: false,
},
{
page: 1,
limit: 10,
data: [],
loading: true,
finished: false,
},
],
});
// 初始化
onMounted(() => {
fetchData(active.value);
});
// 上拉加载
const onLoad = async () => {
fetchData(active.value);
};
// 下拉刷新
async function onRefresh() {
onLoad();
}
// 获取数据
async function fetchData(index) {
try {
let res;
// 刷新重置数据
if (refreshLoading.value) {
state.list[index].page = 1;
state.list[index].loading = true;
state.list[index].finished = false;
}
// 当前列表全部加载完成退出不再请求接口
if (state.list[index].finished) return;
if (index == 0) {
res = await workSchedules({
page: state.list[index].page,
limit: state.list[index].limit,
});
} else {
res = await workReviewSchedules({
page: state.list[index].page,
limit: state.list[index].limit,
});
}
// 全局 loading 刷新状态
globalLoading.value = false;
refreshLoading.value = false;
// 加载状态结束
state.list[index].loading = false;
if (state.list[index].page <= 1) {
// 如果是第一页数据赋值
state.list[index].data = res.data;
} else {
// 否则数据合并
state.list[index].data = [...state.list[index].data, ...res.data];
}
// 数据加载完成 finished 设置 true
if (state.list[index].page >= res.meta.last_page) {
return (state.list[index].finished = true);
}
// page + 1
state.list[index].page = state.list[index].page * 1 + 1;
} catch (error) {
console.log(error, "error");
}
}
van-field 默认展示一行高度自适应
rows=“1”
autosize
<van-field
rows="1"
type="textarea"
maxlength="60"
placeholder="暂未填写"
v-model="user.serve"
autosize
:readonly="serveEdit"
/>
阻止默认下拉回弹效果
document.body.addEventListener(
"touchmove",
function (e) {
e.preventDefault(); // 阻止默认的处理方式(阻止下拉滑动的效果)
},
{ passive: false } // passive 参数不能省略,用来兼容ios和android
);
判断手势滑动方向
type Direction = "left" | "right" | "up" | "down" | "initial";
// 获取方向
function getSlideDirection(startX: number, startY: number, endX: number, endY: number) {
const dy = startY - endY;
const dx = endX - startX;
let direction: Direction = "initial";
if (Math.abs(dx) < 10 && Math.abs(dy) < 10) return direction;
const angle = getSlideAngle(dx, dy);
if (angle >= -45 && angle < 45) direction = "right";
else if (angle >= 45 && angle < 135) direction = "up";
else if (angle >= -135 && angle < -45) direction = "down";
else if ((angle >= 135 && angle <= 180) || (angle >= -180 && angle < -135)) direction = "left";
return direction;
}
// 获取角度
function getSlideAngle(dx: number, dy: number) {
return (Math.atan2(dy, dx) * 180) / Math.PI;
}
阻止鼠标右键
// 禁止
export function prohibitCopy() {
// 禁用右键
document.oncontextmenu = function () {
return false;
};
// 禁用选择
document.onselectstart = function () {
return false;
};
}
动态加载本地资源
// 加载图片
export const getAssetsFile = (url: string) => {
return new URL(`../assets/${url}`, import.meta.url).href;
};
<img
:src="props.active ? getAssetsFile('tabbar/skill_select.png') : getAssetsFile('tabbar/skill.png')"
/>
一键复制粘贴板功能
// 复制
export const copyText = (str: string) => {
//使用textarea的原因是能进行换行,input不支持换行
var copyTextArea = document.createElement("textarea");
//自定义复制内容拼接
copyTextArea.value = str;
document.body.appendChild(copyTextArea);
copyTextArea.select();
try {
var copyed = document.execCommand("copy");
if (copyed) {
document.body.removeChild(copyTextArea);
showToast("复制成功");
}
} catch {
showToast("失败");
}
};
picker 自定义 Columns 的结构
<van-picker
:columns="shiftData"
:columns-field-names="customFieldName"
/>
const customFieldName = {
text: "value",
value: "key",
};
const shiftData = [
{key: 16, value: "xx"}
]
字符串数组与数字数组快速转换
const arr = ["1", "2", 3];
console.log(arr.map(Number)); // [1, 2, 3]
console.log(arr.map(String)); // ["1", "2", "3"]
$attrs 与 inheritAttrs 使用
基于 button 组件二次封装时,想要之前的属性 v-bind=“$attrs”
<template>
<div>
<slot>
<van-button
v-bind="$attrs"
type="primary"
size="large"
:class="customClass"
></van-button
></slot>
</div>
</template>
添加完之后你会发现 click 事件多次触发 inheritAttrs:false
export default {
inheritAttrs:false
}
Tab 标签页 NavBar 导航栏 粘贴性布局
yarn add @vueuse/core
<nav-bar ref="navBarRef" />
<van-tabs
v-model:active="active"
color="var(--van-primary-color)"
sticky
:offset-top="height"
@click-tab="handleClickTab"
>
</van-tabs>
import { useElementSize } from "@vueuse/core";
const navBarRef = ref(null);
const { height } = useElementSize(navBarRef);
Dialog 弹出框 配合 onBeforeRouteLeave 实现返回页面提示
注意 Dialog 要设置 closeOnPopstate: false 否则后面会出现弹框一闪
closeOnPopstate 是否在页面回退时自动关闭
<script setup>
import { onBeforeRouteLeave } from "vue-router";
import NavBar from "@/components/NavBar/index.vue";
import { Dialog } from "vant";
onBeforeRouteLeave(async () => {
try {
const res = await Dialog.confirm({
message:
"如果解决方法是丑陋的,那就肯定还有更好的解决方法,只是还没有发现而已。",
closeOnPopstate: false,
});
} catch (error) {
return false;
}
});
</script>
如果当前页还有其他路由跳转或者确定之后跳转其他页面(出现死循环)
修改如下
onBeforeRouteLeave(async (to) => {
try {
if (to.path == "/mine/result") {
return true;
}
// 同步方式有问题使用 .then 方式
Dialog.confirm({
message: "是否确认提交答案并退出考试。",
closeOnPopstate: false,
confirmButtonText: "提交答案",
}).then(async () => {
await examFinish({
examId: route.query?.id,
});
router.replace({
path: "/mine/result",
query: {
id: route.query?.id,
},
});
});
return false;
} catch (error) {
return false;
}
});
Unhandled error during execution of render function / Unhandled error during execution of scheduler flush.
多半原因是定义的变量层级过深
<template v-if="detail.exam?.status === 1"> 模板中使用 ?.
const detail = ref({});
// 推荐
const detail = ref({
userExam: {},
exam: {},
});
vue 结合腾讯选点组件使用
调用方式二 用的是哈希路由需要
encodeURIComponent 可以将特殊字符转义
referer 名称
const url = encodeURIComponent(`${window.location.origin}/#/checkWork/form`);
const key = "YO7BZ-7353J";
window.location.replace(
`https://apis.map.qq.com/tools/locpicker?search=1&type=0&backurl=${url}&key=${key}&referer=myapp`
);
根据经纬度计算距离
// 经纬度计算距离 单位/米
export const GetDistance = (lat1, lng1, lat2, lng2) => {
var radLat1 = (lat1 * Math.PI) / 180.0;
var radLat2 = (lat2 * Math.PI) / 180.0;
var a = radLat1 - radLat2;
var b = (lng1 * Math.PI) / 180.0 - (lng2 * Math.PI) / 180.0;
var s =
2 *
Math.asin(
Math.sqrt(
Math.pow(Math.sin(a / 2), 2) +
Math.cos(radLat1) * Math.cos(radLat2) * Math.pow(Math.sin(b / 2), 2)
)
);
s = s * 6378.137;
s = Math.round(s * 10000) / 10;
return s;
};
vue 使用 js-sdk 动态引入
loadJs
function loadJs(src) {
return new Promise((resolve, reject) => {
let script = document.createElement('script');
script.type = "text/javascript";
script.src = src;
document.body.appendChild(script);
script.onload = () => {
resolve();
}
script.onerror = () => {
reject();
}
})
}
export default loadJs
// 加载微信配置
const loadWxConfig = async () => {
try {
if (typeof wx !== "object") {
await loadJs("https://res.wx.qq.com/open/js/jweixin-1.6.0.js");
}
let wxConfig = await wxConfigApi();
wx.config(wxConfig);
wx.ready(() => {
wxLocation();
});
} catch (error) {
Notify(error?.message || error?.msg || "加载微信配置失败");
}
};
使用 weixin-js-sdk
pnpm add weixin-js-sdk
wx.d.ts
declare module "weixin-js-sdk";
import wx from "weixin-js-sdk";
wx.config(wxConfig);
wx.ready(() => {
})
terser not found. Since Vite v3, terser has become an optional dependency. You need to install it.
pnpm add terser
编写 svg 通用组件
下载
pnpm add vite-plugin-svg-icons -D
配置
vite.config.ts
import { createSvgIconsPlugin } from "vite-plugin-svg-icons";
plugins: [
DefineOptions(),
createSvgIconsPlugin({
// 指定需要缓存的图标文件夹(路径为存放所有svg图标的文件夹不单个svg图标)
iconDirs: [path.resolve(process.cwd(), "src/assets")],
// 指定symbolId格式
symbolId: "icon-[dir]-[name]",
}),
],
src/main.ts
import 'virtual:svg-icons-register'
编写通用组件
src/components/svgIcons/index.vue
<template>
<svg aria-hidden="true" :style="styles">
<use :xlink:href="iconName" :fill="color" />
</svg>
</template>
<script setup lang="ts">
import { computed } from "vue";
const props = defineProps({
iconClass: {
type: String,
required: true,
},
color: {
type: String,
default: "#08bebe",
},
width: {
type: String,
default: "20px",
},
height: {
type: String,
default: "20px",
},
});
const styles = computed(() => {
return `width: ${props.width}; height:${props.height}`;
});
const iconName = computed(() => {
return `#icon-${props.iconClass}`;
});
</script>
tsconfig.json
{
"compilerOptions": {
"types": ["vite-plugin-svg-icons/client"]
}
}
使用
svg存放的地址
如果是文件夹以 - 拼接
例1:src/assets/mask_star.svg
例2:src/assets/mine/mask_star.svg
引入组件
例1:icon-class="mask_star"
例2:icon-class="mine-mask_star"
<SvgIcons width="99px" height="99px" color="#ffffff" icon-class="mine-mask_star" />
测试公众号申请
配置
配置 JS 接口安全域名
启动vue项目的地址
关注测试公众号
此时还需要配置 体验接口权限
格式和上面一致
准备工作已经完成
代码
const AppID = "wx0b5b0581d4b4bd63";
const redirect_uri = 'http://192.168.1.5'
// 跳转至授权页面
export const gotoWxAuth = (randStr) => {
window.location.href = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${AppID}&redirect_uri=${encodeURIComponent(
redirect_uri
)}&response_type=code&scope=snsapi_userinfo&state=${randStr}`;
};
测试
微信开发者工具测试
使用 weixin-js-sdk
下载
pnpm add weixin-js-sdk
wx.d.ts
declare module "weixin-js-sdk";
使用
import wx from "weixin-js-sdk";
wx.config(wxConfig);
wx.ready(() => {
...
})
自定义分享
在公众号里进入分享手机端才可以展示自定义标题
type WxData = {
appId: string;
debug: boolean;
jsApiList: string[];
nonceStr: string;
openTagList: string[];
signature: string;
timestamp: number;
url: string;
};
// 获取微信配置
export const getWechat = async (list: string[]) => {
const res: AxiosData<WxData> = await services.get(
"/api/wechat/config?url=" + encodeURIComponent(window.location.href),
{ params: { list: list.join(",") } }
);
return res;
};
import wx from "weixin-js-sdk";
import { getWechat } from "@/services/common";
type LoadWxConfig = {
title?: string;
desc?: string;
link?: string;
imgUrl?: string;
cb?: () => void;
};
export const loadWxConfig = async (
list: string[] = ["updateAppMessageShareData", "updateTimelineShareData", "onMenuShareWeibo"]
) => {
try {
let wxConfig = await getWechat(list);
wx.config(wxConfig.data);
} catch (error) {
console.log(error);
}
};
export const customShare = async ({
title = "设文研",
desc = "",
link = window.location.href,
imgUrl = "http://swy.meikr.com/images/logo.jpg",
cb = () => {},
}: LoadWxConfig) => {
await loadWxConfig();
wx.ready(() => {
// 自定义“分享给朋友”及“分享到QQ”按钮的分享内容
wx.updateAppMessageShareData({
title,
desc,
link,
imgUrl,
success: function () {
cb?.();
},
});
// 自定义“分享到朋友圈”及“分享到QQ空间”按钮的分享内容
wx.updateTimelineShareData({
title,
link,
imgUrl,
success: function () {
cb?.();
},
});
// 获取“分享到腾讯微博”按钮点击状态及自定义分享内容接口
wx.onMenuShareWeibo({
title,
desc,
link,
imgUrl,
success: function () {
cb?.();
},
cancel: function () {},
});
});
};
微信公众号开放标签
<wx-open-subscribe
template="16sQK6zFtZNB8eXMyf6yYcAmACnSpug8kM-yXQ22pjU"
id="subscribe-btn"
@success="onSuccess"
@error="onError"
>
<div v-is="'script'" type="text/wxtag-template">
<div
class="btn"
style="
font-size: 18px;
width: 200px;
height: 60px;
line-height: 60px;
text-align: center;
margin: 10px auto;
background-color: aquamarine;
color: #fff;
"
@click="onClick"
>
订阅
</div>
</div>
</wx-open-subscribe>
<script lang="ts" setup>
import wx from "weixin-js-sdk";
wx.config(wxConfig.data);
</script>
去除线上打印信息 console
下载
pnpm add vite-plugin-remove-console -D
vite.config.ts
import removeConsole from "vite-plugin-remove-console";
plugins: [
removeConsole(),
],
h5 移动端调试插件 vconsole
https://github.com/Tencent/vConsole
下载
pnpm add vconsole -D
引入使用
import { onBeforeUnmount } from "vue";
import VConsole from "vconsole";
const vConsole = new VConsole();
console.log("Hello world");
onBeforeUnmount(() => {
vConsole.destroy();
});
隐藏 textarea 滚动条
:deep(textarea) {
overflow: hidden !important;
}
unplugin-auto-import 按需自动导入 API
下载
pnpm add unplugin-auto-import -D
vite.config.ts
import AutoImport from "unplugin-auto-import/vite";
plugins: [
AutoImport({
imports: ["vue", "vue-router"],
dirs: ["./src/components"],
dts: "./auto-imports.d.ts",
}),
],
tsconfig.json
{
"include": ["auto-imports.d.ts"]
}
更多推荐
所有评论(0)