跳转到内容

解决方案

根据目录自动生成路由

生成路由后访问的路径就是目录的路径:/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;
}

主题换肤

  1. 编写 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;
	/* ... */
}
  1. 设置默认主题
js
// 设置默认主题类名
if (!document.body.classList.contains('theme-default') && !document.body.classList.contains('theme-blue')) {
	document.body.classList.add('theme-default');
}
  1. 更新主题

不用 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');
}

全局更换字体大小

  1. 先设置全局参数
css
:root {
	--font-size-small: 12px;
	--font-size-normal: 14px;
	--font-size-large: 18px;
	--font-size-base: var(--font-size-normal);
}
  1. 切换不同大小的字体
js
// 更新字体大小
const updateFontSize = (size) => {
	themeConfig.fontSize = size;
	document.documentElement.style.setProperty('--font-size-base', `var(--font-size-${size})`);
};
  1. 全局替换字体
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打包时会报如下错误

  1. 指定rollup版本
json
{
	"resolutions": { "rollup": "4.50.0" }, // 指定rollup版本
	"devDependencies": {
		"vite": "npm:rolldown-vite@latest"
	}
}
  1. 替换关于 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);
}

Will Try My Best.