从0到一个前端后台管理框架(五)

上一篇博客中我们实现了Tab栏和面包屑,但发现目前功能还十分简陋,很多功能并没有完善,于是我们这篇博客来完善一些项目

完善目标

1, 打开外部链接,以及在系统内部打开外部链接。
2, 切换页面使用使用动画,以及保留页面滚动高度。
3, 侧边菜单根据路由高亮。
4, 完善首页。

打开外部链接,以及在系统内部打开外部链接。

思路分析

简单来说就是内嵌一个iframe标签,但是要和系统做到兼容,还是需要好好考虑的,

考虑的点

  • 路由的参数传递。
  • 正常页面的切换,以及同为外链页面的切换。
  • 三方网页的加载动画。

编写代码

编辑my-router.ts文件,增加菜单

{
  id: '1-3',
  pid: '1',
  name: '百度',
  path: 'https://www.baidu.com/',
  icon: 'person',
  sort: 3,
},

增加过后,刷新重新登陆,菜单便加载出来,点击发现,跳转到404页面了。查看浏览器路径http://localhost:8080/#/https://www.baidu.com/我们也就知道为什么跳转到404页面了,因为并没有绑定https://www.baidu.com/这个路由,对于三方链接,是没有办法绑定固定路由的,也不需要绑定固定路由,只需要把这个三方链接当一个参数传递给一个固定展示三方链接内容的组件即可。说干就干。

在pages目录下新建OutsidePage.vue组件,用来展示三方链接页面,编写下面代码

<template>
  <q-page class="full-height">
    <!-- 代码很复杂,不用慌,一步一步往下看,这个标签就是简单的iframe标签 
    frameborder  iframe页面边框宽度,这边设置为零。
    @load  三方链接加载完成,会调用此方法。
    :src  这个不解释
    -->
    <iframe
      frameborder="0"
      @load="loading = false"
      :src="url"
      class="fit absolute"
    ></iframe>
<!-- 如果三方链接没加载完,展示这个加载中的标签,
默认覆盖上一级全屏,简单理解这个就是加载动画 -->
    <q-inner-loading
      :showing="loading"
      label="正在加载..."
      label-class="text-primary"
      label-style="font-size: 1.1em"
    />
  </q-page>
</template>

<script setup lang="ts">
import { ref, onActivated, onDeactivated } from 'vue';
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router';
interface Props {
  // 外链地址
  url: string;
}
/* 这个props是通过路由传递的,具体代码,要看编写路由的部分代码,先保持疑惑。 */
withDefaults(defineProps<Props>(), {});

/* 加载动画展示状态 */
const loading = ref(true);
/* 一个定时器 */
let setTime: NodeJS.Timeout;

/* 路由更新钩子 */
onBeforeRouteUpdate(() => {
  loading.value = true;
  setTime = setTimeout(() => {
    loading.value = false;
  }, 5000);
});
/* 离开路由钩子 */
onBeforeRouteLeave(() => {
  loading.value = false;
  clearTimeout(setTime);
});
/* 激活钩子 */
onActivated(() => {
  loading.value = true;
  setTime = setTimeout(() => {
    loading.value = false;
  }, 5000);
});
/* 暂存钩子 */
onDeactivated(() => {
  loading.value = false;
  clearTimeout(setTime);
});
</script>

<style lang="scss" scoped></style>

代码中的钩子函数,是为了在各种场景下能有一致的表现。比如三方页面之间切换场景,定时器是为了防止一些三方页面加载时间过长,而使页面一直处于加载中状态。

修改MenuItem.vue组件

  • 修改handleClick函数使其兼容外链点击
    • 修改base.d.ts中MenuItem类型定义,增加isBlank?: boolean;字段;
    • 修改my-router.ts文件增加一个百度-外路由信息。
    • 修改MenuItem.vue中的handleClick函数使其适应三方页面的链接
    • 最后修改routes.ts文件增加外链的路由信息重要

编辑完成后的base.d.ts文件


declare namespace WJ {
  /* 菜单项 */
  interface MenuItem {
    // 菜单ID
    id: string;
    // 菜单父级Id根节点使用"0"
    pid: string;
    // 菜单名称
    name: string;
    // 菜单路径
    path: string;
    // 菜单图标
    icon: string;
    // 菜单权限编码
    auth: string;
    // 排序
    sort: number;
    // 是否新页面打开链接,
    isBlank?: boolean;
    // 子级别菜单
    children?: Array<WJ.MenuItem>;
  }
}

在my-router.ts文件中增加一条路由信息


{
  id: '1-4',
  pid: '1',
  name: '百度-外',
  path: 'https://www.baidu.com/',
  icon: 'person',
  isBlank: true,
  sort: 3,
},

编辑过后的MenuItem.vue文件,重点查看handleClick函数

<template>
  <template v-for="item in menuTree" :key="item.id">
    <q-item
      v-if="!item.children"
      @click="handleClick(item)"
      clickable
    >
      <q-item-section avatar>
        <q-icon :name="item.icon" />
      </q-item-section>
      <q-item-section>
        <q-item-label>{{ item.name }}</q-item-label>
      </q-item-section>
    </q-item>
    <q-expansion-item
      :icon="item.icon"
      :label="item.name"
      :content-inset-level="0.2"
      v-else
    >
      <MenuItem :menu-tree="item.children"></MenuItem>
    </q-expansion-item>
  </template>
</template>

<script setup lang="ts">
import { useRouter } from 'vue-router';
import { openURL } from 'quasar';
import { useLayoutStore } from 'src/stores/layout-store';

interface Props {
  // 菜单树
  menuTree: Array<WJ.MenuItem>;
}
withDefaults(defineProps<Props>(), {});
const layout = useLayoutStore();
const router = useRouter();

const handleClick = (item: WJ.MenuItem) => {
  if (item.isBlank) {
    if (layout.isHttp(item.path)) {
      openURL(item.path);
    } else {
      openURL(router.resolve({ path: item.path }).href);
    }
  } else {
    router.push(layout.toBind(item));
  }
};
</script>

