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";

vue3 + vant3.x 项目---记录总结_第1张图片
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";

自定义的全局变量没有覆盖上 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 写入响应式变量
const mainHeight = computed(() => `calc(100% - ${height.value}px - 30px)`);
<div :style="{ height: mainHeight, minHeight: minHeight }"></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"
/>

阻止默认下拉回弹效果

vue3 + vant3.x 项目---记录总结_第2张图片

document.body.addEventListener(
	"touchmove",
	function (e) {
		e.preventDefault(); // 阻止默认的处理方式(阻止下拉滑动的效果)
	},
	{ passive: false } // passive 参数不能省略,用来兼容ios和android
);

动态加载本地资源

// 加载图片
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 实现返回页面提示

vue3 + vant3.x 项目---记录总结_第3张图片

注意 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 结合腾讯选点组件使用

vue3 + vant3.x 项目---记录总结_第4张图片

腾讯选点组件

调用方式二  用的是哈希路由需要 
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" />

测试公众号申请

vue3 + vant3.x 项目---记录总结_第5张图片

测试公众号申请地址

配置

配置 JS 接口安全域名
vue3 + vant3.x 项目---记录总结_第6张图片
启动vue项目的地址
在这里插入图片描述

关注测试公众号
vue3 + vant3.x 项目---记录总结_第7张图片

此时还需要配置 体验接口权限
vue3 + vant3.x 项目---记录总结_第8张图片
格式和上面一致
vue3 + vant3.x 项目---记录总结_第9张图片
准备工作已经完成

代码

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}`;
};

测试

微信开发者工具测试
vue3 + vant3.x 项目---记录总结_第10张图片

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

去除线上打印信息 console

下载

pnpm add vite-plugin-remove-console -D

vite.config.ts

import removeConsole from "vite-plugin-remove-console";

plugins: [
    removeConsole(),
],

隐藏 textarea 滚动条

:deep(textarea) {
	overflow: hidden !important;
}

你可能感兴趣的:(#,vant,vue,vue.js)