主题
解决方案
根据目录自动生成路由
生成路由后访问的路径就是目录的路径:/views/element/dialog-demo
js
import { createRouter, createWebHashHistory } from 'vue-router';
import routes from 'virtual:generated-pages';
routes.push({
path: '/',
redirect: '/home',
});
routes.push({
path: '/:pathMatch(.*)*',
name: 'NotFound',
meta: { title: '404 Not Found' },
component: () => import('@/views/404.vue'),
});
const router = createRouter({
history: createWebHashHistory(),
routes,
scrollBehavior(to, from, savedPosition) {
return savedPosition || { top: 0 };
},
});
export const allRoutes = routes;
export default router;js
Pages({
dirs: 'src/views',
routeBlockLang: 'json5',
// 排序路由
onRoutesGenerated(routes) {
const newRoutes = routes.slice().sort((a, b) => {
const hasA = a.meta?.order != null;
const hasB = b.meta?.order != null;
if (hasA && hasB) return a.meta.order - b.meta.order; // 都有 order
if (hasA) return -1; // a 有 order,排前
if (hasB) return 1; // b 有 order,排前
return 0; // 都没有 order,保持原顺序
});
return newRoutes
},
})全局请求 Loading 加载
再进行网络请求的时候进行 loading,如果有多个网络请求,loading 的结束应该在最后一个请求结束后关闭
js
import { customRef } from 'vue';
/**
* 全局loading变量,多个请求保持loading状态,而不会在1个请求结束后关闭loading
*/
export const loading = customRef((track, trigger) => {
let loadingCount = 0;
return {
// loading.value会触发get
get() {
track(); // 收集哪些地方调用了这个依赖,后续通知更新
// loadingCount为0自动关闭loading状态
return loadingCount > 0;
},
set(value) {
// 如果loading设置为true就+1否则-1
loadingCount += value ? 1 : -1;
loadingCount = Math.max(0, loadingCount); // 处理边界,如果小于0就赋值为0
trigger(); // 通知更新
},
};
});vue
<template>
<div v-loading="loading" class="app_content"></div>
</template>
<script setup>
// js部分
import { ref } from 'vue';
import { loading } from '@/hooks/useLoadingRef.js';
// **************** Loading Start****************
const userList = ref([]);
const getUser = () => {
loading.value = true;
setTimeout(() => {
loading.value = false;
userList.value.push('张三');
console.log('getUser请求结束');
}, 2000);
};
const getAllUser = () => {
userList.value = [];
getUser();
loading.value = true;
setTimeout(() => {
loading.value = false;
userList.value.push('李四');
console.log('getAllUser请求结束');
}, 5000);
};
// **************** Loading End****************
</script>批量注册目录组件
typescript
const modules = import.meta.glob('./*.vue');
const components = ref<Record<string, ReturnType<typeof defineAsyncComponent>>>({});
Object.entries(modules).forEach(([path, asyncCom]) => {
const name = path.replace(/\.\/(.*)\.vue/, '$1');
components.value[name] = markRaw(defineAsyncComponent(asyncCom as any));
});
export default components;vue
<template>
<div v-for="(item, index) in componentList" :key="index">
<component :is="customComponents[item]"></component>
</div>
</template>
<script setup lang="ts">
import customComponents from './components/customComponents';
const componentList: any = ref(['comp1', 'comp-test', 'Comp2', 'CompTest2']);
</script>动态获取图片地址
vue3 中使用,如果部署线上出现资源 404 找不到,很有可能是动态图片地址没有使用该方法。
javascript
/**
* 动态获取图片路径
* @param icon
*/
export function getImgUrl(icon: string) {
return new URL(`../assets/images/${icon}`, import.meta.url).href;
}主题换肤
- 编写 2 套主题 css 更换
css
.theme-default {
/* 主色(Primary) */
--el-color-primary: #d93824 !important;
--el-color-primary-light-9: #fdf0ee !important;
--el-color-primary-light-8: #fce1dd !important;
--el-color-primary-light-7: #f9c9c2 !important;
/* ... */
}css
.theme-blue {
/* 主色(Primary) */
--el-color-primary: #1976d2 !important;
--el-color-primary-light-9: #eaf3fc !important;
--el-color-primary-light-8: #d5e7f9 !important;
--el-color-primary-light-7: #b3d1f2 !important;
--el-color-primary-light-6: #8cbaeb !important;
/* ... */
}- 设置默认主题
js
// 设置默认主题类名
if (!document.body.classList.contains('theme-default') && !document.body.classList.contains('theme-blue')) {
document.body.classList.add('theme-default');
}- 更新主题
不用 toggle 的原因是,有就删除没有就加,容易留下多个主题
js
// 更新DOM中的主题类
const body = document.body;
if (theme === 'blueTheme') {
body.classList.add('theme-blue');
body.classList.remove('theme-default');
} else {
body.classList.add('theme-default');
body.classList.remove('theme-blue');
}全局更换字体大小
- 先设置全局参数
css
:root {
--font-size-small: 12px;
--font-size-normal: 14px;
--font-size-large: 18px;
--font-size-base: var(--font-size-normal);
}- 切换不同大小的字体
js
// 更新字体大小
const updateFontSize = (size) => {
themeConfig.fontSize = size;
document.documentElement.style.setProperty('--font-size-base', `var(--font-size-${size})`);
};- 全局替换字体
css
div {
font-size: var(--font-size-base);
}打包清除缓存
静态资源如果浏览器缓存过久,会导致用户访问旧版本页面
js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [vue()],
build: {
outDir: 'dist',
assetsDir: 'assets',
// 使用hash,默认就是 true
// 生成 index.[hash].js, style.[hash].css 等文件
rollupOptions: {
output: {
chunkFileNames: 'assets/js/[name]-[hash].js',
entryFileNames: 'assets/js/[name]-[hash].js',
assetFileNames: 'assets/[ext]/[name]-[hash].[ext]',
},
},
},
});防止IOS表单input聚焦时页面放大
html
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no" />跨页面通信
使用BroadcastChannel实现同源页面通信
js
const bc = new BroadcastChannel('channelName');
bc.postMessage(data); // 发送消息
bc.onmessage = (e) => console.log(e.data); // 接收消息阻止浏览器自动填充表单
html
<el-input v-model="loginForm.password" type="password" auto-complete="new-password" placeholder="密码" @keyup.enter.native="handleLogin" />重置组件内的data属性
js
this.dataForm = this.$options.data().dataForm;vant在rollup@4.50.1 下 build时报错(ERROR: The symbol "bem" has already been declared)
rollup更新到 4.50.1 Vite build打包时会报如下错误
- 指定rollup版本
json
{
"resolutions": { "rollup": "4.50.0" }, // 指定rollup版本
"devDependencies": {
"vite": "npm:rolldown-vite@latest"
}
}- 替换关于 rollup 依赖所造成的影响
js
import { defineConfig, loadEnv } from 'vite';
export default ({ mode, command }) => {
return defineConfig({
build: {
minify: 'terser', // 打包移除控制台日志
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
},
// 删除,会使用
/**
mangle: {
properties: true,
toplevel: true,
}, // 混淆变量名
*/
format: {
comments: false, // 移除所有注释
ascii_only: true,
},
},
},
});
};img标签使用base64
js
// 添加前缀
imgUrl.value = 'data:image/png;base64,' + res.data.image;覆盖数组指定的对象元素
js
// 更新数据
const index = contentList.findIndex((it) => it.dictCode === data.dictCode);
if (index !== -1) {
contentList[index] = data;
}v-html图片适配问题
javascript
this.slideList.newsText = this.slideList.newsText.replace(/<img/g, "<img style='max-width:100%;height:auto;'");css
/deep/ img {
width: 100%;
}更名调用方法
js
import { selectSystemPhoto } from "@/utils/uni-api.js"
uploadAvatar: () => selectSystemPhoto((res) => {
console.log(res, "res");
}),浏览器地址栏参数变化回车后重载页面
js
window.addEventListener('popstate', () => {
window.location.reload();
});表单回车提交
html
<el-form @submit.native.prevent @keyup.enter.native="submitSetting" label-position="left" label-width="120px" :model="form"> </el-form>全局配置页
vue
<template>
<div>
<van-cell-group title="全局选项">
<van-cell title="开启控制台" value="内容">
<template #value>
<van-switch v-model="isOpenConsole" @change="(v) => changeStatus(v, 'console')" />
</template>
</van-cell>
</van-cell-group>
<van-cell-group title="快速跳转">
<van-cell title="测试" is-link to="test" />
</van-cell-group>
</div>
</template>
<script setup lang="ts">
import { openVConsole, closeVConsole } from '@/utils/v-console.ts';
import { isNotEmpty } from 'l-js-fn';
const isOpenConsole = ref(false);
const changeStatus = (v, status) => {
localStorage.setItem(status, JSON.stringify(v));
if (isOpenConsole.value) {
openVConsole();
} else {
closeVConsole();
}
};
onMounted(async () => {
// 生产环境开启
if (import.meta.env.VITE_APP_ENV !== 'production') {
return;
}
isOpenConsole.value = isNotEmpty(localStorage.getItem('console')) ? JSON.parse(localStorage.getItem('console')) : false;
changeStatus(isOpenConsole.value, 'console');
});
</script>
<style scoped></style>ts
let vConsole = null;
// 打开
export function openVConsole() {
if (!vConsole) {
vConsole = new VConsole();
console.log('[vConsole] 已打开');
}
}
// 关闭
export function closeVConsole() {
if (vConsole) {
vConsole.destroy();
vConsole = null;
console.log('[vConsole] 已关闭');
}
}文件下载
js
import request from '/@/utils/request';
/**
*
* @param url 目标下载接口
* @param query 查询参数
* @param fileName 文件名称
* @returns {*}
*/
export function downBlobFile(url: any, query: any, fileName: string) {
return request({
url: url,
method: 'get',
responseType: 'blob',
params: query,
}).then((response) => {
handleBlobFile(response, fileName);
});
}
/**
* blob 文件刘处理
* @param response 响应结果
* @returns
*/
export function handleBlobFile(response: any, fileName: string) {
// 处理返回的文件流
const blob = response;
if (blob && blob.size === 0) {
return;
}
const link = document.createElement('a');
// 兼容一下 入参不是 File Blob 类型情况
var binaryData = [] as any;
binaryData.push(response);
link.href = window.URL.createObjectURL(new Blob(binaryData));
link.download = fileName;
document.body.appendChild(link);
link.click();
window.setTimeout(function () {
// @ts-ignore
URL.revokeObjectURL(blob);
document.body.removeChild(link);
}, 0);
}