<style scoped></style>

最后修改routes.ts文件增加外链的路由信息

import { RouteRecordRaw } from 'vue-router';

const routes: RouteRecordRaw[] = [
  {
    path: '/login',
    component: () => import('pages/LoginPage.vue'),
  },
  {
    path: '/',
    component: () => import('layouts/MainLayout.vue'),
    children: [
      { path: '', component: () => import('pages/IndexPage.vue') },
      { path: '/sys-menu', component: () => import('pages/IndexPage.vue') },
      { path: '/sys-user', component: () => import('pages/IndexPage.vue') },
      /* 这里是新加的,注意后面的写法,这是核心 */
      { path: '/outside-link', component: () => import('pages/OutsidePage.vue'), props: route => ({ url: route.query.url }) },
    ],
  },

  // Always leave this as last one,
  // but you can also remove it
  {
    path: '/:catchAll(.*)*',
    component: () => import('pages/ErrorNotFound.vue'),
  },
];

export default routes;

到这里外链打开相关的代码就已经编写完成。重启项目查看效果。这时候我们看见貌似有些问题原来是百度主页不给内嵌网页展示,嘚,换成Vue官方文档得了。

修改my-router.ts文件,修改过后代码

export default [
  {
    id: '1',
    pid: '0',
    name: '系统管理',
    path: '',
    icon: 'settings_applications',
    sort: 0,
  },
  {
    id: '1-1',
    pid: '1',
    name: '菜单管理',
    path: '/sys-menu',
    icon: 'menu',
    sort: 1,
  },
  {
    id: '1-2',
    pid: '1',
    name: '用户管理',
    path: '/sys-user',
    icon: 'person',
    sort: 2,
  },

  {
    id: '1-3',
    pid: '1',
    name: 'Vue官网',
    path: 'https://cn.vuejs.org/',
    icon: 'person',
    sort: 3,
  },
  {
    id: '1-4',
    pid: '1',
    name: 'Vue官网-外',
    path: 'https://cn.vuejs.org/',
    icon: 'person',
    isBlank: true,
    sort: 3,
  },
]

刷新页面,再次查看,已经完成。

例子

实现侧边菜单根据路由高亮

思路分析

猛地一想并不复杂嗷,使用router获取当前路由然后根据菜单的path属性进行比对即可。这里会出现一个问题就是无法高亮父级菜单,不完美。

完美应该怎么解决呢,这里可以用面包屑的信息判断当前菜单是否高亮,因为面包屑是包含所有层级菜单,思路有了,代码编写就很简单了。

编写代码

修改MenuItem.vue文件,修改过后完整代码如下

<template>
  <template v-for="item in menuTree" :key="item.id">
    <q-item
      v-if="!item.children"
      :active="activeStatus(item)"
      @click="handleClick(item)"
      clickable
    >
      <q-item-section avatar>
        <q-icon :name="item.icon" />
      </q-item-section>
      <q-item-section>
        <q-item-label>{{ item.name }}</q-item-label>
      </q-item-section>
    </q-item>
    <q-expansion-item
      :header-class="{
        'text-primary': activeStatus(item),
      }"
      :icon="item.icon"
      :label="item.name"
      :content-inset-level="0.2"
      v-else
    >
      <MenuItem :menu-tree="item.children"></MenuItem>
    </q-expansion-item>
  </template>
</template>

<script setup lang="ts">
import { useRouter } from 'vue-router';
import { openURL } from 'quasar';
import { useLayoutStore } from 'src/stores/layout-store';

interface Props {
  // 菜单树
  menuTree: Array<WJ.MenuItem>;
}
withDefaults(defineProps<Props>(), {});
const layout = useLayoutStore();
const router = useRouter();

/* 注意这里是更新的代码 */
const activeStatus = (item: WJ.MenuItem) => {
  return layout.crumbsList.filter((i) => i.id === item.id).length > 0;
};

const handleClick = (item: WJ.MenuItem) => {
  if (item.isBlank) {
    if (layout.isHttp(item.path)) {
      openURL(item.path);
    } else {
      openURL(router.resolve({ path: item.path }).href);
    }
  } else {
    router.push(layout.toBind(item));
  }
};
</script>

<style scoped></style>

菜单高亮已经完成,那么只剩下最后的保留页面滚动高度了

切换页面添加动画,以及保留页面滚动高度

思路分析

切页动画,这个需要使用Vue的transition组件,其实这个组件我们已经用过不止一次了,可以查看Tab和面包屑组件代码,其中使用了此组件。

如果我们把页面滚动高度也视作为一种状态,那么是不是就可以很好的理解,页面的滚动高度就是一种状态,我只需要把页面的状态缓存起来,那么对应也就保存了页面对应的滚动高度。这里用到了Vue自带的keep-alive组件。如何让Vue缓存路由对应的页面信息呢,且看代码。

编写代码

增加切页动画

修改MailLayout.vue代码,修改过后完整代码,注意看下代码中的注释

