vue3 + ts + pinia + antd 搭建后台

339 阅读1分钟

pinia.jpeg

前言

公司项目做一个后台系统,在网上找了一圈模板,最后还是决定自己做。

线上demo : Ds-Admin admin 123456

github: GitHub - guanshundai/vue3-ts-admin

这里只展示部分代码, 感兴趣的去看源码吧

开始

npm init vue@latest

选 jsx、ts、pinia、router 其他看你自己

yarn add less less-loader
yarn add ant-design-vue
yarn add unplugin-vue-components //按需引入

vite.config.ts

import { fileURLToPath, URL } from 'url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import Components from 'unplugin-vue-components/vite';
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers';

// https://vitejs.dev/config/
export default defineConfig({
  css: {
    preprocessorOptions: {
      less: {
        javascriptEnabled: true
      },
    },
  },
  plugins: [vue(), vueJsx(),Components({
    resolvers: [AntDesignVueResolver()],
  }),],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  },
  server: {
    host: '0.0.0.0',
    port: 9900,
    open: true
  }
})

main.ts

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPluginPersist from 'pinia-plugin-persist' //持久化状态管理
import 'ant-design-vue/dist/antd.less';
import '@/assets/base.less'
import defineComponents from '@/utils/defineComponents'  //全局注册自定义组件

import App from './App.vue'
import router from './router'
import { useStore } from './stores/useStore';

const pinia = createPinia()
const app = createApp(App)

app.use(router)
app.use(pinia.use(piniaPluginPersist))

app.use(defineComponents)

router.beforeEach((to) => {
  const { token } = useStore(pinia)
  if (to.path !== '/login' && !token) return '/login'
})

app.mount('#app')

App.vue

<template>
  <a-spin :spinning="spinning">
    <router-view v-if="isRefresh"></router-view>
  </a-spin>
</template>

<script setup lang='ts'>
import { ref, provide, nextTick } from 'vue'

const isRefresh = ref<boolean>(true)
const spinning = ref<boolean>(false)
let timer: any = null

//刷新
const refresh = async () => {
  isRefresh.value = false;
  spinning.value = true
  await nextTick()
  isRefresh.value = true;

//loding效果
  timer = setTimeout(() => {
    spinning.value = false
    timer = null
  }, 500)
};

provide('refresh', refresh)
</script>

<style scoped lang='less'>
</style>

router/index.ts

import { createRouter, createWebHistory } from 'vue-router'
import Layout from '../views/index.vue'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      name: 'home',
      redirect: {name: 'option1'},
      component: Layout,
      children: [
        {
          path: 'option1',
          name: 'option1',
          component: () => import('@/views/Home/Option1.vue')
        },
        {
          path: 'option2',
          name: 'option2',
          component: () => import('@/views/Home/Option2.vue')
        },
      ]
    },
    {
      path: '/nav2',
      name: 'nav2',
      redirect: { name: 'nav2Option1' },
      component: Layout,
      children: [
        {
          path: 'option1',
          name: 'nav2Option1',
          component: () => import('@/views/Nav2/option1.vue')
        },
        {
          path: 'option2',
          name: 'nav2Option2',
          component: () => import('@/views/Nav2/option2.vue')
        }
      ]
    },
    {
      path: '/login',
      name: 'login',
      component: () => import('@/views/Login/index.vue')
    },
  ]
})

export default router

view/index.vue

<template>
  <a-layout style="min-height: 100vh">
    <Sider :select="select" :sideKey="sideKey" />
    <a-layout>
      <Header @getKey="getKey" />
      <Content />
      <Footer />
    </a-layout>
  </a-layout>
</template>

<script lang="ts" setup>
import { ref, onMounted } from 'vue'
import { useStore } from '@/stores/useStore'

const { navKey } = useStore()

const select = ref<string>('nav1')
const sideKey = ref<string[]>(['1'])

onMounted(() => {
  select.value = navKey.join('')
})

const getKey = (key: string, key2: string[]) => {
  select.value = key
  sideKey.value = key2
}
</script>
<style scoped lang="less">
.site-layout .site-layout-background {
  background: #fff;
}

[data-theme='dark'] .site-layout .site-layout-background {
  background: #141414;
}
</style>

Header.vue

<template>
  <a-layout-header class="header flex-r-jb">
    <a-menu v-model:selectedKeys="selectedKeys" @click="getKey" theme="white" mode="horizontal">
      <a-menu-item key="nav1" @click="$router.push('/')">nav 1</a-menu-item>
      <a-menu-item key="nav2" @click="$router.push('/nav2/option1')">nav 2</a-menu-item>
      <a-menu-item key="nav3">nav 3</a-menu-item>
    </a-menu>
    <div class="flex-r-c">
      <redo-outlined style="font-size: 22px;" @click="refresh" />
      <fullscreen-outlined @click="fullScreen" style="font-size: 22px;margin-left: 15px;" v-if="!isFull" />
      <fullscreen-exit-outlined @click="cancelScreen" style="font-size: 22px;margin-left: 15px;" v-else />
      <a-popover :title="'Hello ' + username + ' !'" placement="bottomRight" autoAdjustOverflow>
        <template #content>
          <a>
            <p>Profile</p>
          </a>
          <a>
            <p>Settings</p>
          </a>
          <a @click="logOut">
            <p>Log out</p>
          </a>
        </template>
        <a-avatar shape="square" :src="logo" :size="42" style="margin-left: 15px;"></a-avatar>
      </a-popover>
    </div>
  </a-layout-header>
