如何快速搭建一个数据灵活,可直接拿来使用的vue3+pc系统项目(详细)

前言

在开发过程中,总会碰到很多的项目中的数据取决于接口,根据接口的数据来进行渲染,就比如项目中的菜单,已不再单单的写死的前端项目里,所以在搭建项目的时候如何快速搭建一个根据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系统,代码仅供参考,具体的业务逻辑场景需各自进行补充

  • 24
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
搭建一个使用vue3+ts+vite+uniapp的微信小程序的步骤如下: 1. 首先安装最新版的Node.js和npm。 2. 安装uni-app-cli脚手架工具,命令如下: ``` npm install -g @vue/cli npm install -g @vue/cli-init npm install -g @dcloudio/uni-cli ``` 3. 创建一个uni-app项目,命令如下: ``` vue create -p dcloudio/uni-preset-vue my-project ``` 4. 进入项目目录,安装依赖包,命令如下: ``` cd my-project npm install ``` 5. 安装并配置微信小程序开发者工具,下载地址:https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html 6. 在微信小程序开发者工具中,选择导入uni-app项目,选择项目目录下的dist/dev/mp-weixin文件夹,导入后即可进行开发和调试。 7. 如果需要使用vue3和typescript,在项目中安装相关依赖包,命令如下: ``` npm install --save-dev vue@next @vue/compiler-sfc typescript ts-loader ``` 8. 在项目根目录下创建vue.config.js文件,配置如下: ``` module.exports = { chainWebpack: config => { config.module .rule('ts') .use('ts-loader') .loader('ts-loader') .tap(options => { options.appendTsSuffixTo = [/\.vue$/] return options }) } } ``` 9. 在src目录下创建shims-vue.d.ts文件,内容如下: ``` declare module "*.vue" { import { ComponentOptions } from "vue"; const component: ComponentOptions; export default component; } ``` 10. 现在你就可以使用vue3和typescript进行开发了。同时,如果需要使用vite进行开发,可以参考uni-app官方文档进行配置:https://uniapp.dcloud.io/collocation/vite 以上就是使用vue3+ts+vite+uniapp搭建微信小程序的步骤。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值