<template>
  <q-layout view="hHh LpR lff" class="fit">
    <q-header bordered height-hint="48">
      <q-toolbar>
        <q-btn
          dense
          flat
          round
          :icon="leftDrawerOpen ? 'menu_open' : 'menu'"
          @click="toggleLeftDrawer"
        />
        <q-btn flat no-caps no-wrap class="q-ml-xs" v-if="$q.screen.gt.xs">
          <q-icon name="catching_pokemon" size="2rem" />
          <q-toolbar-title shrink class="text-weight-bold">
            W_LF
          </q-toolbar-title>
        </q-btn>
        <head-crumbs v-if="$q.screen.gt.xs"></head-crumbs>
        <q-space />
        <div class="q-gutter-sm row items-center no-wrap">
          <q-btn dense flat round icon="refresh" @click="refreshPage">
            <q-tooltip> 重载页面 </q-tooltip>
          </q-btn>
          <q-btn
            round
            dense
            flat
            color="white"
            :icon="$q.fullscreen.isActive ? 'fullscreen_exit' : 'fullscreen'"
            @click="$q.fullscreen.toggle()"
            v-if="$q.screen.gt.sm"
          >
          </q-btn>
          <q-btn
            v-if="$q.screen.gt.sm"
            round
            dense
            flat
            color="white"
            icon="fab fa-github"
            type="a"
            href="https://gitee.com/wlf213/wlf-admin"
            target="_blank"
          >
          </q-btn>
          <q-btn
            v-if="$q.screen.gt.sm"
            round
            dense
            flat
            class="text-red"
            type="a"
            href="https://gitee.com/wlf213"
            target="_blank"
          >
            <i class="fa fa-heart fa-2x fa-beat"></i>
          </q-btn>
          <q-btn round dense flat color="white" icon="notifications">
            <q-badge color="red" text-color="white" floating> 5 </q-badge>
            <q-menu>
              <q-list style="min-width: 100px">
                <q-card class="text-center no-shadow no-border">
                  <q-btn
                    label="全部查看"
                    style="max-width: 120px !important"
                    flat
                    dense
                    class="text-indigo-8"
                  ></q-btn>
                </q-card>
              </q-list>
            </q-menu>
          </q-btn>
          <q-btn round flat>
            <q-avatar size="26px">
              <img
                src="https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fc-ssl.duitang.com%2Fuploads%2Fblog%2F202107%2F19%2F20210719150601_4401e.thumb.1000_0.jpg&refer=http%3A%2F%2Fc-ssl.duitang.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1685108172&t=232426938cbc64554c6c143fdb13fb4c"
              />
            </q-avatar>
            <q-menu transition-show="scale" transition-hide="scale">
              <q-list dense>
                <q-item v-ripple>
                  <q-item-section avatar>
                    <q-avatar>
                      <img
                        src="https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fc-ssl.duitang.com%2Fuploads%2Fblog%2F202107%2F19%2F20210719150601_4401e.thumb.1000_0.jpg&refer=http%3A%2F%2Fc-ssl.duitang.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1685108172&t=232426938cbc64554c6c143fdb13fb4c"
                      />
                    </q-avatar>
                  </q-item-section>
                  <q-item-section>你好, W</q-item-section>
                </q-item>
                <q-item clickable v-close-popup @click="onenProfile">
                  <q-item-section class="text-center">个人主页</q-item-section>
                </q-item>
                <q-item clickable v-close-popup @click="logout">
                  <q-item-section class="text-center">退出登录</q-item-section>
                </q-item>
              </q-list>
            </q-menu>
          </q-btn>
        </div>

        <q-btn dense flat round icon="menu" @click="toggleRightDrawer" />
      </q-toolbar>

      <head-tab></head-tab>
    </q-header>

    <q-drawer
      show-if-above
      v-model="leftDrawerOpen"
      side="left"
      bordered
      :width="layout.leftDrawerWeight"
    >
      <left-drawer></left-drawer>
    </q-drawer>

    <q-drawer v-model="rightDrawerOpen" side="right" overlay elevated>
      <!-- drawer content -->
    </q-drawer>

    <q-page-container class="fit">
        <!-- 注意这里,就是核心 -->
      <router-view v-slot="{ Component }" v-if="isRouterAlive">
        <transition
          mode="out-in"
          enter-active-class="animated fadeIn"
          leave-active-class="animated fadeOut"
        >
          <!-- 最多缓存20个页面-->
          <keep-alive :max="20">
            <component :is="Component" />
          </keep-alive>
        </transition>
      </router-view>
      <q-inner-loading
        :showing="!isRouterAlive"
        label-class="text-primary"
        label="载入中..."
        label-style="font-size: 1.1em"
      />
    </q-page-container>
  </q-layout>
</template>

<script setup lang="ts">
import { ref, nextTick, defineAsyncComponent } from 'vue';
import { useRouter } from 'vue-router';
import { useQuasar } from 'quasar';
import { useAuthStore, useLayoutStore } from 'src/stores/layout-store';
/* 这里通通使用异步的方式加载组件,缩小打包大小,提升首屏加载页面 */
const HeadTab = defineAsyncComponent(() => import('./componet/HeadTab.vue'));
const HeadCrumbs = defineAsyncComponent(
  () => import('./componet/HeadCrumbs.vue')
);
const LeftDrawer = defineAsyncComponent(
  () => import('./componet/LeftDrawer.vue')
);
const leftDrawerOpen = ref(false);
const rightDrawerOpen = ref(false);
const router = useRouter();
const auth = useAuthStore();
const layout = useLayoutStore();
const $q = useQuasar();
const isRouterAlive = ref(true);
const toggleLeftDrawer = () => {
  leftDrawerOpen.value = !leftDrawerOpen.value;
};

const toggleRightDrawer = () => {
  rightDrawerOpen.value = !rightDrawerOpen.value;
};

const refreshPage = () => {
  isRouterAlive.value = false;
  setTimeout(() => {
    nextTick(() => {
      isRouterAlive.value = true;
    });
  }, 500);
};

const logout = () => {
  auth.token = '';
  router.push('/login');
};

const onenProfile = () => {
    //
};
</script>

<style lang="scss" scoped></style>

仔细看代码的应该发现好像不止增加了切页动画功能,好像夹带了私货,😊😊😊,核心还是切页动画,私货啥的,只是一些小小的优化,或者说是一些细节,比如页面刷新功能。

到这里页面切换动画是就是以及实现了,但是实际操作发现好像动画并未生效,主要是因为每个页面都一样,导致不容易发现且也动画导致的。修改不同路由对应的页面即可。

保留页面滚动条高度

  • 在pages下新增组件MenuPage.vueUserPage.vue组件, 为了演示滚动条滚动。
  • 修改routes.ts文件内容

MenuPage.vue中代码

