feat(projects): 添加侧边菜单
This commit is contained in:
parent
371fad4f26
commit
e25afe2fad
@ -27,7 +27,8 @@ const routes: AuthRoute.Route[] = [
|
||||
meta: {
|
||||
title: '仪表盘',
|
||||
requiresAuth: true,
|
||||
icon: 'carbon:dashboard'
|
||||
icon: 'carbon:dashboard',
|
||||
order: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -38,7 +39,8 @@ const routes: AuthRoute.Route[] = [
|
||||
title: '关于',
|
||||
singleLayout: 'basic',
|
||||
permissions: ['super', 'admin', 'test'],
|
||||
icon: 'fluent:book-information-24-regular'
|
||||
icon: 'fluent:book-information-24-regular',
|
||||
order: 7
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -66,13 +68,21 @@ const routes: AuthRoute.Route[] = [
|
||||
}
|
||||
],
|
||||
meta: {
|
||||
title: '多级菜单'
|
||||
title: '多级菜单',
|
||||
icon: 'carbon:menu',
|
||||
order: 6
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const routeHome: AuthRoute.RoutePath = '/dashboard/analysis';
|
||||
|
||||
function sortRoutes() {
|
||||
routes.sort((next, pre) => Number(next.meta?.order) - Number(pre.meta?.order));
|
||||
}
|
||||
|
||||
sortRoutes();
|
||||
|
||||
const data: ApiRoute.Route = {
|
||||
routes,
|
||||
home: routeHome
|
||||
|
11
package.json
11
package.json
@ -7,7 +7,7 @@
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
"build": "npm run typecheck && cross-env VITE_HTTP_ENV=prod vite build",
|
||||
"build:test": "npm run typecheck && cross-env VITE_HTTP_ENV=test vite build",
|
||||
"build:vercel": "npm run typecheck && cross-env VITE_HTTP_ENV=prod VITE_IS_VERCEL=1 vite build",
|
||||
"build:vercel": "npm run typecheck && cross-env VITE_HTTP_ENV=prod VITE_IS_VERCEL=1 vite build",
|
||||
"preview": "vite preview --port 5050",
|
||||
"lint": "eslint --fix ./ --ext .vue,.js,jsx,.ts,tsx",
|
||||
"prepare": "husky install",
|
||||
@ -22,6 +22,7 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@antv/g2plot": "^2.4.5",
|
||||
"@vueuse/core": "^7.5.3",
|
||||
"axios": "^0.24.0",
|
||||
"clipboard": "^2.0.8",
|
||||
@ -39,13 +40,13 @@
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^16.0.2",
|
||||
"@commitlint/config-conventional": "^16.0.0",
|
||||
"@iconify/json": "^1.1.453",
|
||||
"@iconify/json": "^1.1.454",
|
||||
"@iconify/vue": "^3.1.1",
|
||||
"@types/crypto-js": "^4.1.0",
|
||||
"@types/node": "^17.0.8",
|
||||
"@types/qs": "^6.9.7",
|
||||
"@typescript-eslint/eslint-plugin": "^5.9.0",
|
||||
"@typescript-eslint/parser": "^5.9.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.9.1",
|
||||
"@typescript-eslint/parser": "^5.9.1",
|
||||
"@vitejs/plugin-vue": "^2.0.1",
|
||||
"@vue/eslint-config-prettier": "^7.0.0",
|
||||
"@vue/eslint-config-typescript": "^10.0.0",
|
||||
@ -75,7 +76,7 @@
|
||||
"vite-plugin-mock": "^2.9.6",
|
||||
"vite-plugin-windicss": "^1.6.2",
|
||||
"vue-tsc": "^0.30.2",
|
||||
"vueuc": "^0.4.19",
|
||||
"vueuc": "^0.4.21",
|
||||
"windicss": "^3.4.2"
|
||||
}
|
||||
}
|
||||
|
1355
pnpm-lock.yaml
1355
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
1
src/assets/svg/common/avatar01.svg
Normal file
1
src/assets/svg/common/avatar01.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 8.6 KiB |
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="text-18px hover:text-primary cursor-pointer" @click="handleSwitch">
|
||||
<div class="flex-center text-18px hover:text-primary cursor-pointer" @click="handleSwitch">
|
||||
<icon-mdi-moon-waning-crescent v-if="darkMode" />
|
||||
<icon-mdi-white-balance-sunny v-else />
|
||||
</div>
|
||||
|
108
src/components/custom/CountTo/index.vue
Normal file
108
src/components/custom/CountTo/index.vue
Normal file
@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<span>{{ value }}</span>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted, watch, watchEffect } from 'vue';
|
||||
import { useTransition, TransitionPresets } from '@vueuse/core';
|
||||
import { isNumber } from '@/utils';
|
||||
|
||||
interface Props {
|
||||
/** 初始值 */
|
||||
startValue?: number;
|
||||
/** 结束值 */
|
||||
endValue?: number;
|
||||
/** 动画时长 */
|
||||
duration?: number;
|
||||
/** 自动动画 */
|
||||
autoplay?: boolean;
|
||||
/** 进制 */
|
||||
decimals?: number;
|
||||
/** 前缀 */
|
||||
prefix?: string;
|
||||
/** 后缀 */
|
||||
suffix?: string;
|
||||
/** 分割符号 */
|
||||
separator?: string;
|
||||
/** 小数点 */
|
||||
decimal?: string;
|
||||
/** 使用缓冲动画函数 */
|
||||
useEasing?: boolean;
|
||||
/** 缓冲动画函数类型 */
|
||||
transition?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
startValue: 0,
|
||||
endValue: 2021,
|
||||
duration: 1500,
|
||||
autoplay: true,
|
||||
decimals: 0,
|
||||
prefix: '',
|
||||
suffix: '',
|
||||
separator: ',',
|
||||
decimal: '.',
|
||||
useEasing: true,
|
||||
transition: 'linear'
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'on-started'): void;
|
||||
(e: 'on-finished'): void;
|
||||
}>();
|
||||
|
||||
const source = ref(props.startValue);
|
||||
let outputValue = useTransition(source);
|
||||
const value = computed(() => formatNumber(outputValue.value));
|
||||
const disabled = ref(false);
|
||||
|
||||
function run() {
|
||||
outputValue = useTransition(source, {
|
||||
disabled,
|
||||
duration: props.duration,
|
||||
onStarted: () => emit('on-started'),
|
||||
onFinished: () => emit('on-finished'),
|
||||
...(props.useEasing ? { transition: TransitionPresets[props.transition] } : {})
|
||||
});
|
||||
}
|
||||
|
||||
function start() {
|
||||
run();
|
||||
source.value = props.endValue;
|
||||
}
|
||||
|
||||
function formatNumber(num: number | string) {
|
||||
if (!num) {
|
||||
return '';
|
||||
}
|
||||
const { decimals, decimal, separator, suffix, prefix } = props;
|
||||
let number = Number(num).toFixed(decimals);
|
||||
number += '';
|
||||
|
||||
const x = number.split('.');
|
||||
let x1 = x[0];
|
||||
const x2 = x.length > 1 ? decimal + x[1] : '';
|
||||
const rgx = /(\d+)(\d{3})/;
|
||||
if (separator && !isNumber(separator)) {
|
||||
while (rgx.test(x1)) {
|
||||
x1 = x1.replace(rgx, `$1${separator}$2`);
|
||||
}
|
||||
}
|
||||
return prefix + x1 + x2 + suffix;
|
||||
}
|
||||
|
||||
watch([() => props.startValue, () => props.endValue], () => {
|
||||
if (props.autoplay) {
|
||||
start();
|
||||
}
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
source.value = props.startValue;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (props.autoplay) {
|
||||
start();
|
||||
}
|
||||
});
|
||||
</script>
|
@ -1,3 +1,4 @@
|
||||
import CountTo from './CountTo/index.vue';
|
||||
import ImageVerify from './ImageVerify/index.vue';
|
||||
|
||||
export { ImageVerify };
|
||||
export { CountTo, ImageVerify };
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<global-content />
|
||||
<global-content :show-padding="false" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
@ -1,14 +1,28 @@
|
||||
<template>
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="fade-slide" mode="out-in" appear>
|
||||
<component :is="Component" v-if="app.reloadFlag" />
|
||||
</transition>
|
||||
</router-view>
|
||||
<div
|
||||
:class="{ 'p-16px': showPadding }"
|
||||
class="h-full bg-[#f6f9f8] dark:bg-[#101014] transition duration-300 ease-in-out"
|
||||
>
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="fade-slide" mode="out-in" appear>
|
||||
<component :is="Component" v-if="app.reloadFlag" />
|
||||
</transition>
|
||||
</router-view>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useAppStore } from '@/store';
|
||||
|
||||
interface Props {
|
||||
/** 显示padding */
|
||||
showPadding?: boolean;
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
showPadding: true
|
||||
});
|
||||
|
||||
const app = useAppStore();
|
||||
</script>
|
||||
<style scoped></style>
|
||||
|
14
src/layouts/common/GlobalHeader/components/FullScreen.vue
Normal file
14
src/layouts/common/GlobalHeader/components/FullScreen.vue
Normal file
@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<hover-container class="w-40px h-full" tooltip-content="全屏" @click="toggle">
|
||||
<icon-gridicons-fullscreen-exit v-if="isFullscreen" class="text-18px" />
|
||||
<icon-gridicons-fullscreen v-else class="text-18px" />
|
||||
</hover-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useFullscreen } from '@vueuse/core';
|
||||
import { HoverContainer } from '@/components';
|
||||
|
||||
const { isFullscreen, toggle } = useFullscreen();
|
||||
</script>
|
||||
<style scoped></style>
|
12
src/layouts/common/GlobalHeader/components/GithubSite.vue
Normal file
12
src/layouts/common/GlobalHeader/components/GithubSite.vue
Normal file
@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<hover-container tooltip-content="github" class="w-40px h-full">
|
||||
<a href="https://github.com/honghuangdc/soybean-admin" target="_blank" class="flex-center">
|
||||
<icon-mdi-github class="text-20px text-[#666]" />
|
||||
</a>
|
||||
</hover-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { HoverContainer } from '@/components';
|
||||
</script>
|
||||
<style scoped></style>
|
14
src/layouts/common/GlobalHeader/components/ThemeMode.vue
Normal file
14
src/layouts/common/GlobalHeader/components/ThemeMode.vue
Normal file
@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<hover-container class="w-40px" content-class="hover:text-primary" tooltip-content="主题模式">
|
||||
<dark-mode-switch :dark="theme.darkMode" class="wh-full" @update:dark="setDarkMode" />
|
||||
</hover-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { HoverContainer, DarkModeSwitch } from '@/components';
|
||||
import { useThemeStore } from '@/store';
|
||||
|
||||
const theme = useThemeStore();
|
||||
const { setDarkMode } = useThemeStore();
|
||||
</script>
|
||||
<style scoped></style>
|
54
src/layouts/common/GlobalHeader/components/UserAvatar.vue
Normal file
54
src/layouts/common/GlobalHeader/components/UserAvatar.vue
Normal file
@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<n-dropdown :options="options" @select="handleDropdown">
|
||||
<hover-container class="px-12px">
|
||||
<img :src="avatar" class="w-32px h-32px" />
|
||||
<span class="pl-8px text-16px font-medium">{{ auth.userInfo.userName }}</span>
|
||||
</hover-container>
|
||||
</n-dropdown>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { NDropdown, useDialog } from 'naive-ui';
|
||||
import { HoverContainer } from '@/components';
|
||||
import { useAuthStore } from '@/store';
|
||||
import { iconifyRender } from '@/utils';
|
||||
import avatar from '@/assets/svg/common/avatar01.svg';
|
||||
|
||||
type DropdownKey = 'user-center' | 'logout';
|
||||
|
||||
const auth = useAuthStore();
|
||||
const dialog = useDialog();
|
||||
|
||||
const options = [
|
||||
{
|
||||
label: '用户中心',
|
||||
key: 'user-center',
|
||||
icon: iconifyRender('carbon:user-avatar')
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
key: 'divider'
|
||||
},
|
||||
{
|
||||
label: '退出登录',
|
||||
key: 'logout',
|
||||
icon: iconifyRender('carbon:logout')
|
||||
}
|
||||
];
|
||||
|
||||
function handleDropdown(optionKey: string) {
|
||||
const key = optionKey as DropdownKey;
|
||||
if (key === 'logout') {
|
||||
dialog.info({
|
||||
title: '提示',
|
||||
content: '您确定要退出登录吗?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: () => {
|
||||
auth.resetAuthStore(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped></style>
|
@ -1,3 +1,7 @@
|
||||
import MenuCollapse from './MenuCollapse.vue';
|
||||
import GithubSite from './GithubSite.vue';
|
||||
import FullScreen from './FullScreen.vue';
|
||||
import ThemeMode from './ThemeMode.vue';
|
||||
import UserAvatar from './UserAvatar.vue';
|
||||
|
||||
export { MenuCollapse };
|
||||
export { MenuCollapse, GithubSite, FullScreen, ThemeMode, UserAvatar };
|
||||
|
@ -5,6 +5,17 @@
|
||||
<menu-collapse v-if="showMenuCollape" />
|
||||
<!-- <global-breadcrumb v-if="theme.header.crumb.visible" /> -->
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="flex-1-hidden flex-y-center h-full"
|
||||
:style="{ justifyContent: theme.menu.horizontalPosition }"
|
||||
></div>
|
||||
<div class="flex justify-end h-full">
|
||||
<github-site />
|
||||
<full-screen />
|
||||
<theme-mode />
|
||||
<user-avatar />
|
||||
</div>
|
||||
</dark-mode-container>
|
||||
</template>
|
||||
|
||||
@ -13,7 +24,7 @@ import { DarkModeContainer } from '@/components';
|
||||
import { useThemeStore } from '@/store';
|
||||
import type { GlobalHeaderProps } from '@/interface';
|
||||
import GlobalLogo from '../GlobalLogo/index.vue';
|
||||
import { MenuCollapse } from './components';
|
||||
import { MenuCollapse, GithubSite, FullScreen, ThemeMode, UserAvatar } from './components';
|
||||
|
||||
interface Props {
|
||||
/** 显示logo */
|
||||
|
69
src/layouts/common/GlobalSider/components/SiderMenu.vue
Normal file
69
src/layouts/common/GlobalSider/components/SiderMenu.vue
Normal file
@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<n-scrollbar>
|
||||
<n-menu
|
||||
:value="activeKey"
|
||||
:collapsed="app.siderCollapse"
|
||||
:collapsed-width="theme.sider.collapsedWidth"
|
||||
:collapsed-icon-size="22"
|
||||
:options="menus"
|
||||
:expanded-keys="expandedKeys"
|
||||
:indent="18"
|
||||
@update:value="handleUpdateMenu"
|
||||
@update:expanded-keys="handleUpdateExpandedKeys"
|
||||
/>
|
||||
</n-scrollbar>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { NScrollbar, NMenu } from 'naive-ui';
|
||||
import type { MenuOption } from 'naive-ui';
|
||||
import { useAppStore, useThemeStore, useRouteStore } from '@/store';
|
||||
import { useRouterPush } from '@/composables';
|
||||
import type { GlobalMenuOption } from '@/interface';
|
||||
|
||||
const route = useRoute();
|
||||
const app = useAppStore();
|
||||
const theme = useThemeStore();
|
||||
const routeStore = useRouteStore();
|
||||
const { routerPush } = useRouterPush();
|
||||
|
||||
const menus = computed(() => routeStore.menus as GlobalMenuOption[]);
|
||||
const activeKey = computed(() => route.name as string);
|
||||
const expandedKeys = ref<string[]>([]);
|
||||
|
||||
function getExpendedKeys() {
|
||||
const keys = menus.value.map(menu => getActiveKeysInMenus(menu)).flat();
|
||||
return keys;
|
||||
}
|
||||
|
||||
function getActiveKeysInMenus(menu: GlobalMenuOption) {
|
||||
const keys: string[] = [];
|
||||
if (activeKey.value.includes(menu.routeName)) {
|
||||
keys.push(menu.routeName);
|
||||
}
|
||||
if (menu.children) {
|
||||
keys.push(...menu.children.map(item => getActiveKeysInMenus(item as GlobalMenuOption)).flat());
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
function handleUpdateMenu(_key: string, item: MenuOption) {
|
||||
const menuItem = item as GlobalMenuOption;
|
||||
routerPush(menuItem.routePath);
|
||||
}
|
||||
|
||||
function handleUpdateExpandedKeys(keys: string[]) {
|
||||
expandedKeys.value = keys;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => route.name,
|
||||
() => {
|
||||
expandedKeys.value = getExpendedKeys();
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
<style scoped></style>
|
3
src/layouts/common/GlobalSider/components/index.ts
Normal file
3
src/layouts/common/GlobalSider/components/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import SiderMenu from './SiderMenu.vue';
|
||||
|
||||
export { SiderMenu };
|
@ -1,9 +1,18 @@
|
||||
<template>
|
||||
<dark-mode-container class="global-sider flex-y-center h-full"></dark-mode-container>
|
||||
<dark-mode-container class="global-sider flex-col-stretch h-full">
|
||||
<global-logo :show-title="!app.siderCollapse" :style="{ height: theme.header.height + 'px' }" />
|
||||
<sider-menu class="flex-1-hidden" />
|
||||
</dark-mode-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { DarkModeContainer } from '@/components';
|
||||
import { useAppStore, useThemeStore } from '@/store';
|
||||
import GlobalLogo from '../GlobalLogo/index.vue';
|
||||
import { SiderMenu } from './components';
|
||||
|
||||
const app = useAppStore();
|
||||
const theme = useThemeStore();
|
||||
</script>
|
||||
<style scoped>
|
||||
.global-sider {
|
||||
|
@ -52,6 +52,9 @@
|
||||
@update:value="handleSetNumber($event, setMixSiderWidth)"
|
||||
/>
|
||||
</setting-menu>
|
||||
<setting-menu label="固定底部">
|
||||
<n-switch :value="theme.footer.fixed" @update:value="setFooterIsFixed" />
|
||||
</setting-menu>
|
||||
</n-space>
|
||||
</template>
|
||||
|
||||
@ -68,7 +71,8 @@ const {
|
||||
setTabHeight,
|
||||
setSiderWidth,
|
||||
setMixSiderWidth,
|
||||
setTabIsCache
|
||||
setTabIsCache,
|
||||
setFooterIsFixed
|
||||
} = useThemeStore();
|
||||
|
||||
function handleSetNumber(value: number | null, callback: (value: number) => void) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<main class="soybean-layout__main" :style="style">
|
||||
<main :style="style" class="soybean-layout__main">
|
||||
<slot></slot>
|
||||
</main>
|
||||
</template>
|
||||
|
@ -78,7 +78,7 @@ const defaultThemeSetting: ThemeSetting = {
|
||||
]
|
||||
},
|
||||
footer: {
|
||||
fixed: true,
|
||||
fixed: false,
|
||||
height: 48
|
||||
},
|
||||
page: {
|
||||
|
@ -17,6 +17,8 @@ interface RouteStore {
|
||||
isAddedDynamicRoute: Ref<boolean>;
|
||||
/** 初始化动态路由 */
|
||||
initDynamicRoute(router: Router): Promise<void>;
|
||||
/** 菜单 */
|
||||
menus: Ref<GlobalMenuOption[]>;
|
||||
}
|
||||
|
||||
export const useRouteStore = defineStore('route-store', () => {
|
||||
@ -50,7 +52,8 @@ export const useRouteStore = defineStore('route-store', () => {
|
||||
routes,
|
||||
setRoutes,
|
||||
isAddedDynamicRoute,
|
||||
initDynamicRoute
|
||||
initDynamicRoute,
|
||||
menus
|
||||
};
|
||||
|
||||
return routeStore;
|
||||
|
@ -30,7 +30,7 @@ interface ThemeStore extends LayoutFunc, HeaderFunc, TabFunc, SiderFunc, FooterF
|
||||
/** 设置暗黑模式 */
|
||||
setDarkMode(dark: boolean): void;
|
||||
/** 切换/关闭 暗黑模式 */
|
||||
toggleDarkMode(dark: boolean): void;
|
||||
toggleDarkMode(): void;
|
||||
/** 布局样式 */
|
||||
layout: ThemeSetting['layout'];
|
||||
/** 主题颜色 */
|
||||
|
137
src/views/dashboard/analysis/components/BottomPart/index.vue
Normal file
137
src/views/dashboard/analysis/components/BottomPart/index.vue
Normal file
@ -0,0 +1,137 @@
|
||||
<template>
|
||||
<n-grid :x-gap="16" :y-gap="16" :item-responsive="true" responsive="screen">
|
||||
<n-grid-item span="s:24 m:8">
|
||||
<n-card title="时间线" :bordered="false" class="rounded-16px shadow-sm">
|
||||
<div class="h-360px">
|
||||
<n-timeline>
|
||||
<n-timeline-item v-for="item in timelines" :key="item.type" v-bind="item" />
|
||||
</n-timeline>
|
||||
</div>
|
||||
</n-card>
|
||||
</n-grid-item>
|
||||
<n-grid-item span="s:24 m:16">
|
||||
<n-card title="表格" :bordered="false" class="rounded-16px shadow-sm">
|
||||
<div class="h-360px">
|
||||
<n-data-table size="small" :columns="columns" :data="tableData" />
|
||||
</div>
|
||||
</n-card>
|
||||
</n-grid-item>
|
||||
</n-grid>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue';
|
||||
import { NGrid, NGridItem, NCard, NTimeline, NTimelineItem, NDataTable, NTag } from 'naive-ui';
|
||||
|
||||
interface TimelineData {
|
||||
type: 'default' | 'info' | 'success' | 'warning' | 'error';
|
||||
title: string;
|
||||
content: string;
|
||||
time: string;
|
||||
}
|
||||
|
||||
interface TableData {
|
||||
key: number;
|
||||
name: string;
|
||||
age: number;
|
||||
address: string;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
const timelines: TimelineData[] = [
|
||||
{ type: 'default', title: '啊', content: '', time: '2021-10-10 20:46' },
|
||||
{ type: 'success', title: '成功', content: '哪里成功', time: '2021-10-10 20:46' },
|
||||
{ type: 'error', title: '错误', content: '哪里错误', time: '2021-10-10 20:46' },
|
||||
{ type: 'warning', title: '警告', content: '哪里警告', time: '2021-10-10 20:46' },
|
||||
{ type: 'info', title: '信息', content: '是的', time: '2021-10-10 20:46' }
|
||||
];
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'Name',
|
||||
key: 'name'
|
||||
},
|
||||
{
|
||||
title: 'Age',
|
||||
key: 'age'
|
||||
},
|
||||
{
|
||||
title: 'Address',
|
||||
key: 'address'
|
||||
},
|
||||
{
|
||||
title: 'Tags',
|
||||
key: 'tags',
|
||||
render(row: TableData) {
|
||||
const tags = row.tags.map(tagKey => {
|
||||
return h(
|
||||
NTag,
|
||||
{
|
||||
style: {
|
||||
marginRight: '6px'
|
||||
},
|
||||
type: 'info'
|
||||
},
|
||||
{
|
||||
default: () => tagKey
|
||||
}
|
||||
);
|
||||
});
|
||||
return tags;
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const tableData: TableData[] = [
|
||||
{
|
||||
key: 0,
|
||||
name: 'John Brown',
|
||||
age: 32,
|
||||
address: 'New York No. 1 Lake Park',
|
||||
tags: ['nice', 'developer']
|
||||
},
|
||||
{
|
||||
key: 1,
|
||||
name: 'Jim Green',
|
||||
age: 42,
|
||||
address: 'London No. 1 Lake Park',
|
||||
tags: ['wow']
|
||||
},
|
||||
{
|
||||
key: 2,
|
||||
name: 'Joe Black',
|
||||
age: 32,
|
||||
address: 'Sidney No. 1 Lake Park',
|
||||
tags: ['cool', 'teacher']
|
||||
},
|
||||
{
|
||||
key: 3,
|
||||
name: 'Soybean',
|
||||
age: 25,
|
||||
address: 'China Shenzhen',
|
||||
tags: ['handsome', 'programmer']
|
||||
},
|
||||
{
|
||||
key: 4,
|
||||
name: 'John Brown',
|
||||
age: 32,
|
||||
address: 'New York No. 1 Lake Park',
|
||||
tags: ['nice', 'developer']
|
||||
},
|
||||
{
|
||||
key: 5,
|
||||
name: 'Jim Green',
|
||||
age: 42,
|
||||
address: 'London No. 1 Lake Park',
|
||||
tags: ['wow']
|
||||
},
|
||||
{
|
||||
key: 6,
|
||||
name: 'Joe Black',
|
||||
age: 32,
|
||||
address: 'Sidney No. 1 Lake Park',
|
||||
tags: ['cool', 'teacher']
|
||||
}
|
||||
];
|
||||
</script>
|
||||
<style scoped></style>
|
@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<div class="p-16px rounded-16px text-white" :style="{ backgroundImage: gradientStyle }">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
interface Props {
|
||||
/** 渐变开始的颜色 */
|
||||
startColor?: string;
|
||||
/** 渐变结束的颜色 */
|
||||
endColor?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
startColor: '#56cdf3',
|
||||
endColor: '#719de3'
|
||||
});
|
||||
|
||||
const gradientStyle = computed(() => `linear-gradient(to bottom right, ${props.startColor}, ${props.endColor})`);
|
||||
</script>
|
||||
<style scoped></style>
|
@ -0,0 +1,3 @@
|
||||
import GradientBg from './GradientBg.vue';
|
||||
|
||||
export { GradientBg };
|
70
src/views/dashboard/analysis/components/DataCard/index.vue
Normal file
70
src/views/dashboard/analysis/components/DataCard/index.vue
Normal file
@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<n-grid cols="s:1 m:2 l:4" responsive="screen" :x-gap="16" :y-gap="16">
|
||||
<n-grid-item v-for="item in cardData" :key="item.id">
|
||||
<gradient-bg class="h-100px" :start-color="item.colors[0]" :end-color="item.colors[1]">
|
||||
<h3 class="text-16px">{{ item.title }}</h3>
|
||||
<div class="flex justify-between pt-12px">
|
||||
<Icon :icon="item.icon" class="text-32px" />
|
||||
<count-to
|
||||
:prefix="item.unit"
|
||||
:start-value="1"
|
||||
:end-value="item.value"
|
||||
class="text-30px text-white dark:text-dark"
|
||||
/>
|
||||
</div>
|
||||
</gradient-bg>
|
||||
</n-grid-item>
|
||||
</n-grid>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { NGrid, NGridItem } from 'naive-ui';
|
||||
import { Icon } from '@iconify/vue';
|
||||
import { CountTo } from '@/components';
|
||||
import { GradientBg } from './components';
|
||||
|
||||
interface CardData {
|
||||
id: string;
|
||||
title: string;
|
||||
value: number;
|
||||
unit: string;
|
||||
colors: [string, string];
|
||||
icon: string;
|
||||
}
|
||||
|
||||
const cardData: CardData[] = [
|
||||
{
|
||||
id: 'visit',
|
||||
title: '访问量',
|
||||
value: 1000000,
|
||||
unit: '',
|
||||
colors: ['#ec4786', '#b955a4'],
|
||||
icon: 'ant-design:bar-chart-outlined'
|
||||
},
|
||||
{
|
||||
id: 'amount',
|
||||
title: '成交额',
|
||||
value: 234567.89,
|
||||
unit: '$',
|
||||
colors: ['#865ec0', '#5144b4'],
|
||||
icon: 'ant-design:money-collect-outlined'
|
||||
},
|
||||
{
|
||||
id: 'download',
|
||||
title: '下载数',
|
||||
value: 666666,
|
||||
unit: '',
|
||||
colors: ['#56cdf3', '#719de3'],
|
||||
icon: 'carbon:document-download'
|
||||
},
|
||||
{
|
||||
id: 'trade',
|
||||
title: '成交数',
|
||||
value: 999999,
|
||||
unit: '',
|
||||
colors: ['#fcbc25', '#f68057'],
|
||||
icon: 'ant-design:trademark-circle-outlined'
|
||||
}
|
||||
];
|
||||
</script>
|
||||
<style scoped></style>
|
152
src/views/dashboard/analysis/components/TopChart/data.json
Normal file
152
src/views/dashboard/analysis/components/TopChart/data.json
Normal file
@ -0,0 +1,152 @@
|
||||
[
|
||||
{
|
||||
"date": "2021/10/1",
|
||||
"type": "下载量",
|
||||
"value": 4623
|
||||
},
|
||||
{
|
||||
"date": "2021/10/1",
|
||||
"type": "注册数",
|
||||
"value": 2208
|
||||
},
|
||||
{
|
||||
"date": "2021/10/2",
|
||||
"type": "下载量",
|
||||
"value": 6145
|
||||
},
|
||||
{
|
||||
"date": "2021/10/2",
|
||||
"type": "注册数",
|
||||
"value": 2016
|
||||
},
|
||||
{
|
||||
"date": "2021/10/3",
|
||||
"type": "下载量",
|
||||
"value": 508
|
||||
},
|
||||
{
|
||||
"date": "2021/10/3",
|
||||
"type": "注册数",
|
||||
"value": 2916
|
||||
},
|
||||
{
|
||||
"date": "2021/10/4",
|
||||
"type": "下载量",
|
||||
"value": 6268
|
||||
},
|
||||
{
|
||||
"date": "2021/10/4",
|
||||
"type": "注册数",
|
||||
"value": 4512
|
||||
},
|
||||
{
|
||||
"date": "2021/10/5",
|
||||
"type": "下载量",
|
||||
"value": 6411
|
||||
},
|
||||
{
|
||||
"date": "2021/10/5",
|
||||
"type": "注册数",
|
||||
"value": 8281
|
||||
},
|
||||
{
|
||||
"date": "2021/10/6",
|
||||
"type": "下载量",
|
||||
"value": 1890
|
||||
},
|
||||
{
|
||||
"date": "2021/10/6",
|
||||
"type": "注册数",
|
||||
"value": 2008
|
||||
},
|
||||
{
|
||||
"date": "2021/10/7",
|
||||
"type": "下载量",
|
||||
"value": 4251
|
||||
},
|
||||
{
|
||||
"date": "2021/10/7",
|
||||
"type": "注册数",
|
||||
"value": 1963
|
||||
},
|
||||
{
|
||||
"date": "2021/10/8",
|
||||
"type": "下载量",
|
||||
"value": 2978
|
||||
},
|
||||
{
|
||||
"date": "2021/10/8",
|
||||
"type": "注册数",
|
||||
"value": 2367
|
||||
},
|
||||
{
|
||||
"date": "2021/10/9",
|
||||
"type": "下载量",
|
||||
"value": 3880
|
||||
},
|
||||
{
|
||||
"date": "2021/10/9",
|
||||
"type": "注册数",
|
||||
"value": 2956
|
||||
},
|
||||
{
|
||||
"date": "2021/10/10",
|
||||
"type": "下载量",
|
||||
"value": 3606
|
||||
},
|
||||
{
|
||||
"date": "2021/10/10",
|
||||
"type": "注册数",
|
||||
"value": 678
|
||||
},
|
||||
{
|
||||
"date": "2021/10/11",
|
||||
"type": "下载量",
|
||||
"value": 4311
|
||||
},
|
||||
{
|
||||
"date": "2021/10/11",
|
||||
"type": "注册数",
|
||||
"value": 3188
|
||||
},
|
||||
{
|
||||
"date": "2021/10/12",
|
||||
"type": "下载量",
|
||||
"value": 4116
|
||||
},
|
||||
{
|
||||
"date": "2021/10/12",
|
||||
"type": "注册数",
|
||||
"value": 3491
|
||||
},
|
||||
{
|
||||
"date": "2021/10/13",
|
||||
"type": "下载量",
|
||||
"value": 6419
|
||||
},
|
||||
{
|
||||
"date": "2021/10/13",
|
||||
"type": "注册数",
|
||||
"value": 2852
|
||||
},
|
||||
{
|
||||
"date": "2021/10/14",
|
||||
"type": "下载量",
|
||||
"value": 1643
|
||||
},
|
||||
{
|
||||
"date": "2021/10/14",
|
||||
"type": "注册数",
|
||||
"value": 4788
|
||||
},
|
||||
{
|
||||
"date": "2021/10/15",
|
||||
"type": "下载量",
|
||||
"value": 445
|
||||
},
|
||||
{
|
||||
"date": "2021/10/15",
|
||||
"type": "注册数",
|
||||
"value": 4319
|
||||
}
|
||||
]
|
129
src/views/dashboard/analysis/components/TopChart/index.vue
Normal file
129
src/views/dashboard/analysis/components/TopChart/index.vue
Normal file
@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<n-grid :x-gap="16" :y-gap="16" :item-responsive="true" responsive="screen">
|
||||
<n-grid-item span="s:24 m:16">
|
||||
<n-card :bordered="false" class="rounded-16px shadow-sm">
|
||||
<div class="flex w-full h-360px">
|
||||
<div class="w-200px h-full py-12px">
|
||||
<h3 class="text-16px font-bold">Dashboard</h3>
|
||||
<p class="text-[#aaa]">Overview Of Lasted Month</p>
|
||||
<h3 class="pt-36px text-24px font-bold">
|
||||
<count-to prefix="$" :start-value="0" :end-value="7754" />
|
||||
</h3>
|
||||
<p class="text-[#aaa]">Current Month Earnings</p>
|
||||
<h3 class="pt-36px text-24px font-bold">
|
||||
<count-to :start-value="0" :end-value="1234" />
|
||||
</h3>
|
||||
<p class="text-[#aaa]">Current Month Sales</p>
|
||||
<n-button class="mt-24px" type="primary">Last Month Summary</n-button>
|
||||
</div>
|
||||
<div class="flex-1-hidden h-full">
|
||||
<div ref="lineRef" class="wh-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
</n-card>
|
||||
</n-grid-item>
|
||||
<n-grid-item span="s:24 m:8">
|
||||
<n-card :bordered="false" class="rounded-16px shadow-sm">
|
||||
<div ref="pieRef" class="w-full h-360px"></div>
|
||||
</n-card>
|
||||
</n-grid-item>
|
||||
</n-grid>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { NGrid, NGridItem, NCard, NButton } from 'naive-ui';
|
||||
import { Line, Pie } from '@antv/g2plot';
|
||||
import { CountTo } from '@/components';
|
||||
import data from './data.json';
|
||||
|
||||
const lineRef = ref<HTMLElement | null>(null);
|
||||
const line = ref<Line | null>(null);
|
||||
const pieRef = ref<HTMLElement | null>(null);
|
||||
const pie = ref<Pie | null>(null);
|
||||
|
||||
function renderLineChart() {
|
||||
line.value = new Line(lineRef.value!, {
|
||||
data,
|
||||
autoFit: true,
|
||||
xField: 'date',
|
||||
yField: 'value',
|
||||
seriesField: 'type',
|
||||
lineStyle: {
|
||||
lineWidth: 4
|
||||
},
|
||||
area: {
|
||||
style: {
|
||||
fill: 'l(270) 0:#ffffff 0.5:#7ec2f3 1:#1890ff'
|
||||
}
|
||||
},
|
||||
smooth: true,
|
||||
animation: {
|
||||
appear: {
|
||||
animation: 'wave-in',
|
||||
duration: 2000
|
||||
}
|
||||
}
|
||||
});
|
||||
line.value.render();
|
||||
}
|
||||
function renderPieChart() {
|
||||
const data = [
|
||||
{ type: '学习', value: 20 },
|
||||
{ type: '娱乐', value: 10 },
|
||||
{ type: '工作', value: 30 },
|
||||
{ type: '休息', value: 40 }
|
||||
];
|
||||
pie.value = new Pie(pieRef.value!, {
|
||||
appendPadding: 10,
|
||||
data,
|
||||
angleField: 'value',
|
||||
colorField: 'type',
|
||||
radius: 0.8,
|
||||
innerRadius: 0.65,
|
||||
meta: {
|
||||
value: {
|
||||
formatter: v => `${v}%`
|
||||
}
|
||||
},
|
||||
label: {
|
||||
type: 'inner',
|
||||
autoRotate: false,
|
||||
formatter: ({ percent }) => `${(percent * 100).toFixed(0)}%`
|
||||
},
|
||||
statistic: undefined,
|
||||
pieStyle: {
|
||||
radius: [20]
|
||||
},
|
||||
color: ['#025DF4', '#DB6BCF', '#2498D1', '#FF745A', '#007E99', '#FFA8A8', '#2391FF'],
|
||||
legend: {
|
||||
position: 'bottom'
|
||||
},
|
||||
interactions: [
|
||||
{ type: 'element-selected' },
|
||||
{ type: 'element-active' },
|
||||
{
|
||||
type: 'pie-statistic-active',
|
||||
cfg: {
|
||||
start: [
|
||||
{ trigger: 'element:mouseenter', action: 'pie-statistic:change' },
|
||||
{ trigger: 'legend-item:mouseenter', action: 'pie-statistic:change' }
|
||||
],
|
||||
end: [
|
||||
{ trigger: 'element:mouseleave', action: 'pie-statistic:reset' },
|
||||
{ trigger: 'legend-item:mouseleave', action: 'pie-statistic:reset' }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
pie.value.render();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
renderLineChart();
|
||||
renderPieChart();
|
||||
});
|
||||
</script>
|
||||
<style scoped></style>
|
5
src/views/dashboard/analysis/components/index.ts
Normal file
5
src/views/dashboard/analysis/components/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import TopChart from './TopChart/index.vue';
|
||||
import DataCard from './DataCard/index.vue';
|
||||
import BottomPart from './BottomPart/index.vue';
|
||||
|
||||
export { TopChart, DataCard, BottomPart };
|
@ -1,10 +1,13 @@
|
||||
<template>
|
||||
<div>
|
||||
<h3>DashboardAnalysis</h3>
|
||||
<router-link to="/about">about</router-link>
|
||||
<router-link to="/dashboard/workbench">workbench</router-link>
|
||||
</div>
|
||||
<n-space :vertical="true" :size="16">
|
||||
<top-chart />
|
||||
<data-card />
|
||||
<bottom-part />
|
||||
</n-space>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
<script lang="ts" setup>
|
||||
import { NSpace } from 'naive-ui';
|
||||
import { TopChart, DataCard, BottomPart } from './components';
|
||||
</script>
|
||||
<style scoped></style>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<n-card :bordered="false" class="rounded-16px shadow-sm">
|
||||
<div class="flex-y-center justify-between">
|
||||
<div class="flex-y-center">
|
||||
<img src="@/assets/svg/avatar/avatar01.svg" alt="" class="w-70px h-70px" />
|
||||
<img src="@/assets/svg/common/avatar01.svg" alt="" class="w-70px h-70px" />
|
||||
<div class="pl-12px">
|
||||
<h3 class="text-18px font-semibold">早安,{{ auth.userInfo.userName }}, 今天又是充满活力的一天!</h3>
|
||||
<p class="leading-30px text-[#999]">今日多云转晴,20℃ - 25℃!</p>
|
||||
|
@ -20,7 +20,7 @@
|
||||
<n-list-item v-for="item in activity" :key="item.id">
|
||||
<template #prefix>
|
||||
<div class="w-48px h-48px">
|
||||
<img src="@/assets/svg/avatar/avatar01.svg" alt="" class="wh-full" />
|
||||
<img src="@/assets/svg/common/avatar01.svg" alt="" class="wh-full" />
|
||||
</div>
|
||||
</template>
|
||||
<n-thing :title="item.content" :description="item.time" />
|
||||
|
@ -51,8 +51,8 @@ const { toLoginModule } = useRouterPush();
|
||||
|
||||
const formRef = ref<(HTMLElement & FormInst) | null>(null);
|
||||
const model = reactive({
|
||||
phone: '',
|
||||
pwd: ''
|
||||
phone: '15170283876',
|
||||
pwd: 'abc123456'
|
||||
});
|
||||
const rules: FormRules = {
|
||||
phone: formRules.phone,
|
||||
|
Loading…
Reference in New Issue
Block a user