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()],
}),
],
};
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: ['*'],
},
},
};
import "lib-flexible/flexible.js";
package.json
删除 “type”: “module”, 或者改为 “type”:“commjs”
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>
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");
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"]
}
下载
yarn add normalize.css
main.js
import "normalize.css";
:root {
--van-primary-color: #00418e !important;
}
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 写法
.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");
}
}
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
);
// 加载图片
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("失败");
}
};
<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"]
基于 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
}
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);
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;
}
});
多半原因是定义的变量层级过深
<template v-if="detail.exam?.status === 1"> 模板中使用 ?.
const detail = ref({});
// 推荐
const detail = ref({
userExam: {},
exam: {},
});
腾讯选点组件
调用方式二 用的是哈希路由需要
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;
};
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 || "加载微信配置失败");
}
};
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
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" />
测试公众号申请地址
此时还需要配置 体验接口权限
格式和上面一致
准备工作已经完成
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}`;
};
下载
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 () {},
});
});
};
下载
pnpm add vite-plugin-remove-console -D
vite.config.ts
import removeConsole from "vite-plugin-remove-console";
plugins: [
removeConsole(),
],
:deep(textarea) {
overflow: hidden !important;
}