<template>
  <q-page :style-fn="styFn">
    <q-scroll-area :style="{ height: pageHeight + 'px' }">
      菜单管理
      <div v-for="i in 50" :key="i">菜单管理--{{ i }}</div>
    </q-scroll-area>
  </q-page>
</template>

<script setup lang="ts">
import { ref } from 'vue';
const pageHeight = ref(400);
const styFn = (offset: number, height: number) => {
  pageHeight.value = height - offset;
};
</script>

UserPage.vue中代码

<template>
  <q-page :style-fn="styFn">
    <q-scroll-area :style="{ height: pageHeight + 'px' }">
      用户管理
      <div v-for="i in 50" :key="i">菜单管理--{{ i }}</div>
    </q-scroll-area>
  </q-page>
</template>

<script setup lang="ts">
import { ref } from 'vue';
const pageHeight = ref(400);
const styFn = (offset: number, height: number) => {
  pageHeight.value = height - offset;
};
</script>

routes.ts修改过后完整代码

import { RouteRecordRaw } from 'vue-router';

const routes: RouteRecordRaw[] = [
  {
    path: '/login',
    component: () => import('pages/LoginPage.vue'),
  },
  {
    path: '/',
    component: () => import('layouts/MainLayout.vue'),
    children: [
      { path: '', component: () => import('pages/IndexPage.vue') },
      { path: '/sys-menu', component: () => import('pages/MenuPage.vue') },
      { path: '/sys-user', component: () => import('pages/UserPage.vue') },
      { path: '/outside-link', component: () => import('pages/OutsidePage.vue'), props: route => ({ url: route.query.url }) },
    ],
  },

  // Always leave this as last one,
  // but you can also remove it
  {
    path: '/:catchAll(.*)*',
    component: () => import('pages/ErrorNotFound.vue'),
  },
];

export default routes;

编写完成,重启项目,切换用户管理和菜单管理页面,发现滚动条高度是能够被保存的,至此保存滚动条高度功能已经实现。

内容只是作为演示,实际使用中,内容标签应该写在q-scroll-area标签之中,以实现对滚动高度的保存。

到这框架的核心内容就已经介绍完了,如需继续管理,可关注wlf-admin项目,此项目为此教程的完整版,此教程,只简单介绍了后台管理框架较为核心的内容,组件展示,以及布局样例,都在完整版项目中。下面的代码,取自完整项目中

简单编辑首页

首先编辑package.json文件,编辑过后代码如下

{
  "name": "wlf-admin-study",
  "version": "0.0.1",
  "description": "A Quasar Project",
  "productName": "Wlf-admin",
  "author": "wlf",
  "private": true,
  "scripts": {
    "dev": "quasar dev",
    "build": "quasar build",
    "lint": "eslint --ext .js,.ts,.vue ./",
    "format": "prettier --write \"**/*.{js,ts,vue,scss,html,md,json}\" --ignore-path .gitignore",
    "test": "echo \"No test specified\" && exit 0"
  },
  "dependencies": {
    "axios": "^1.2.1",
    "vue-i18n": "^9.2.2",
    "pinia": "^2.0.11",
    "@quasar/extras": "^1.0.0",
    "core-js": "^3.6.5",
    "quasar": "^2.6.0",
    "vue": "^3.0.0",
    "vue-router": "^4.0.0",
      // 添加这两个依赖
    "echarts": "^5.4.2",
    "vue-echarts": "^6.5.5",
  },
  "devDependencies": {
    "@typescript-eslint/eslint-plugin": "^5.10.0",
    "@typescript-eslint/parser": "^5.10.0",
    "eslint": "^8.10.0",
    "eslint-plugin-vue": "^9.0.0",
    "eslint-config-prettier": "^8.1.0",
    "prettier": "^2.5.1",
    "@types/node": "^12.20.21",
    "@quasar/app-webpack": "^3.0.0"
  },
  "browserslist": [
    "last 10 Chrome versions",
    "last 10 Firefox versions",
    "last 4 Edge versions",
    "last 7 Safari versions",
    "last 8 Android versions",
    "last 8 ChromeAndroid versions",
    "last 8 FirefoxAndroid versions",
    "last 10 iOS versions",
    "last 5 Opera versions"
  ],
  "engines": {
    "node": ">= 12.22.1",
    "npm": ">= 6.13.4",
    "yarn": ">= 1.21.1"
  }
}

增加组件

按路径增加组件

  • src\components\cards\CardCharts2.vue
  • src\components\cards\CardSocial.vue
  • src\components\tabs\TabSocial.vue

CardCharts2.vue代码内容

