前言
在开发过程中,总会碰到很多的项目中的数据取决于接口,根据接口的数据来进行渲染,就比如项目中的菜单,已不再单单的写死的前端项目里,所以在搭建项目的时候如何快速搭建一个根据mockjs来模拟接口返回数据就显得尤为重要。下面文件有一点点点点长。。。
技术站:vue3+vite+javascript+pina+mockjs+ant-design-vue+less
效果展示:
效果展示
代码地址
一、新建一个vue3项目
1、npm create vite
vite版本若是最新的版本5.0以上,则要求node版本在18以上,所以使用的vite版本是^4.3.9
2、选择项目中使用的语言
3、进入项目中安装依赖启动
4、项目启动成功
二、项目中引入vue-router和pina
1、引入vur-router 和 pina
yarn add vue-router pinia pinia-plugin-persistedstate 或
yarn add vue-router
yarn add pina
yarn add pinia-plugin-persistedstate
2、在src目录下新增router和store文件夹,并新建index.js文件
1)、/store/index.js
import { createPinia } from 'pinia';
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
const store = createPinia();
store.use(piniaPluginPersistedstate);
export default store;
2)、在/store中新增模块文件,如:module/use.js
/store/module/use.js
import { defineStore } from 'pinia';
const useStoreOut = defineStore('user', {
state: () => ({
token: '',
}),
getters: {
getLoginToken() {
return this.token;
},
},
actions: {
setLoginToken(data) {
this.token = data || '';
},
}
})
export default useStoreOut;
3)、/router/index.js
import { createRouter, createWebHashHistory } from 'vue-router';
import indexRoute from './module';
const Router = createRouter({
history: createWebHashHistory(),
strict: true,
routes: [...indexRoute],
scrollBehavior: () => ({ left: 0, top: 0 }),
});
export default Router;
/router/module/index.js中引入登录页面
const indexRouters = [
{
path: '/login',
name: 'login',
component: () => import('/@/views/sys/login/login.vue'),
meta: {
title: '登录',
keepAlive: false,
},
},
];
export default indexRouters;
在src目录下新建views文件夹(每个页面模块文件),并在该文件夹下新建功能模块sys文件夹下vue文件,
如:/src/views/sys/login/login.vue
<template>
<div>
登录
<Button type="primary">点击</Button>
</div>
</template>
<script>
import { defineComponent } from 'vue';
import { Button } from 'ant-design-vue';
export default defineComponent({
components: {
Button,
},
setup() {
return {};
},
});
</script>
<style lang="less" scoped></style>
4)、在main.js文件中直接接入vue-router和pina,因为项目中使用了ant-design-vue组件库,所以使用按需引入:
yarn add ant-design-vue
import { createApp } from 'vue';
import router from './router/index.js';
import App from './App.vue';
import store from '/@/store/index.js';
import('ant-design-vue/dist/antd.css');
const app = createApp(App);
app.use(store);
app.use(router);
app.mount('#app');
5)、最后在项目的vite配置文件(vite.config.js)中添加文件访问的相对路径和项目启动后的配置
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { resolve } from 'path';
import { loadEnv } from 'vite';
// https://vitejs.dev/config/
export default defineConfig((mode, command) => {
const env = loadEnv(mode.mode, process.cwd(), '');
const { VITE_PUBLIC_PATH, VITE_PORT } = env;
return {
plugins: [
vue(),
],
root: process.cwd(),
base: VITE_PUBLIC_PATH,
resolve: {
alias: [
{
find: '/@',
replacement: resolve(__dirname, '/src'),
},
],
},
define: {
'process.env': env,
__INTLIFY_PROD_DEVTOOLS__: false,
FEATURE_FLAG: true,
},
server: {
host: '0.0.0.0', // Listening on all local IPs
port: VITE_PORT, // 将端口号替换为你希望使用的端口号
proxy: {
'/auth': {
target: 'https://example.com', //接口域名
changeOrigin: true, //是否跨域
rewrite: (path) => path.replace(/^\/auth/, ''),
},
},
},
};
});
至此一个简单的项目就配置好了,
三、实现上面动态背景的登陆页
1、在login/component中新建LoginBg.vue文件,动画效果引用了tsparticles插件,所以项目中引入:tsparticles
yarn add tsparticles
yarn add vue3-particles
在main.js中全局引入Particles
import { createApp } from 'vue';
import router from './router/index.js';
import App from './App.vue';
import store from '/@/store/index.js';
import Particles from 'vue3-particles';
if (process.env.NODE_ENV) {
import('ant-design-vue/dist/antd.css');
}
const app = createApp(App);
app.use(store);
app.use(router);
app.use(Particles);
app.mount('#app');
LoginBg.vue文件直接上代码
<template>
<Particles
id="tsparticles"
class="login-partic"
ref="tsparticles"
:options="options"
:particlesInit="particlesInit"
:particlesLoaded="particlesLoaded"
/>
</template>
<script>
import { defineComponent, onUnmounted, ref } from 'vue';
import { loadFull } from 'tsparticles';
import data from './bgData';
export default defineComponent({
components: {},
setup() {
const options = data;
const tsparticles = ref(null);
const particlesInit = async (engine) => {
await loadFull(engine);
};
const particlesLoaded = () => {};
onUnmounted(() => {
if (tsparticles.value) {
tsparticles.value.destroy();
}
});
return {
options,
tsparticles,
particlesInit,
particlesLoaded,
};
},
});
</script>
<style lang="less" scoped>
.login-partic {
position: absolute;
width: 100%;
z-index: 1;
height: 100%;
}
</style>
定制样式
bgData.js文件
export default {
fpsLimit: 60,
fullScreen: {
enable: true,
zIndex: -1,
},
background: {
color: {
value: 'rgba(1,0,53,0.5)',
},
image: "url('')",
position: '100% 100%',
repeat: 'no-repeat',
size: 'cover',
opacity: 1,
zIndex: 12,
},
particles: {
number: {
value: 80,
density: {
enable: true,
value_area: 800,
},
},
color: {
value: ['#2EB67D', '#ECB22E', '#E01E5B', '#36C5F0'],
},
shape: {
type: ['circle'],
stroke: {
width: 0,
color: '#fff',
},
polygon: {
nb_sides: 5,
},
},
opacity: {
value: 1,
random: false,
anim: {
enable: false,
speed: 1,
opacity_min: 0.1,
sync: false,
},
},
size: {
value: 8,
random: true,
anim: {
enable: false,
speed: 10,
size_min: 10,
sync: false,
},
},
line_linked: {
enable: true,
distance: 150,
color: '#fff',
opacity: 0.5,
width: 1,
},
move: {
enable: true,
speed: 3,
direction: 'none',
random: false,
straight: false,
out_mode: 'out',
bounce: false,
attract: {
enable: false,
rotateX: 600,
rotateY: 1200,
},
},
},
interactivity: {
detect_on: 'canvas',
events: {
onhover: {
enable: true,
mode: 'grab',
},
onclick: {
enable: true,
mode: 'push',
},
resize: true,
},
modes: {
grab: {
distance: 140,
line_linked: {
opacity: 1,
},
},
bubble: {
distance: 400,
size: 40,
duration: 2,
opacity: 8,
speed: 2,
},
repulse: {
distance: 200,
duration: 0.4,
},
push: {
particles_nb: 4,
},
remove: {
particles_nb: 2,
},
},
},
retina_detect: true,
};
然后根据自己的需求在这个背景上加上登录页面,
四、给新建的项目搭建Layout布局
1、在src中新建layout文件夹,在layout中新建父组件index.vue,并新增子组件LayoutSider.vue、LayoutMenu.vue、LayoutHeader.vue、LayoutContent.vue文件,data.js存储布局变量
1)、首先在父组件index.vue中构建layout整体结构,引入子组件:
<template>
<Layout class="layout-space">
<!-- 菜单列表 -->
<LayoutSider />
<Layout class="layout-right">
<!-- 头部 -->
<LayoutHeader />
<!-- 内容区 -->
<LayoutContent />
<!-- 底部 -->
<LayoutFooter :style="footerStyle">
{{ t('index.description') }}
</LayoutFooter>
</Layout>
</Layout>
</template>
<script>
import { defineComponent } from 'vue';
import { Layout } from 'ant-design-vue';
import LayoutSider from './components/LayoutSider.vue';
import LayoutHeader from './components/LayoutHeader.vue';
import LayoutContent from './components/LayoutContent.vue';
import t from '/@/lang/t.js';
export default defineComponent({
components: {
Layout,
LayoutSider,
LayoutHeader,
LayoutContent,
LayoutFooter: Layout.Footer,
},
setup() {
const footerStyle = {
textAlign: 'center',
color: '#fff',
padding: 0,
height: 50,
lineHeight: '50px',
backgroundColor: '#7dbcea',
};
return { t, footerStyle };
},
});
</script>
<style lang="less" scoped>
.layout-space {
width: 100%;
height: 100%;
.layout-right {
width: 100%;
::v-deep(.ant-layout-header) {
height: auto;
padding: 0 5px;
}
}
}
</style>
2)、布局整体结构后,首先开始搭建layout左侧菜单栏,引入layout.sider,布局菜单栏整体样式
<template>
<Sider
:style="siderStyle"
:width="200"
class="sider-class"
collapsedWidth="80"
v-model:collapsed="isCollapsed"
:trigger="null"
collapsible
>
<div class="sys-title">
<SvgIcon name="vite" size="22" style="margin-right: 5px" />
{{ !isCollapsed ? sys_name : '' }}
</div>
<LayoutMenu class="layout-menu" />
<div class="sys-title">{{ sys_frame }}</div>
</Sider>
</template>
<script>
import { computed, defineComponent, ref } from 'vue';
import { Layout } from 'ant-design-vue';
import LayoutMenu from './LayoutMenu.vue';
import SvgIcon from '/@/components/SvgIcon/index.vue';
import useMenuSiderStore from '/@/store/module/useMenuSider.js';
export default defineComponent({
components: {
Layout,
LayoutMenu,
SvgIcon,
Sider: Layout.Sider,
},
setup() {
const sys_name = import.meta.env.VITE_SYSTEM_NAME;
const sys_frame = import.meta.env.VITE_SYS_FRAME;
const siderStyle = {
textAlign: 'center',
minHeight: '100vh',
maxHeight: '100vh',
color: '#fff',
backgroundColor: '#3ba0e9',
};
const isCollapsed = computed(() => {
return useMenuSiderStore().getMenuCollapsed;
});
return { siderStyle, sys_name, sys_frame, isCollapsed };
},
});
</script>
<style lang="less" scoped>
.sider-class {
height: 100vh;
overflow: hidden;
::v-deep(.ant-layout-sider-children) {
display: flex;
flex-direction: column;
}
.sys-title {
height: @common-height;
display: flex;
align-items: center;
justify-content: center;
// line-height: @common-height;
}
.layout-menu {
flex: 1;
background: #fff;
color: #000000;
}
}
</style>
3)、在LayoutMenu.vue中开始引入menu菜单,因为目前只需要两层,所以没有递归菜单布局逻辑,引入菜单时需要考虑父子级菜单关系,并且要根据当前地址去展开相对应的父级菜单,
<template>
<div class="menu-list">
<Menu
v-model:openKeys="openKeys"
v-model:selectedKeys="selectedKeys"
class="vue-sys-menu"
style="width: 100%"
mode="inline"
>
<template v-for="item in menuList" :key="item.key">
<template v-if="item.children && item.children.length > 0">
<SubMenu :key="item.key">
<template #icon>
<SvgIcon :name="item.icon" size="17" />
<!-- <AppstoreOutlined /> -->
</template>
<!-- <SvgIcon :name="item.icon" /> -->
<template #title>{{ item.meta.title }}</template>
<MenuItem
v-for="obj in item.children"
:key="obj.key"
:disabled="obj.disabled"
>
<router-link :to="obj.path">
<span>{{ obj.meta.title }}</span>
</router-link>
</MenuItem>
</SubMenu>
</template>
<template v-else>
<MenuItem :key="item.key" :disabled="item.disabled">
<template #icon>
<SvgIcon :name="item.icon" size="17" />
</template>
<router-link :to="item.path">
<span>{{ item.meta.title }}</span>
</router-link>
</MenuItem>
</template>
</template>
</Menu>
</div>
</template>
<script>
import { defineComponent, ref, watch } from 'vue';
import { Menu, SubMenu } from 'ant-design-vue';
import SvgIcon from '/@/components/SvgIcon/index.vue';
import {
MailOutlined,
CalendarOutlined,
AppstoreOutlined,
SettingOutlined,
} from '@ant-design/icons-vue';
import { useRoute } from 'vue-router';
import useMenuSiderStore from '/@/store/module/useMenuSider.js';
import { isArray } from '/@/utils/is';
export default defineComponent({
components: {
Menu,
SubMenu,
MenuItem: Menu.Item,
SvgIcon,
MailOutlined,
CalendarOutlined,
AppstoreOutlined,
SettingOutlined,
},
setup() {
const routes = useRoute();
const useMenuSider = useMenuSiderStore();
const selectedKeys = ref([]);
const openKeys = ref([]);
const collapsed = ref(false);
const menuList = ref(useMenuSider.getRoutesList);
// 监听路由变化
const querySelectedKeys = (path, arrList) => {
if (!isArray(arrList)) return;
arrList.forEach((item) => {
if (item.path == path) {
selectedKeys.value = Array(item.key);
if (item.isHideChildMenu) openKeys.value = [];
} else {
if (item.children && item.children.length > 0) {
const arr = item.children.find((el) => el.path == path);
if (arr) {
selectedKeys.value = Array(arr.key);
openKeys.value = Array(item.key);
}
}
}
});
};
watch(
() => routes.path,
(newValue) => {
querySelectedKeys(newValue, menuList.value);
},
{
immediate: true,
}
);
return { selectedKeys, collapsed, openKeys, menuList };
},
});
</script>
<style lang="less" scoped>
.menu-list {
.vue-sys-menu {
height: 100%;
overflow-y: auto;
overflow-x: hidden;
}
.vue-sys-menu::-webkit-scrollbar {
width: 0;
}
::v-deep(.ant-menu-inline .ant-menu-item) {
margin-top: 0;
}
::v-deep(.ant-menu-inline > .ant-menu-item) {
height: 45px;
line-height: 45px;
}
}
</style>
4)、开始布局项目header组件,这个地方会放入一些基础功能,如头像,中英文切换,全屏等功能,在LayoutHeader.vue组件中,引入layout.header组件
<template>
<Header :style="headerStyle" class="layout-header">
<div class="header-left">
<menu-unfold-outlined
v-if="collapsed"
class="trigger"
@click="handleCollapsed"
/>
<menu-fold-outlined v-else class="trigger" @click="handleCollapsed" />
</div>
<!-- 中间区域 -->
<div style="flex: auto; margin: 0 10px"></div>
<div class="header-right">
<!-- 语种 -->
<Language />
<Dropdown
destroyPopupOnHide
:getPopupContainer="getPopupContainer"
placement="bottom"
>
<div class="header-avatar">
<Avatar :src="avatorImg">
<template #icon><UserOutlined /></template>
</Avatar>
<span class="title">Mamba</span>
</div>
<template #overlay>
<Menu @click="handleItem">
<MenuItem v-for="item in itemList" :key="item.key">
<SvgIcon :name="item.icon" />
{{ item.title }}
</MenuItem>
</Menu>
</template>
</Dropdown>
<ForgetPassword ref="forgetPwd" />
</div>
</Header>
</template>
<script>
import { defineComponent, ref } from 'vue';
import { Layout, Avatar, Dropdown, Menu, Modal } from 'ant-design-vue';
import {
UserOutlined,
MenuUnfoldOutlined,
MenuFoldOutlined,
} from '@ant-design/icons-vue';
import useMenuSiderStore from '/@/store/module/useMenuSider.js';
import SvgIcon from '/@/components/SvgIcon/index.vue';
import Language from './Language.vue';
import avatorImg from '/@/assets/images/avatar.png';
import { itemList } from '../data';
import useStoreOut from '/@/store/module/use.js';
import ForgetPassword from './ForgetPassword.vue';
export default defineComponent({
components: {
Header: Layout.Header,
Avatar,
Language,
UserOutlined,
MenuUnfoldOutlined,
MenuFoldOutlined,
Dropdown,
Menu,
MenuItem: Menu.Item,
SvgIcon,
ForgetPassword,
},
setup() {
const userStore = useStoreOut();
const collapsed = ref(false);
const forgetPwd = ref(null);
const headerStyle = {
height: 50,
lineHeight: '50px',
borderBottom: '1px solid #D9D9D9',
backgroundColor: '#fff',
};
// 菜单收起展开
const handleCollapsed = () => {
collapsed.value = !collapsed.value;
useMenuSiderStore().setMenuCollapsed(collapsed.value);
};
const getPopupContainer = (trigger) => {
return trigger.parentNode ?? document.body;
};
// 点击头像选择功能:修改密码,退出登录
const handleItem = (item) => {
if (item.key == 'loginOut') {
Modal.confirm({
title: '提示',
content: '确定退出登录?',
onOk: async () => {
await userStore.loginOut(true);
},
});
} else {
forgetPwd.value.openModal();
}
};
return {
headerStyle,
collapsed,
avatorImg,
itemList,
forgetPwd,
handleCollapsed,
handleItem,
getPopupContainer,
};
},
});
</script>
<style lang="less" scoped>
.layout-header {
display: flex;
align-items: center;
justify-content: space-between;
.trigger {
font-size: 16px;
color: #666;
cursor: pointer;
transition: color 0.3s;
}
.header-right {
padding: 0 15px;
color: #000000;
display: flex;
.header-avatar {
display: flex;
align-items: center;
cursor: pointer;
.title {
margin-left: 10px;
}
}
::v-deep(.ant-dropdown) {
min-width: 120px !important;
}
}
}
</style>
5)、最后要开始布局内容区,这个区域开发一些实际性的功能,在这个区域中渲染views中的功能性组件,因为实际中需要缓存组件操作,loading页面操作,所以在渲染组件时进行控制
<template>
<div class="vue-sys-content">
<Content class="sys-ant-content">
<Spin
size="large"
:spinning="spinning"
:wrapperClassName="
spinning ? 'no-spin-scroll' : 'no-spin-scroll spin-scroll'
"
>
<router-view v-slot="{ Component }">
<keep-alive>
<component
:is="Component"
v-if="routes.meta.keepAlive"
:key="routes.path"
/>
</keep-alive>
<component
:is="Component"
v-if="!routes.meta.keepAlive"
/> </router-view
></Spin>
</Content>
</div>
</template>
<script>
import { computed, defineComponent, ref } from 'vue';
import { Layout, Spin } from 'ant-design-vue';
import { useRoute } from 'vue-router';
import useStoreOut from '/@/store/module/use';
export default defineComponent({
components: {
Content: Layout.Content,
Spin,
},
setup() {
const routes = useRoute();
const useStore = useStoreOut();
const spinning = computed(() => {
return useStore.getLoadingPage || false;
});
return { routes, spinning };
},
});
</script>
<style lang="less" scoped>
.vue-sys-content {
height: calc(100vh - 101px);
width: 100%;
.sys-ant-content {
height: 100%;
width: 100%;
padding: 12px 0 12px 12px;
overflow-y: scroll;
background: @all-bg-color;
.no-spin-scroll {
overflow: hidden;
height: 100%;
width: 100%;
}
.spin-scroll {
overflow-y: scroll;
}
::v-deep(.ant-spin-nested-loading > div > .ant-spin) {
max-height: none;
}
}
}
</style>
五、项目中引入axios,
1、项目的数据要从接口获取,所以需要在项目中引入axios来获取后端接口数据,如果每个页面都引入,那就代码太冗余,维护起来也很麻烦,所以需要封装单独的请求文件,接下来就在scr文件下新建utils文件夹,在里面新增http文件夹,并新建axios.js和request.js文件,在其中简单封装一个request请求
在axios.js文件中引入axios
import axios from 'axios';
import qs from 'qs';
import useStore from '/@/store/module/use.js';
const service = axios.create({
baseUrl: import.meta.env.VITE_WEB_BASIC_URL_API,
timeout: 60 * 1000,
headers: {
'Content-Type': 'application/json;charset=UTF-8',
},
requestOptions: {
isTransformResponse: false, // 是否返回不处理的响应数据
},
});
// 请求拦截
service.interceptors.request.use(
(config) => {
const userStore = useStore();
const token = userStore.getLoginToken;
// const contentType =
// config.headers?.['Content-Type'] || config.headers?.['content-type'];
if (token) {
config.headers = {
...config.headers,
Authorization: `Bearer ${userStore.getLoginToken}`,
};
}
return { ...config };
},
(error) => {
return Promise.reject(error);
}
);
// 响应拦截
service.interceptors.response.use(
(response) => {
const data = response;
const options = response.config?.requestOptions;
if (data.status === 200) {
const res = data?.data;
if (!options.isTransformResponse && res.code == '0000') {
return res.data;
} else {
return res;
}
} else if (data.status === 401) {
return Promise.reject(new Error('认证失败!'));
} else if (data.status == 403) {
return Promise.reject(new Error('请求被拒绝!'));
} else if (data.status == 404) {
return Promise.reject(new Error('请求不存在!'));
} else if (data.status == 500) {
return Promise.reject(new Error('服务器错误!'));
} else if (data.status == 500) {
return Promise.reject(new Error('服务器系统繁忙!'));
}
return Promise.reject(new Error('network err!'));
},
(err) => {
return Promise.reject(err.response);
}
);
export default service;
在request.js文件中:
import service from './axios';
class requestHttp {
constructor() {}
get(url, data, config) {
return this.request('GET', url, { params: data }, config);
}
post(url, data, config) {
return this.request('POST', url, { data }, config);
}
put(url, data, config) {
return this.request('PUT', url, { data }, config);
}
delete(url, data, config) {
return this.request('DELETE', url, { params: data }, config);
}
request(method = 'GET', url, data, config) {
return new Promise((resolve, reject) => {
service({ method, url, ...data, ...config })
.then((res) => {
try {
resolve(res);
} catch (err) {
reject(err || new Error('request error!'));
}
})
.catch((e) => reject(e || new Error('request error!')));
});
}
}
export default new requestHttp();
六、项目中引入mockJs插件,
1、上面封装了简单的request请求文件,因为在项目中经常会和后端同时开发,这个时候没有数据,所以需要mockJs来模拟接口返回的数据就显得很重要了,如果引入mockjs,怎么使用呢,首先在根目录下我们要新建一个mock文件夹,在里面放入我们的模拟数据
使用命令引入mockjs插件
yarn add mockjs --save
在我们的配置文件vite.config.js中引入mockjs,
2、这样我们就可以在mock文件夹里新增模拟数据了,比如我新建了use.js和menu.js模拟数据,来渲染项目中用户信息和菜单信息
use.js文件
import { loginTimeout } from '../error';
export default [
{
url: '/basic-api/login',
method: 'post',
timeout: 2000,
statusCode: 200,
response: ({ body }) => {
if (body.usename == 'admin' && body.password == 'admin@123') {
return {
code: '0000',
message: 'ok',
status: true,
data: {
token: 'success_login_1234567898765432',
},
};
} else {
return {
code: '4002',
status: false,
message: body.usename != 'admin' ? '用户名错误' : '用户密码错误',
};
}
},
},
{
url: '/basic-api/getUsers',
method: 'get',
timeout: 500,
statusCode: 200,
response: ({ headers }) => {
if (headers.authorization && headers.authorization.split('Bearer')[1]) {
return {
code: '0000',
message: 'ok',
status: true,
data: {
name: 'New Trainee',
avator: '',
loginTime: formatDateTime(new Date()),
role: 'manager',
userId: null,
// 'rows|10': [
// {
// id: '@guid',
// name: '@cname',
// 'age|20-30': 23,
// 'job|1': ['前端工程师', '后端工程师', 'UI工程师', '需求工程师'],
// },
// ],
},
};
} else {
return {
...loginTimeout,
};
}
},
},
{
url: '/basic-api/loginOut',
method: 'get',
timeout: 3000,
statusCode: 200,
response: ({ headers }) => {
if (headers.authorization && headers.authorization.split('Bearer')[1]) {
return {
code: '0000',
message: 'ok',
status: true,
data: null,
};
} else {
return {
...loginTimeout,
};
}
},
},
];
function formatDateTime(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
menu.js文件
import { loginTimeout } from '../error';
export default [
{
url: '/basic-api/getMenu',
methods: 'get',
timeout: 100,
statusCode: 200,
response: ({ headers }) => {
if (headers.authorization && headers.authorization.split('Bearer')[1]) {
return {
code: '0000',
message: 'ok',
status: true,
data: [
{
key: '1',
sort: 1,
parentKey: '0',
name: 'Dashboard',
title: '首页看板',
icon: 'home',
path: '/dashboard',
disabled: false,
component: 'views/dashboard/index',
redirect: null,
keepAlive: false,
children: null,
},
{
key: '2',
sort: 3,
parentKey: '0',
name: 'Person',
title: '人员管理',
path: '/person',
icon: 'person',
children: null,
disabled: false,
component: 'views/person/index',
redirect: null,
keepAlive: false,
},
{
key: '3',
sort: 3,
parentKey: '0',
name: 'Package',
title: '组件',
icon: 'manage',
path: '/package',
disabled: false,
component: 'Layout',
redirect: null,
keepAlive: false,
children: [
{
key: '31',
sort: '31',
parentKey: '3',
name: 'Form',
title: 'Form',
icon: '',
path: '/form',
disabled: false,
component: 'views/package/form/index',
redirect: null,
keepAlive: false,
children: null,
},
{
key: '32',
sort: '32',
parentKey: '3',
name: 'Table',
title: 'Table',
icon: '',
path: '/table',
disabled: false,
component: 'views/package/table/index',
redirect: null,
keepAlive: false,
children: null,
},
],
},
{
key: '4',
sort: 4,
parentKey: '0',
name: 'menuManage',
title: '菜单管理',
path: '/menu-manage',
icon: 'menu',
children: null,
disabled: false,
component: 'views/menuManage/index',
redirect: null,
keepAlive: false,
},
],
};
} else {
return {
...loginTimeout,
};
}
},
},
];
项目中怎么使用请求mock数据呢,这就可以使用我们上面封装的request文件了
七、怎么根据mock数据动态的控制菜单呢,
1、上面我们封装了layout布局,这个时候需要一个菜单数据来渲染菜单列表,如何来封装根据上面定义好的模拟菜单数据来进行页面跳转渲染开发呢。首先需要在store中新建一个模块useMenuSider.js文件,这个里面放入我们的封装方法,来出来接口返回的数据,或者mock返回的数据
useMenuSider.js=====>>
import { defineStore } from 'pinia';
import { getMenuList } from '/@/api/sys/user';
import { isArray } from '/@/utils/is';
import { message } from 'ant-design-vue';
const useMenuSiderStore = defineStore('useMenuSider', {
state: () => ({
collapsed: false, // menu collapsed
menuList: [], // menu list
}),
getters: {
getMenuCollapsed() {
return this.collapsed;
},
getRoutesList() {
return this.menuList;
},
},
actions: {
setMenuCollapsed(flag) {
this.collapsed = flag;
},
setRoutesList(list) {
this.menuList = list || [];
// setLocCache(cacheKey.USER_MENU_LIST, list);
},
/**
* @description build menu routes
*/
async buildMenuRoutes(unload) {
let routes = [];
let routeList = [];
if (unload) {
message.loading({
content: '菜单加载中。。。',
key: 'updatable',
});
}
try {
const data = await getMenuList();
routeList = await this.handleMenuList(data);
routeList = await this.convertChildPath(routeList);
if (routeList && routeList.length > 1) {
const routeRediect = {
path: '/',
redirect: routeList[0].path,
name: 'Layout',
meta: {
title: 'Redirect',
},
component: () => import('/@/layout/index.vue'),
children: [...routeList],
};
routes = [routeRediect];
this.setRoutesList(routeList);
// setLocCache(cacheKey.USER_MENU_LIST, routeList);
} else {
routes = [];
}
} catch (error) {
console.error(error);
}
return routes;
},
// handle menu list
async handleMenuList(menuLists) {
if (!isArray(menuLists)) return [];
const list = await Promise.all(
menuLists.map(async (item) => {
const route = {
key: item.key,
parentKey: item.parentKey,
component:
item.component != 'Layout'
? await this.tranformToRoute(item.component)
: null,
sort: item.sort,
path: item.path,
name: item.name,
redirect: item.redirect,
icon: item.icon,
disabled: item.disabled,
isHideChildMenu: item?.isHideChildMenu || true, // 收起当前菜单外的其他展开菜单列表
meta: {
title: item.title,
keepAlive: item.keepAlive,
},
children: [],
};
if (item.children && item.children.length > 0) {
route.children = await this.handleMenuList(item.children);
}
return route;
})
);
return list;
},
// Convert component
async tranformToRoute(component) {
const list = import.meta.glob('../../views/**/*.{vue,jsx}');
const routeKeys = Object.keys(list);
const mateKey = routeKeys.filter((item) => {
const _r = item.replace('../..', '');
const startSign = component.startsWith('/');
const endSign =
component.endsWith('.vue') || component.endsWith('.jsx');
const startIndex = startSign ? 0 : 1;
const endIndex = endSign ? _r.length : _r.lastIndexOf('.');
return _r.substring(startIndex, endIndex) === component;
});
if (mateKey?.length == 1) {
return list[mateKey[0]];
} else if (mateKey?.length > 1) {
console.warn(
'Please do not create `.vue` and `.JSX` files with the same file name in the same hierarchical directory under the views folder. This will cause dynamic introduction failure'
);
return;
} else {
console.warn(
'在src/views/下找不到`' +
component +
'.vue` 或 `' +
component +
'.jsx`, 请自行创建!'
);
return;
}
},
// convert children path
convertChildPath(list) {
if (!isArray(list)) return [];
const routerMap = list.map((item) => {
if (item.children && item.children.length) {
item.children = item.children.map((route) => {
route.path = `${item.path || ''}${
route.path
? route.path[0] == '/'
? route.path
: '/' + route.path
: ''
}`;
route.path =
route.path.indexOf('//') != -1
? route.path.replace(/\/\//g, '/')
: route.path;
route.path = route.path[0] == '/' ? route.path : '/' + route.path;
return route;
});
item.redirect = item.children[0].path;
}
return item;
});
return routerMap;
},
},
});
export default useMenuSiderStore;
2、这个时候需要在登录成功之后来调用用户信息接口和菜单接口,来布局整体数据结构,新增一个use.js文件,来处理我们登录之后的逻辑
user.js=====>
import { defineStore } from 'pinia';
import { login, getUserInfo, loginOut } from '/@/api/sys/user';
import router from '/@/router';
import { pagePath, cacheKey } from '/@/common/constant';
import useMenuSiderStore from './useMenuSider';
import { getLocCache, setLocCache } from '/@/utils/localCache';
const useStoreOut = defineStore('user', {
state: () => ({
token: '',
pageLoading: false,
userInfo: {},
}),
getters: {
getLoginToken() {
return this.token || getLocCache(cacheKey.LOGIN_TOKEN);
},
getUserInfo() {
return this.userInfo || getLocCache(cacheKey.USER_INFO);
},
getLoadingPage() {
return this.pageLoading;
},
},
actions: {
setLoginToken(data) {
this.token = data || '';
setLocCache(cacheKey.LOGIN_TOKEN, data);
},
setUserInfo(info) {
this.userInfo = info || {};
setLocCache(cacheKey.USER_INFO, info);
},
setLoadingPage(flag) {
this.pageLoading = flag;
},
/**
* @description: 登录
*/
async login(data) {
try {
const res = await login(data);
const { token = '' } = res;
this.setLoginToken(token);
setLocCache(cacheKey.LOGIN_TOKEN, token);
await this.afterLoginAction(true);
} catch (error) {
Promise.reject(error);
}
},
/**
* @description: 获取用户信息
*/
async afterLoginAction(goHome = true) {
const useMenuSider = useMenuSiderStore();
try {
if (!this.getLoginToken) return;
const userInfo = await getUserInfo();
this.setUserInfo(userInfo);
setLocCache(cacheKey.USER_INFO, userInfo);
const routes = await useMenuSider.buildMenuRoutes(goHome);
routes.forEach((route) => {
router.addRoute(route);
});
goHome && router.replace(pagePath.BASE_HOME);
} catch (error) {
Promise.reject(error);
}
},
/**
* @description: 登出,清空缓存
*/
async loginOut(goLogin = false) {
if (this.getLoginToken) {
try {
await loginOut();
} catch {
Promise.reject('注销Token失败');
}
}
this.setLoginToken('');
this.setUserInfo(null);
localStorage.clear();
sessionStorage.clear();
goLogin && router.replace(pagePath.BASE_LOGIN);
},
},
});
export default useStoreOut;
八、如何封装一个svg组件
1、因为我项目中使用了svg格式的图片,所以封装了一个svg使用组件,在项目的componetsz文件夹下新增一个SvgIcon文件夹,在里面新建index.vue文件,因为我们一般会把svg图片放入项目中,然后在代码里使用,如何在配置文件中引入,然后我们直接使用名字就可以预览展示
2、在SvgIcon/index.vue文件中封装svg展示
<template>
<svg
aria-hidden="true"
class="svg-icon"
:style="{ width: `${props.size}px`, height: `${props.size}px` }"
>
<use :xlink:href="symbolId" :fill="props.color" />
</svg>
</template>
<script>
import { defineComponent, ref, computed, onMounted } from 'vue';
export default defineComponent({
props: {
prefix: {
default: 'icon',
},
name: {
required: false,
default: 'fixPwd',
},
size: {
default: '20',
},
color: {
default: () => '',
},
},
setup(props) {
const symbolId = computed(() => {
return `#${props.prefix}-${props.name}`;
});
onMounted(() => {
});
return { props, symbolId };
},
});
</script>
<style lang="less" scoped>
.svg-icon {
vertical-align: -5px;
overflow: hidden;
fill: currentColor;
margin-right: 2px;
}
</style>
3、最重要一点是要在我们的main.js中引入下面一行代码,来使我们成功预览svg格式的图片
import 'virtual:svg-icons-register'
总结:
上面就是如何封装一个简易的灵活的pc系统,代码仅供参考,具体的业务逻辑场景需各自进行补充