Monorepo项目管理,vue3+ts+antd+pinia后台管理模板搭建记录(四)

493 阅读4分钟

写在开头

  1. 鸽了挺久的,期间在学其他的东西。这周把基本功能都完善了
  2. 内容包括mock,国际化,Layout, 配置生产和开发
  3. 后续可能还会更新一章,加入一些常用页面的组件和模板生成之类的,也可能彻底咕咕了。

正文

mock配置

安装依赖

pnpm i mockjs vite-plugin-mock -D

引入依赖

import { viteMockServe } from 'vite-plugin-mock';
...
  plugins: [
    ...
    viteMockServe({
      mockPath: './src/mock',
      supportTs: true,
      watchFiles: true
    })
  ]

mockPath可以指定引入mock接口的位置。暂时是放在了project2/src/mock文件夹下。但其实使用pnpm多项目的话放在外层比较好,可以两个平台共用。这次更新我也是在packages下建了一个public文件夹,用于放置不需要打包的公共文件。

user.ts mock接口的数据,暂时没有用到mock.js里的一些方法,都是写死的数据。

export default [
  {
    url: '/login',
    method: 'post',
    response: () => ({
      code: 200,
      message: 'ok',
      data: 'admin-token'
    })
  },
  {
    url: '/user',
    method: 'get',
    response: () => ({
      code: 200,
      message: 'ok',
      data: {
        roles: ['admin'],
        userName: 'MOmo'
      }
    })
  },
  {
    url: '/routes',
    methods: 'get',
    response: () => ({
      code: 200,
      message: 'ok',
      data: [
        {
          path: '/test',
          name: 'testP',
          meta: {
            title: 'page.test.test'
          },
          children: [
            {
              path: '',
              name: 'test',
              component: 'test',
              meta: {
                title: 'page.test.test'
              }
            }
          ]
        }
      ]
    })
  }
];

baseURL为'/',就可以访问mock接口了。

国际化

主要分两个部分,antd自带的国际化和vue-i18n处理页面中的硬编码。

首先是antd的国际化

<script setup lang="ts">
import { Locale } from 'ant-design-vue/es/locale-provider';
import zhCN from 'ant-design-vue/es/locale/zh_CN';
import enUS from 'ant-design-vue/es/locale/en_US';
import { ref, toRef, watch } from 'vue';
import dayjs from 'dayjs';
import 'dayjs/locale/zh-cn';
import 'dayjs/locale/en';
interface Props {
  locale?: string;
}

interface Config {
  antd: Locale;
  dayjs: string;
}

interface Configs {
  zhCN: Config;
  enUS: Config;
}

const configs: Configs = {
  zhCN: {
    antd: zhCN,
    dayjs: 'zh-cn'
  },
  enUS: {
    antd: enUS,
    dayjs: 'en'
  }
};

const props = withDefaults(defineProps<Props>(), {
  locale: 'zhCN'
});
const locale = toRef(props, 'locale');
const currentConfig = configs[locale.value as keyof Configs];
const currentAntd = ref<Locale>(currentConfig.antd);
dayjs.locale(currentConfig.dayjs);

watch(locale, (newLocale) => {
  const tempConfig = configs[newLocale as keyof Configs];
  dayjs.locale(tempConfig.dayjs);
  currentAntd.value = tempConfig.antd;
});
</script>

<template>
  <a-config-provider :locale="currentAntd">
    <router-view />
  </a-config-provider>
</template>

<style scoped></style>

用a-config-provider包裹全局,引入antd的语言包。

  1. a-config-provider不仅能配置国际化,还可以配置主题切换等等,可以在antd文档上查看详细
  2. antd的时间类组件是需要引入其他包的,这边引入了dayjs

vue-i18n国际化

import { createI18n } from 'vue-i18n';
import zh from './zh.json';
import en from './en.json';

const i18n = createI18n({
  locale: 'zhCN',
  globalInjection: true,
  messages: {
    enUS: en,
    zhCN: zh
  }
});

export default i18n;

在public/i18n下导出一个i18n实例,

...
import i18n from '@public/i18n';

app.use(i18n);
...

在main.ts中引入i18n实例

<template>
  {{ $t('page.login') }}
</template>

在vue文件中的使用方法,用$t函数,不需要额外引入包,然后参数是一个字符串,对应配置的json文件里的对象,如

{
  "page": {
    "login": "登录"
  }
}

在ts文件中的使用方法

import i18n from '@public/i18n';

i18n.global.t('page.login')

引入i18n实例然后用global.t方法,和$t用法一致

切换语言封装

import { defineStore } from 'pinia';
import i18n from '@public/i18n';
import { I18ns } from '@/interfaces/i18n';

interface I18nStore {
  locale: I18ns;
}

const i18nStore = defineStore('i18n', {
  state: (): I18nStore => ({
    locale: 'zhCN'
  }),
  actions: {
    setLocale(locale: I18ns) {
      this.locale = locale;
      i18n.global.locale = locale;
    }
  }
});