<template>
  <q-card class="q-mt-sm no-shadow" bordered>
    <q-card-section class="text-h6 q-pb-none">
      <q-item>
        <q-item-section avatar class="">
          <q-icon color="blue" name="fas fa-chart-line" size="44px" />
        </q-item-section>

        <q-item-section>
          <div class="text-h6">销售记录</div>
        </q-item-section>
      </q-item>
    </q-card-section>
    <q-card-section class="row">
      <div class="col-lg-7 col-sm-12 col-xs-12 col-md-7">
        <div class="row">
          <div class="col-lg-3 col-md-3 col-xs-6 col-sm-6">
            <q-item>
              <q-item-section top avatar>
                <q-avatar color="blue" text-color="white" icon="bluetooth" />
              </q-item-section>
              <q-item-section>
                <q-item-label class="text-h6 text-blue text-bold"
                  >4321</q-item-label
                >
                <q-item-label caption>时尚</q-item-label>
              </q-item-section>
            </q-item>
          </div>
          <div class="col-lg-3 col-md-3 col-xs-6 col-sm-6">
            <q-item>
              <q-item-section top avatar>
                <q-avatar color="grey-8" text-color="white" icon="bluetooth" />
              </q-item-section>
              <q-item-section>
                <q-item-label class="text-h6 text-grey-8 text-bold"
                  >9876</q-item-label
                >
                <q-item-label caption>电子</q-item-label>
              </q-item-section>
            </q-item>
          </div>
          <div class="col-lg-3 col-md-3 col-xs-6 col-sm-6">
            <q-item>
              <q-item-section top avatar>
                <q-avatar color="green-6" text-color="white" icon="bluetooth" />
              </q-item-section>
              <q-item-section>
                <q-item-label class="text-h6 text-green-6 text-bold"
                  >345</q-item-label
                >
                <q-item-label caption>男装</q-item-label>
              </q-item-section>
            </q-item>
          </div>
          <div class="col-lg-3 col-md-3 col-xs-6 col-sm-6">
            <q-item>
              <q-item-section top avatar>
                <q-avatar
                  color="orange-8"
                  text-color="white"
                  icon="bluetooth"
                />
              </q-item-section>
              <q-item-section>
                <q-item-label class="text-h6 text-orange-8 text-bold"
                  >1021</q-item-label
                >
                <q-item-label caption>证书</q-item-label>
              </q-item-section>
            </q-item>
          </div>
        </div>
        <div>
          <VChart
            :option="sales_options"
            class="q-mt-md"
            :resizable="true"
            autoresize
            style="height: 250px"
          />
        </div>
      </div>
      <div class="col-lg-5 col-sm-12 col-xs-12 col-md-5">
        <q-item>
          <q-item-section avatar class="">
            <q-icon
              color="blue"
              name="fas fa-gift"
              class="q-pl-md"
              size="24px"
            />
          </q-item-section>

          <q-item-section>
            <div class="text-h6">今日</div>
          </q-item-section>
        </q-item>
        <div>
          <VChart
            :option="pie_options"
            class="q-mt-md"
            :resizable="true"
            autoresize
            style="height: 250px"
          />
        </div>
      </div>
    </q-card-section>
  </q-card>
</template>

<script setup lang="ts">
import 'echarts';
import VChart from 'vue-echarts';
const sales_options = {
  tooltip: {
    trigger: 'axis',
    axisPointer: {
      // Coordinate axis indicator, coordinate axis trigger is valid
      type: 'shadow', // The default is a straight line, optional:'line' |'shadow'
    },
  },
  grid: {
    left: '2%',
    right: '2%',
    top: '4%',
    bottom: '3%',
    containLabel: true,
  },
  xAxis: [
    {
      type: 'category',
      data: [
        '一月',
        '二月',
        '三月',
        '四月',
        '五月',
        '六月',
        '七月',
        '八月',
        '九月',
        '十月',
        '十一月',
        '十二月',
      ],
    },
  ],
  yAxis: [
    {
      type: 'value',
      splitLine: {
        show: false,
      },
    },
  ],
  series: [
    {
      name: '时尚',
      type: 'bar',
      data: [40, 45, 27, 50, 32, 50, 70, 30, 30, 40, 67, 29],
      color: '#546bfa',
    },
    {
      name: '电子',
      type: 'bar',
      data: [124, 100, 20, 120, 117, 70, 110, 90, 50, 90, 20, 50],
      color: '#3a9688',
    },
    {
      name: '男装',
      type: 'bar',
      data: [17, 2, 0, 29, 20, 10, 23, 0, 8, 20, 11, 30],
      color: '#02a9f4',
    },
    {
      name: '证书',
      type: 'bar',
      data: [20, 100, 80, 14, 90, 86, 100, 70, 120, 50, 30, 60],
      color: '#f88c2b',
    },
  ],
};
const pie_options = {
  tooltip: {
    trigger: 'item',
    formatter: '{a} <br/>{b}: {c} ({d}%)',
  },
  legend: {
    bottom: 10,
    left: 'center',
    data: ['时尚', '电子', '男装', '证书'],
  },
  series: [
    {
      name: '销售额',
      type: 'pie',
      radius: ['50%', '70%'],
      avoidLabelOverlap: false,
      label: {
        show: false,
        position: 'center',
      },
      emphasis: {
        label: {
          show: false,
          fontSize: '30',
          fontWeight: 'bold',
        },
      },
      labelLine: {
        show: false,
      },
      data: [
        {
          value: 335,
          name: '时尚',
          itemStyle: {
            color: '#546bfa',
          },
        },
        {
          value: 310,
          name: '电子',
          itemStyle: {
            color: '#3a9688',
          },
        },
        {
          value: 234,
          name: '男装',
          itemStyle: {
            color: '#02a9f4',
          },
        },
        {
          value: 135,
          name: '证书',
          itemStyle: {
            color: '#f88c2b',
          },
        },
      ],
    },
  ],
};
</script>

<style scoped></style>

CardSocial.vue代码内容

<template>
  <q-card class="bg-transparent no-shadow no-border" bordered>
    <q-card-section class="q-pa-none">
      <div class="row q-col-gutter-sm">
        <div
          v-for="(item, index) in items"
          :key="index"
          class="col-md-3 col-sm-12 col-xs-12"
        >
          <q-item :style="`background-color: ${item.color1}`" class="q-pa-none">
            <q-item-section
              v-if="icon_position === 'left'"
              side
              :style="`background-color: ${item.color2}`"
              class="q-pa-lg q-mr-none text-white"
            >
              <q-icon :name="item.icon" color="white" size="24px"></q-icon>
            </q-item-section>
            <q-item-section class="q-pa-md q-ml-none text-white">
              <q-item-label class="text-white text-h6 text-weight-bolder">{{
                item.value
              }}</q-item-label>
              <q-item-label>{{ item.title }}</q-item-label>
            </q-item-section>
            <q-item-section
              v-if="icon_position === 'right'"
              side
              class="q-mr-md text-white"
            >
              <q-icon :name="item.icon" color="white" size="44px"></q-icon>
            </q-item-section>
          </q-item>
        </div>
      </div>
    </q-card-section>
  </q-card>
</template>

<script setup lang="ts">
import { computed } from 'vue';
interface Props {
  // 菜单定位
  icon_position: string;
}
const props = withDefaults(defineProps<Props>(), {
  icon_position: 'left',
});