</template>

<script setup lang='ts'>
import { ref, onMounted, inject } from 'vue'
import { FullscreenOutlined, FullscreenExitOutlined, RedoOutlined } from '@ant-design/icons-vue';
import logo from '@/assets/jingyu.png'
import { useRouter } from 'vue-router';
import { useStore } from '@/stores/useStore'

const router = useRouter()

const { navKey, changeNavKey, changeSideKey, username } = useStore()

const emit = defineEmits<{ (event: 'getKey', key: string, sideKey: string[]): void }>()

const selectedKeys = ref<string[]>(['nav1'])
const isFull = ref<boolean>(false)

const fresh = inject<any>('refresh')

onMounted(() => {
  selectedKeys.value = navKey
})

const getKey = ({ key }: { key: string }) => {
  emit('getKey', key, ['1'])
  changeSideKey(['1'])
  changeNavKey([key])
}

const refresh = () => {
  fresh()
}
const fullScreen = () => {
  isFull.value = true
  document.documentElement.requestFullscreen();
}
const cancelScreen = () => {
  isFull.value = false
  document.exitFullscreen()
}

const logOut = () => {
  sessionStorage.clear()
  router.push('/login')
}
</script>

<style scoped lang='less'>
.header {
  background: #fff;
  padding: 0 15px;
}

.ant-layout-header {
  :deep(.ant-menu-item) {
    padding: 0 30px;
  }
}
</style>

Sider.vue

<template>
  <a-layout-sider v-model:collapsed="collapsed" collapsible>
    <div class="flex-r-c" style="width: 100%; padding: 10px; color: #fff; font-size: 20px">
      <div class="logo"></div>
      <Transition name="slide-fade">
        <span v-if="!collapsed">AutoFeed</span>
      </Transition>
    </div>
    <a-menu v-model:selectedKeys="selectedKeys" v-model:openKeys="openKeys" theme="dark" mode="inline"
      @click="getOpens">
      <component :is="
        select === 'nav1'
          ? 'List1'
          : select === 'nav2'
            ? 'List2'
            : select === 'nav3'
              ? 'List3'
              : 'List1'
      ">
      </component>
    </a-menu>
  </a-layout-sider>
</template>

<script setup lang="ts">
import { ref, watch, onMounted } from "vue";

import { useStore } from "@/stores/useStore";

const { sideKey, changeSideKey } = useStore();

const props = defineProps<{
  select: string;
  sideKey: string[];
}>();

const collapsed = ref<boolean>(false);
const selectedKeys = ref<string[]>(props.sideKey);
const openKeys = ref<string[]>(props.sideKey);
const select = ref<string>(props.select);

watch(
  () => props.select,
  () => (select.value = props.select)
);
watch(
  () => props.sideKey,
  () => {
    selectedKeys.value = props.sideKey;
    openKeys.value = props.sideKey;
  }
);

onMounted(() => {
  selectedKeys.value = sideKey;
  openKeys.value = sideKey;
});

const getOpens = ({ keyPath }: { keyPath: string[] }) => {
  openKeys.value = keyPath;
  changeSideKey(keyPath);
};
</script>

<style scoped lang="less">
.logo {
  width: 50px;
  height: 50px;
  margin-right: 10px;
  background-image: url("@/assets/jingyu.png");
  background-size: auto 100%;
  background-repeat: no-repeat;
}

.slide-fade-enter-active {
  transition: all 1s ease-out;
}

.slide-fade-enter-from,
.slide-fade-leave-to {
  transform: translateX(20px);
  opacity: 0;
}
</style>

Content.vue

<template>
  <a-layout-content class="content">
    <router-view />
  </a-layout-content>
</template>

<script setup lang='ts'>
</script>

<style scoped lang='less'>
.content {
  margin: 10px 16px 0 16px;
  height: 100%;
  background-color: #fff;
  overflow: auto;
  padding: 15px;
}
</style>

自定义组件在defineComponents.ts中做了集中处理

import type { App } from "vue";

import Header from "@/components/Layout/Header.vue";
import Sider from "@/components/Layout/Sider.vue";
import Content from "@/components/Layout/Content.vue";
import Footer from "@/components/Layout/Footer.vue";
import List1 from "@/components/SideList/List1.vue";
import List2 from "@/components/SideList/List2.vue";
import List3 from "@/components/SideList/List3.vue";

export default (app: App) => {
  app
    .component("Header", Header)
    .component("Sider", Sider)
    .component("Content", Content)
    .component("Footer", Footer)
    .component('List1', List1)
    .component('List2', List2)
    .component('List3', List3)
};

main.js中use一下

完整版: GitHub - guanshundai/vue3-ts-admin