export default i18nStore;

在store中封装了一个i18nStore,然后用locale做全局的标识,setLocale来切换语言状态

Layout

image.png

PS

目前菜单只做了最高两级

左侧菜单栏

<script setup lang="ts">
import { ref } from 'vue';
import { useRouter, useRoute } from 'vue-router';

const router = useRouter();
const routes = router.getRoutes();
const route = useRoute();
const path = route.path;
// 获取最高级菜单
const parentsRoutes = routes.filter((item) => item.components?.default?.name === 'Layout');
parentsRoutes.sort((a, b) => {
  const aIndex = (a.meta.index ?? Infinity) as number;
  const bIndex = (b.meta.index ?? Infinity) as number;
  return aIndex - bIndex;
});

const selectedKeys = ref<string[]>([]);
const openKeys = ref<string[]>([]);

const selectRoute = routes.find((item) => item.path === path);
selectedKeys.value = [selectRoute?.name as string];

const selectParentRoute = routes.find((item) => item.path === `/${path.split('/').slice(1)[0]}`);
openKeys.value = [selectParentRoute?.name as string];
</script>
  1. 做了路由过滤成菜单和菜单排序
  2. 做了刷新页面后还是有菜单选中初始化

面包屑

<template>
  <a-breadcrumb style="margin-bottom: 5px; height: 20px">
    <a-breadcrumb-item v-for="title in titles" :key="title">{{ $t(title) }}</a-breadcrumb-item>
  </a-breadcrumb>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import { useRouter, useRoute } from 'vue-router';

const router = useRouter();
const routes = router.getRoutes();
const route = useRoute();
const titles = ref<string[]>([]);

const computedTitles = (path: string) => {
  const paths = path.split('/').slice(1);
  const tempTitles: string[] = [];
  tempTitles.push(routes.find((item) => item.path === `/${paths[0]}`)?.meta?.title as string);
  if (paths.length === 2) {
    tempTitles.push(routes.find((item) => item.path === path)?.meta?.title as string);
  }
  titles.value = tempTitles;
};
computedTitles(route.path);
watch(route, (newRoute) => {
  computedTitles(newRoute.path);
});
</script>
  1. 做了面包屑的title计算,在路由里的title格式也是按照i18n的格式来的,所以用了$t
  2. 目前没有做面包屑的点击跳转上一级

顶部navbar

目前就做了退出登录和国际化切换

开发和生成配置

import { defineConfig, loadEnv } from 'vite';
export default defineConfig(({ mode }) => {
  const env = loadEnv(mode, process.cwd());
  ...
}

在vite.config.ts中引入loadEnv,就可以使用.env文件中的配置参数

  1. mode的值为package.json中脚本vite --port 9527 --mode development的--mode的值
  2. 寻找的文件为.env.{mode}
  3. .env中的参数一定要为VITE_开头

在项目中使用

import.meta.env.VITE_NODE_ENV

这样直接引入VITE_NODE_ENV的值

解决ts验证报错需要在env.d.ts文件中声明ImportMetaEnv,如下

interface ImportMetaEnv {
  VITE_NODE_ENV: string;
  VITE_BASE_URL: string;
}

其他改动

这次改动也是改了一下其他地方,修缮了一下,解决了一些bug。

路由

项目中路由直接结合了两种模式,后台获取和权限过滤,这store/modules/permission.ts中可以看到

async getAsyncRoute(): Promise<RouteBase[]> {
  const asyncServerRoutes = initAsyncRoutes(await getRoutes());
  const store = userStore();
  const { getUserInfo } = store;
  let { roles } = store;
  if (!roles.length) {
    roles = await getUserInfo();
  }
  const roleRoutes = filterRoutes(asyncRoutes, roles);
  asyncServerRoutes.push(...roleRoutes);
  asyncServerRoutes.push({
    path: '/:pathMatch(.*)',
    redirect: '404'
  } as RouteBase);
  this.asyncRoutes = asyncServerRoutes;
  return asyncServerRoutes;
},

不仅getRoutes从后台获取了路由,而且还在userStore中拿到了roles然后进行了路由过滤。 正常开发中只需要一种模式,把另一种删掉就好了。

后端获取路由有一个比较恶心的地方就是需要在一个文件中把页面文件全部引入,如store/option.ts

export const routeComponents = {
  test: import('@/views/test/index.vue')
};

App.vue

App.vue拖到了public/components下,因为加了antd的国际化,所以变得比较复杂,可以用来项目通用。

eslint和prettier配置

稍微改动了一下,之前好像有一些bug导致没有提示。

写在最后

  1. 这个版本是比较基础的后台整体架子都有了。
  2. gitee项目地址,这次是主分支,后面如果有更新应该是修复bug和一些页面的更新,但是页面的更新会放在project1里面,project2中会保留这个简单的架子,方便即拿即用。所以不单独开分支了
  3. 只做了一些简单的测试,可能会有一些bug。欢迎指正。