const items = computed(() => {
  return props.icon_position === 'left'
    ? [
        {
          title: '我的账户',
          icon: 'person',
          value: '200',
          color1: '#5064b5',
          color2: '#3e51b5',
        },
        {
          title: '我的关注',
          icon: 'fab fa-twitter',
          value: '500',
          color1: '#f37169',
          color2: '#f34636',
        },
        {
          title: '粉丝数',
          icon: 'fab fa-google',
          value: '50',
          color1: '#ea6a7f',
          color2: '#ea4b64',
        },
        {
          title: '网站访问统计',
          icon: 'bar_chart',
          value: '1020',
          color1: '#a270b1',
          color2: '#9f52b1',
        },
      ]
    : [
        {
          title: '每月收入',
          icon: 'fas fa-dollar-sign',
          value: '$ 20k',
          color1: '#546bfa',
          color2: '#3e51b5',
        },
        {
          title: '每周销售',
          icon: 'fas fa-chart-bar',
          value: '20',
          color1: '#3a9688',
          color2: '#3e51b5',
        },
        {
          title: '新客户',
          icon: 'fas fa-chart-line',
          value: '321',
          color1: '#7cb342',
          color2: '#3e51b5',
        },
        {
          title: '活跃用户',
          icon: 'person',
          value: '82',
          color1: '#f88c2b',
          color2: '#3e51b5',
        },
      ];
});
</script>

TabSocial.vue代码内容

<template>
  <div class="col-lg-6 col-md-6 col-sm-12 col-xs-12">
    <q-card class="fit no-shadow" bordered>
      <q-tabs
        v-model="tab"
        dense
        class="text-grey"
        active-color="primary"
        indicator-color="primary"
        align="justify"
      >
        <q-tab
          name="contact"
          :class="tab == 'contact' ? 'text-blue' : ''"
          icon="contacts"
          label="联系人"
        />
        <q-tab
          name="message"
          :class="tab == 'message' ? 'text-blue' : ''"
          icon="comment"
          label="消息"
        >
          <q-badge color="red" floating>{{ messages.length }}</q-badge>
        </q-tab>
        <q-tab
          name="notification"
          :class="tab == 'notification' ? 'text-blue' : ''"
          icon="notifications"
          label="通知"
        >
          <q-badge color="red" floating>4</q-badge>
        </q-tab>
      </q-tabs>

      <q-separator />

      <q-tab-panels v-model="tab" animated>
        <q-tab-panel name="contact" class="q-pa-sm">
          <q-list class="rounded-borders" separator>
            <q-item
              v-for="(contact, index) in contacts"
              :key="index"
              clickable
              v-ripple
            >
              <q-item-section avatar>
                <q-avatar>
                  <img :src="contact.avatar" />
                </q-avatar>
              </q-item-section>

              <q-item-section>
                <q-item-label lines="1">{{ contact.name }}</q-item-label>
                <q-item-label caption lines="2">
                  <span class="text-weight-bold">{{ contact.position }}</span>
                </q-item-label>
              </q-item-section>

              <q-item-section side>
                <div class="text-grey-8 q-gutter-xs">
                  <q-btn
                    class="gt-xs"
                    size="md"
                    flat
                    color="blue"
                    dense
                    round
                    icon="comment"
                  />
                  <q-btn
                    class="gt-xs"
                    size="md"
                    flat
                    color="red"
                    dense
                    round
                    icon="email"
                  />
                  <q-btn
                    size="md"
                    flat
                    dense
                    round
                    color="green"
                    icon="phone"
                  />
                </div>
              </q-item-section>
            </q-item>
          </q-list>
        </q-tab-panel>

        <q-tab-panel name="message" class="q-pa-sm">
          <q-item v-for="msg in messages" :key="msg.id" clickable v-ripple>
            <q-item-section avatar>
              <q-avatar>
                <img :src="msg.avatar" />
              </q-avatar>
            </q-item-section>

            <q-item-section>
              <q-item-label>{{ msg.name }}</q-item-label>
              <q-item-label caption lines="1">{{ msg.msg }}</q-item-label>
            </q-item-section>

            <q-item-section side>
              {{ msg.time }}
            </q-item-section>
          </q-item>
        </q-tab-panel>

        <q-tab-panel name="notification" class="q-pa-sm">
          <q-list>
            <q-item clickable v-ripple>
              <q-item-section avatar>
                <q-avatar color="teal" text-color="white" icon="info" />
              </q-item-section>

              <q-item-section>图标</q-item-section>
            </q-item>
            <q-item clickable v-ripple>
              <q-item-section avatar>
                <q-avatar color="teal" text-color="white" icon="report" />
              </q-item-section>

              <q-item-section>图标</q-item-section>
            </q-item>
            <q-item clickable v-ripple>
              <q-item-section avatar>
                <q-avatar color="teal" text-color="white" icon="remove" />
              </q-item-section>

              <q-item-section>图标</q-item-section>
            </q-item>

            <q-item clickable v-ripple>
              <q-item-section avatar>
                <q-avatar
                  color="teal"
                  text-color="white"
                  icon="remove_circle_outline"
                />
              </q-item-section>

              <q-item-section>图标</q-item-section>
            </q-item>
          </q-list>
        </q-tab-panel>
      </q-tab-panels>
    </q-card>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';
const tab = ref('contact');
import { contacts, messages } from 'pages/data/list';
</script>

增加文件

按此路径新建文件src\pages\data\list.ts




export const baseImg =
  'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fsafe-img.xhscdn.com%2Fbw1%2F4d2a8885-131d-4530-835a-0ee12ae4201b%3FimageView2%2F2%2Fw%2F1080%2Fformat%2Fjpg&refer=http%3A%2F%2Fsafe-img.xhscdn.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1686664782&t=8789789109da9319243a2ea7ffe4d1fe';
export const baseImg2 =
  'https://img2.baidu.com/it/u=473659940,2616707866&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500';

export const baseImg3 =
  'https://img0.baidu.com/it/u=2837801980,2735196303&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500';


export const messages = [
  {
    id: 5,
    name: '小红',
    msg: ' 如果小狗',
    avatar: baseImg2,
    time: '10:42 PM',
  },
  {
    id: 6,
    name: '小白',
    msg: ' 如果小狗',
    avatar: baseImg2,
    time: '11:17 AM',
  },
  {
    id: 1,
    name: '孩子',
    msg: ' 如果小狗',
    avatar: baseImg2,
    time: '5:17 AM',
  },
  {
    id: 2,
    name: '小杰',
    msg: ' 如果小狗',
    avatar: baseImg2,
    time: '5:17 AM',
  },
  {
    id: 3,
    name: '小红',
    msg: ' 如果小狗',
    avatar: baseImg2,
    time: '5:17 AM',
  },
];
export const contacts = [
  {
    name: '小P',
    position: '开发',
    avatar: baseImg2,
  },
  {
    name: 'R先生',
    position: '开发',
    avatar: baseImg2,
  },
  {
    name: '小杰',
    position: '开发',
    avatar: baseImg2,
  },
  {
    name: 'B友',
    position: '管理员',
    avatar: baseImg2,
  },
  {
    name: '老哥',
    position: '管理员',
    avatar: baseImg2,
  },
];

export const sales_data = [
  {
    name: '小红',
    Progress: 70,
    status: '关闭',
    stock: '14 / 30',
    date: '2018年10月23日',
    avatar: baseImg,
    product_name: '包包',
    total: '300.00',
    code: 'QWE123',
    prod_img: baseImg,
  },
  {
    name: '小黑',
    Progress: 60,
    status: '发送',
    date: '2018年11月11日',
    stock: '25 / 70',
    avatar: baseImg,
    product_name: '电脑',
    total: '230,00',
    code: 'ABC890',
    prod_img: baseImg,
  },
  {
    name: '小刘',
    Progress: 30,
    status: '等待',
    stock: '35 / 50',
    avatar: baseImg,
    product_name: 'PS5',
    total: '34,00',
    date: '19 Sept 2020',
    code: 'GHI556',
    prod_img: baseImg,
  },
  {
    name: '小杰',
    Progress: 100,
    status: '已付费',
    stock: '18 / 33',
    avatar: baseImg,
    product_name: 'SW游戏机',
    total: '208,00',
    date: '2020年9月19日',
    code: 'JKL345',
    prod_img: baseImg,
  },
];

首页代码

<template>
  <q-page :style-fn="styFn">
    <q-scroll-area :style="{ height: pageHeight + 'px' }">
      <div class="q-pa-sm">
        <card-social icon_position="left" />
        <q-separator spaced inset vertical dark />
        <card-social icon_position="right" />
        <card-charts2></card-charts2>
        <q-card class="q-mt-sm no-shadow" bordered>
          <q-card-section class="text-h6 q-pb-none">
            <q-item>
              <q-item-section avatar class="">
                <q-icon color="blue" name="fa fa-shopping-cart" size="44px" />
              </q-item-section>
              <q-item-section>
                <q-item-label>
                  <div class="text-h6">最近</div>
                </q-item-label>
                <q-item-label caption class="text-black"> 详情 </q-item-label>
              </q-item-section>
            </q-item>
          </q-card-section>
          <q-card-section class="q-pa-none q-ma-none">
            <q-scroll-area style="height: 300px">
              <q-table
                class="no-shadow no-border"
                :rows="sales_data"
                :columns="sales_column"
                hide-bottom
              >
                <template v-slot:body-cell-Products="props">
                  <q-td :props="props">
                    <q-item>
                      <q-item-section>
                        <q-avatar square>
                          <img :src="props.row.prod_img" />
                        </q-avatar>
                      </q-item-section>

                      <q-item-section>
                        <q-item-label>{{ props.row.code }}</q-item-label>
                        <q-item-label>{{
                          props.row.product_name
                        }}</q-item-label>
                      </q-item-section>
                    </q-item>
                  </q-td>
                </template>
                <template v-slot:body-cell-Name="props">
                  <q-td :props="props">
                    <q-item>
                      <q-item-section avatar>
                        <q-avatar>
                          <img :src="props.row.avatar" />
                        </q-avatar>
                      </q-item-section>

                      <q-item-section>
                        <q-item-label>{{ props.row.name }}</q-item-label>
                        <q-item-label caption class=""
                          >购买日期: <br />{{ props.row.date }}</q-item-label
                        >
                      </q-item-section>
                    </q-item>
                  </q-td>
                </template>
                <template v-slot:body-cell-Status="props">
                  <q-td :props="props" class="text-left">
                    <q-chip
                      class="text-white text-capitalize"
                      :label="props.row.status"
                      :color="getChipColor(props.row.status)"
                    ></q-chip>
                  </q-td>
                </template>
                <template v-slot:body-cell-Stock="props">
                  <q-td :props="props">
                    <q-item>
                      <q-item-section>
                        <q-item-label>
                          <span class="text-blue">
                            <q-icon
                              name="bug_report"
                              color="blue"
                              size="20px"
                              v-if="props.row.type == 'error'"
                            ></q-icon>
                            <q-icon
                              name="settings"
                              color="blue"
                              size="20px"
                              v-if="props.row.type == 'info'"
                            ></q-icon>
                            <q-icon
                              name="flag"
                              color="blue"
                              size="20px"
                              v-if="props.row.type == 'success'"
                            ></q-icon>
                            <q-icon
                              name="fireplace"
                              color="blue"
                              size="20px"
                              v-if="props.row.type == 'warning'"
                            ></q-icon>
                            {{ props.row.stock }}
                          </span>
                          <q-chip
                            class="float-right text-white text-capitalize"
                            :label="props.row.type"
                            color="positive"
                            v-if="props.row.type == 'success'"
                          ></q-chip>
                          <q-chip
                            class="float-right text-white text-capitalize"
                            :label="props.row.type"
                            color="info"
                            v-if="props.row.type == 'info'"
                          ></q-chip>
                          <q-chip
                            class="float-right text-white text-capitalize"
                            :label="props.row.type"
                            color="warning"
                            v-if="props.row.type == 'warning'"
                          ></q-chip>
                          <q-chip
                            class="float-right text-white text-capitalize"
                            :label="props.row.type"
                            color="negative"
                            v-if="props.row.type == 'error'"
                          ></q-chip>
                        </q-item-label>
                        <q-item-label caption class="">
                          <q-linear-progress
                            dark
                            :color="getColor(props.row.Progress)"
                            :value="props.row.Progress / 100"
                          />
                        </q-item-label>
                      </q-item-section>
                    </q-item>
                  </q-td>
                </template>
              </q-table>
            </q-scroll-area>
          </q-card-section>
        </q-card>

        <div class="row q-col-gutter-sm q-py-sm">
          <tab-social />
          <div class="col-lg-6 col-md-6 col-sm-12 col-xs-12">
            <q-carousel
              animated
              v-model="slide"
              infinite
              height="360px"
              arrows
              transition-prev="slide-right"
              transition-next="slide-left"
            >
              <q-carousel-slide :name="1" class="q-pa-none">
                <q-scroll-area class="fit">
                  <q-card class="my-card">
                    <img :src="baseImg" width="100%" />

                    <q-card-section>
                      <div class="text-h6">做自己热爱的事</div>
                      <div class="text-subtitle2">小王</div>
                    </q-card-section>

                    <q-card-actions align="left">
                      <q-btn
                        label="分享"
                        dense
                        color="primary"
                        text-color="blue"
                        outline
                      />
                      <q-btn
                        label="加载更多"
                        dense
                        color="primary"
                        text-color="blue"
                        outline
                      />
                    </q-card-actions>
                  </q-card>
                </q-scroll-area>
              </q-carousel-slide>
              <q-carousel-slide :name="2" class="q-pa-none">
                <q-scroll-area class="fit">
                  <q-card class="my-card">
                    <img :src="baseImg2" width="100%" />

                    <q-card-section>
                      <div class="text-h6">正确安排你的行程</div>
                      <div class="text-subtitle2">每种生活方式都值得尊重</div>
                    </q-card-section>

                    <q-card-actions align="left">
                      <q-btn
                        label="分享"
                        dense
                        color="primary"
                        text-color="blue"
                        outline
                      />
                      <q-btn
                        label="加载更多"
                        dense
                        color="primary"
                        text-color="blue"
                        outline
                      />
                    </q-card-actions>
                  </q-card>
                </q-scroll-area>
              </q-carousel-slide>
              <q-carousel-slide :name="3" class="q-pa-none">
                <q-scroll-area class="fit">
                  <q-card class="my-card">
                    <img :src="baseImg3" width="100%" />

                    <q-card-section>
                      <div class="text-h6">有机会就去旅行</div>
                      <div class="text-subtitle2">你所热爱的, 就是你的生活</div>
                    </q-card-section>

                    <q-card-actions align="left">
                      <q-btn
                        label="分享"
                        dense
                        color="primary"
                        text-color="blue"
                        outline
                      />
                      <q-btn
                        label="加载更多"
                        dense
                        color="primary"
                        text-color="blue"
                        outline
                      />
                    </q-card-actions>
                  </q-card>
                </q-scroll-area>
              </q-carousel-slide>
            </q-carousel>
          </div>
        </div>
      </div>
    </q-scroll-area>
  </q-page>
</template>

<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
  preFetch(param) {
    console.log('param: ', param);
    console.log('running preFetch');
  },
});
</script>

<script setup lang="ts">
import { defineAsyncComponent, ref } from 'vue';
import { QTableProps } from 'quasar';
import { baseImg, baseImg2, baseImg3, sales_data } from './data/list';
const pageHeight = ref(400);
const styFn = (offset: number, height: number) => {
  pageHeight.value = height - offset;
};
const CardSocial = defineAsyncComponent(
  () => import('components/cards/CardSocial.vue')
);
const CardCharts2 = defineAsyncComponent(
  () => import('components/cards/CardCharts2.vue')
);
const TabSocial = defineAsyncComponent(
  () => import('components/tabs/TabSocial.vue')
);

const sales_column: QTableProps['columns'] = [
  {
    name: 'Products',
    label: '产品',
    field: 'Products',
    sortable: true,
    align: 'left',
  },
  {
    name: 'Name',
    label: '购买人',
    field: 'name',
    sortable: true,
    align: 'left',
  },
  {
    name: 'Total',
    label: '总额',
    field: 'total',
    sortable: true,
    align: 'right',
    classes: 'text-bold',
  },
  {
    name: 'Status',
    label: '状态',
    field: 'status',
    sortable: true,
    align: 'left',
    classes: 'text-bold',
  },
  {
    name: 'Stock',
    label: '余量',
    field: 'task',
    sortable: true,
    align: 'left',
  },
];

const slide = ref(1);

const getColor = (val: number) => {
  if (val > 70 && val <= 100) {
    return 'green';
  } else if (val > 50 && val <= 70) {
    return 'blue';
  }
  return 'red';
};
const getChipColor = (status: string) => {
  if (status == '关闭') {
    return 'negative';
  } else if (status == '发送') {
    return 'positive';
  } else if (status == '等待') {
    return 'warning';
  } else if (status == '已付费') {
    return 'info';
  } else {
    return 'dark';
  }
};
</script>

<style scoped></style>

重启项目项目主页如图所示

index

此教程完结撒花~~~

文章列表:

此篇博客进度对代码分支——end

封面


从0到一个前端后台管理框架(五)
https://wangijun.com/2023/06/21/vue-12/
作者
无良芳
发布于
2023年6月21日
许可协议