feat(statistics): 添加统计分析模块:天医币、实物、培训班。
- 新增天医币、实物和培训班报表页面 - 添加统计相关API接口 - 配置统计分析路由 - 优化SheetFooter和DialogFooter组件样式
This commit is contained in:
33
apps/finance/src/api/statistics/index.ts
Normal file
33
apps/finance/src/api/statistics/index.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
export const statisticsApi = {
|
||||||
|
/**
|
||||||
|
* 获取天医币报表列表
|
||||||
|
*/
|
||||||
|
getReportTianyibi: (data: { month?: string; year: number }) => {
|
||||||
|
return requestClient.post('common/statistics/pointStatistics', data);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取实物报表列表
|
||||||
|
*/
|
||||||
|
getPhysicalStatistics: (data: { month?: string; year: number }) => {
|
||||||
|
return requestClient.post('common/statistics/physicalStatistics', data);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取培训班报表列表
|
||||||
|
*/
|
||||||
|
getTrainingClassStatistics: (data: { month?: string; year: number }) => {
|
||||||
|
return requestClient.post('common/statistics/trainingClassStatistics', data);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载天医币报表
|
||||||
|
*/
|
||||||
|
downloadReportTianyibi: (data: { date: string }) => {
|
||||||
|
return requestClient.post('/common/import/getImportFile', data, {
|
||||||
|
responseType: 'blob',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
45
apps/finance/src/router/routes/modules/statistics.ts
Normal file
45
apps/finance/src/router/routes/modules/statistics.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import type { RouteRecordRaw } from 'vue-router';
|
||||||
|
|
||||||
|
const routes: RouteRecordRaw[] = [
|
||||||
|
{
|
||||||
|
meta: {
|
||||||
|
icon: 'mdi:chart-areaspline',
|
||||||
|
keepAlive: true,
|
||||||
|
order: 1000,
|
||||||
|
title: '统计分析',
|
||||||
|
},
|
||||||
|
name: 'Statistics',
|
||||||
|
path: '/statistics',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
meta: {
|
||||||
|
title: '天医币报表',
|
||||||
|
keepAlive: true,
|
||||||
|
},
|
||||||
|
name: 'TianyibiReport',
|
||||||
|
path: '/statistics/tianyibi-report',
|
||||||
|
component: () => import('#/views/statistics/tianyibi/report.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
meta: {
|
||||||
|
title: '实物报表',
|
||||||
|
keepAlive: true,
|
||||||
|
},
|
||||||
|
name: 'PhysicalReport',
|
||||||
|
path: '/statistics/physical-report',
|
||||||
|
component: () => import('#/views/statistics/physical/report.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
meta: {
|
||||||
|
title: '培训班报表',
|
||||||
|
keepAlive: true,
|
||||||
|
},
|
||||||
|
name: 'TrainingClassReport',
|
||||||
|
path: '/statistics/training-class-report',
|
||||||
|
component: () => import('#/views/statistics/trainingClass/report.vue'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default routes;
|
||||||
125
apps/finance/src/views/statistics/physical/report.vue
Normal file
125
apps/finance/src/views/statistics/physical/report.vue
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { Dayjs } from 'dayjs';
|
||||||
|
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { Page, Spinner } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { VbenIcon } from '@vben-core/shadcn-ui';
|
||||||
|
|
||||||
|
import { Button, Card, DatePicker, Empty } from 'ant-design-vue';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
import { statisticsApi } from '#/api/statistics';
|
||||||
|
|
||||||
|
const year = ref<Dayjs>(dayjs());
|
||||||
|
const disabledDate = (date: Dayjs) => date.year() > dayjs().year();
|
||||||
|
|
||||||
|
interface ReportItem {
|
||||||
|
type: string;
|
||||||
|
fee: number;
|
||||||
|
count: number;
|
||||||
|
icon: {
|
||||||
|
color: string;
|
||||||
|
icon: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 报表数据
|
||||||
|
const list = ref<ReportItem[][]>([]);
|
||||||
|
const loading = ref<boolean>(false);
|
||||||
|
const getList = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
list.value = [];
|
||||||
|
try {
|
||||||
|
// 计算查询年份中包含哪些月份, 若为今年则只查询到当前月份,往年则查询所有月份
|
||||||
|
const monthList =
|
||||||
|
year.value.year() === dayjs().year()
|
||||||
|
? Array.from({ length: dayjs().month() + 1 }, (_, i) => (i + 1).toString().padStart(2, '0'))
|
||||||
|
: Array.from({ length: 12 }, (_, i) => (i + 1).toString().padStart(2, '0'));
|
||||||
|
for (const month of monthList) {
|
||||||
|
const data = await statisticsApi.getPhysicalStatistics({
|
||||||
|
year: year.value.year(),
|
||||||
|
month,
|
||||||
|
});
|
||||||
|
// 确保四种支付方式都存在,默认值为0
|
||||||
|
const paymentTypes = [
|
||||||
|
{ type: '微信', icon: { color: 'text-green-500', icon: 'ant-design:wechat-filled' } },
|
||||||
|
{ type: '支付宝', icon: { color: 'text-blue-500', icon: 'ant-design:alipay-outlined' } },
|
||||||
|
{ type: '银行', icon: { color: 'text-yellow-500', icon: 'ant-design:credit-card-filled' } },
|
||||||
|
{
|
||||||
|
type: '天医币',
|
||||||
|
icon: { color: 'text-purple-500', icon: 'ant-design:pay-circle-filled' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const report: ReportItem[] = paymentTypes.map((type) => {
|
||||||
|
const item = data.map.find((item: any) => item.type === type.type);
|
||||||
|
return {
|
||||||
|
type: type.type,
|
||||||
|
fee: item?.fee || 0,
|
||||||
|
count: item?.count || 0,
|
||||||
|
icon: { color: type.icon.color, icon: type.icon.icon },
|
||||||
|
};
|
||||||
|
});
|
||||||
|
list.value.push(report);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<div class="flex h-full flex-col rounded-md bg-white">
|
||||||
|
<div class="search-form p-4">
|
||||||
|
<DatePicker
|
||||||
|
v-model:value="year"
|
||||||
|
picker="year"
|
||||||
|
:disabled-date="disabledDate"
|
||||||
|
@change="getList"
|
||||||
|
/>
|
||||||
|
<Button type="primary" class="ml-2" @click="getList">查询</Button>
|
||||||
|
</div>
|
||||||
|
<div class="h-2 bg-gray-100"></div>
|
||||||
|
<Spinner class="content flex-1 px-3 py-4" :spinning="loading">
|
||||||
|
<div
|
||||||
|
v-if="list.length === 0 && !loading"
|
||||||
|
class="col-span-3 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<Empty />
|
||||||
|
</div>
|
||||||
|
<div v-else class="grid max-h-full grid-cols-3 gap-3 overflow-auto px-1">
|
||||||
|
<Card v-for="(item, index) in list" :key="index" :title="`${index + 1} 月`" size="small">
|
||||||
|
<div class="-m-2 text-[16px]">
|
||||||
|
<div class="flex flex-row py-2">
|
||||||
|
<div v-for="(payment, index2) in item" :key="index2" class="flex-1 text-center">
|
||||||
|
<div class="flex items-center justify-center font-bold">
|
||||||
|
<VbenIcon
|
||||||
|
class="size-5"
|
||||||
|
:class="payment.icon.color"
|
||||||
|
:icon="payment.icon.icon"
|
||||||
|
fallback
|
||||||
|
/>
|
||||||
|
{{ payment.type }}
|
||||||
|
</div>
|
||||||
|
<!-- <div class="text-gray-500">{{ payment.count }}笔</div> -->
|
||||||
|
<div class="text-black">{{ payment.fee }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</Spinner>
|
||||||
|
</div>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
|
<style scoped lang="scss">
|
||||||
|
:deep(.ant-card-head-title) {
|
||||||
|
font-size: 16px !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
149
apps/finance/src/views/statistics/tianyibi/report.vue
Normal file
149
apps/finance/src/views/statistics/tianyibi/report.vue
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { Dayjs } from 'dayjs';
|
||||||
|
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { Page, Spinner } from '@vben/common-ui';
|
||||||
|
// import { downloadFileFromBlobPart } from '@vben/utils';
|
||||||
|
|
||||||
|
import { Button, Card, DatePicker, Empty } from 'ant-design-vue';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
import { statisticsApi } from '#/api/statistics';
|
||||||
|
|
||||||
|
const year = ref<Dayjs>(dayjs());
|
||||||
|
const disabledDate = (date: Dayjs) => date.year() > dayjs().year();
|
||||||
|
|
||||||
|
interface Report {
|
||||||
|
incomes: Record<string, number>;
|
||||||
|
consumes: Record<string, number>;
|
||||||
|
surplus: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ReportItem {
|
||||||
|
type: string;
|
||||||
|
fee: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 报表数据
|
||||||
|
const list = ref<Report[]>([]);
|
||||||
|
const loading = ref<boolean>(false);
|
||||||
|
const getList = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
list.value = [];
|
||||||
|
try {
|
||||||
|
// 计算查询年份中包含哪些月份, 若为今年则只查询到当前月份,往年则查询所有月份
|
||||||
|
const monthList =
|
||||||
|
year.value.year() === dayjs().year()
|
||||||
|
? Array.from({ length: dayjs().month() + 1 }, (_, i) => (i + 1).toString().padStart(2, '0'))
|
||||||
|
: Array.from({ length: 12 }, (_, i) => (i + 1).toString().padStart(2, '0'));
|
||||||
|
for (const month of monthList) {
|
||||||
|
const data = await statisticsApi.getReportTianyibi({
|
||||||
|
year: year.value.year(),
|
||||||
|
month,
|
||||||
|
});
|
||||||
|
list.value.push({
|
||||||
|
incomes: {
|
||||||
|
微信: 0,
|
||||||
|
支付宝: 0,
|
||||||
|
银行: 0,
|
||||||
|
...Object.fromEntries(data.map.incomes.map((item: ReportItem) => [item.type, item.fee])),
|
||||||
|
},
|
||||||
|
consumes: {
|
||||||
|
实物: 0,
|
||||||
|
培训班: 0,
|
||||||
|
vip: 0,
|
||||||
|
课程: 0,
|
||||||
|
...Object.fromEntries(data.map.consumes.map((item: ReportItem) => [item.type, item.fee])),
|
||||||
|
},
|
||||||
|
surplus: data.map.surplus,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 下载报表
|
||||||
|
// const downloadReport = async (index: number) => {
|
||||||
|
// const month = index > 9 ? `${index + 1}` : `0${index + 1}`;
|
||||||
|
// const date = `${year.value.year()}-${month}`;
|
||||||
|
// const filename = `天医币报表_${year.value.year()}年${month}月_文件.xlsx`;
|
||||||
|
// const res = await statisticsApi.downloadReportTianyibi({
|
||||||
|
// date,
|
||||||
|
// });
|
||||||
|
// downloadFileFromBlobPart({
|
||||||
|
// source: res.data,
|
||||||
|
// fileName: filename,
|
||||||
|
// });
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const downloadAllReport = () => {
|
||||||
|
// list.value.forEach((_, index) => {
|
||||||
|
// downloadReport(index);
|
||||||
|
// });
|
||||||
|
// };
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<div class="flex h-full flex-col rounded-md bg-white">
|
||||||
|
<div class="search-form p-4">
|
||||||
|
<DatePicker
|
||||||
|
v-model:value="year"
|
||||||
|
picker="year"
|
||||||
|
:disabled-date="disabledDate"
|
||||||
|
@change="getList"
|
||||||
|
/>
|
||||||
|
<Button type="primary" class="ml-2" @click="getList">查询</Button>
|
||||||
|
<!-- <Button type="link" class="ml-2" @click="downloadAllReport">
|
||||||
|
下载 {{ year.year() }} 年全部天医币报表
|
||||||
|
</Button> -->
|
||||||
|
</div>
|
||||||
|
<div class="h-2 bg-gray-100"></div>
|
||||||
|
<Spinner class="content flex-1 px-3 py-4" :spinning="loading">
|
||||||
|
<div
|
||||||
|
v-if="list.length === 0 && !loading"
|
||||||
|
class="col-span-3 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<Empty />
|
||||||
|
</div>
|
||||||
|
<div v-else class="grid max-h-full grid-cols-3 gap-3 overflow-auto px-1">
|
||||||
|
<Card v-for="(item, index) in list" :key="index" :title="`${index + 1} 月`" size="small">
|
||||||
|
<!-- <template #extra>
|
||||||
|
<Button type="link" @click="downloadReport(index)">下载报表</Button>
|
||||||
|
</template> -->
|
||||||
|
<div class="-m-2 text-[16px]">
|
||||||
|
<div class="flex">
|
||||||
|
<div class="flex-1 bg-[#F6FFF5] px-2 pb-1">
|
||||||
|
<div class="p-1 text-center font-bold">进项</div>
|
||||||
|
<div v-for="(fee, type) in item.incomes" :key="type" class="p-1">
|
||||||
|
<span class="text-gray-500">{{ type }}:</span>
|
||||||
|
<span class="text-black">{{ fee }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 bg-[#FFFBF0] px-2 pb-1">
|
||||||
|
<div class="p-1 text-center font-bold">出项</div>
|
||||||
|
<div v-for="(fee, type) in item.consumes" :key="type" class="p-1">
|
||||||
|
<span class="text-gray-500">{{ type }}:</span>
|
||||||
|
<span class="text-black">{{ fee }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="px-2">APP用户剩余天医币 : {{ item.surplus }}</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</Spinner>
|
||||||
|
</div>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
|
<style scoped lang="scss">
|
||||||
|
:deep(.ant-card-head-title) {
|
||||||
|
font-size: 16px !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
126
apps/finance/src/views/statistics/trainingClass/report.vue
Normal file
126
apps/finance/src/views/statistics/trainingClass/report.vue
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { Dayjs } from 'dayjs';
|
||||||
|
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { Page, Spinner } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { VbenIcon } from '@vben-core/shadcn-ui';
|
||||||
|
|
||||||
|
import { Button, Card, DatePicker, Empty } from 'ant-design-vue';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
import { statisticsApi } from '#/api/statistics';
|
||||||
|
|
||||||
|
const year = ref<Dayjs>(dayjs());
|
||||||
|
const disabledDate = (date: Dayjs) => date.year() > dayjs().year();
|
||||||
|
|
||||||
|
interface ReportItem {
|
||||||
|
type: string;
|
||||||
|
fee: number;
|
||||||
|
count: number;
|
||||||
|
icon: {
|
||||||
|
color: string;
|
||||||
|
icon: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 报表数据
|
||||||
|
const list = ref<ReportItem[][]>([]);
|
||||||
|
const loading = ref<boolean>(false);
|
||||||
|
const getList = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
list.value = [];
|
||||||
|
try {
|
||||||
|
// 计算查询年份中包含哪些月份, 若为今年则只查询到当前月份,往年则查询所有月份
|
||||||
|
const monthList =
|
||||||
|
year.value.year() === dayjs().year()
|
||||||
|
? Array.from({ length: dayjs().month() + 1 }, (_, i) => (i + 1).toString().padStart(2, '0'))
|
||||||
|
: Array.from({ length: 12 }, (_, i) => (i + 1).toString().padStart(2, '0'));
|
||||||
|
for (const month of monthList) {
|
||||||
|
const data = await statisticsApi.getTrainingClassStatistics({
|
||||||
|
year: year.value.year(),
|
||||||
|
month,
|
||||||
|
});
|
||||||
|
// 确保四种支付方式都存在,默认值为0
|
||||||
|
const paymentTypes = [
|
||||||
|
{ type: '微信', icon: { color: 'text-green-500', icon: 'ant-design:wechat-filled' } },
|
||||||
|
{ type: '支付宝', icon: { color: 'text-blue-500', icon: 'ant-design:alipay-outlined' } },
|
||||||
|
{ type: '银行', icon: { color: 'text-yellow-500', icon: 'ant-design:credit-card-filled' } },
|
||||||
|
{
|
||||||
|
type: '天医币',
|
||||||
|
icon: { color: 'text-purple-500', icon: 'ant-design:pay-circle-filled' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const report: ReportItem[] = paymentTypes.map((type) => {
|
||||||
|
const item = data.map.find((item: any) => item.type === type.type);
|
||||||
|
return {
|
||||||
|
type: type.type,
|
||||||
|
fee: item?.fee || 0,
|
||||||
|
count: item?.count || 0,
|
||||||
|
icon: { color: type.icon.color, icon: type.icon.icon },
|
||||||
|
};
|
||||||
|
});
|
||||||
|
list.value.push(report);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<div class="flex h-full flex-col rounded-md bg-white">
|
||||||
|
<div class="search-form p-4">
|
||||||
|
<DatePicker
|
||||||
|
v-model:value="year"
|
||||||
|
picker="year"
|
||||||
|
:disabled-date="disabledDate"
|
||||||
|
@change="getList"
|
||||||
|
/>
|
||||||
|
<Button type="primary" class="ml-2" @click="getList">查询</Button>
|
||||||
|
</div>
|
||||||
|
<div class="h-2 bg-gray-100"></div>
|
||||||
|
<Spinner class="content flex-1 px-3 py-4" :spinning="loading">
|
||||||
|
<div
|
||||||
|
v-if="list.length === 0 && !loading"
|
||||||
|
class="col-span-3 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<Empty />
|
||||||
|
</div>
|
||||||
|
<div v-else class="grid max-h-full grid-cols-3 gap-3 overflow-auto px-1">
|
||||||
|
<Card v-for="(item, index) in list" :key="index" :title="`${index + 1} 月`" size="small">
|
||||||
|
<div class="-m-2 text-[16px]">
|
||||||
|
<div class="flex flex-row py-2">
|
||||||
|
<div v-for="(payment, index2) in item" :key="index2" class="flex-1 text-center">
|
||||||
|
<div class="flex items-center justify-center font-bold">
|
||||||
|
<VbenIcon
|
||||||
|
class="size-5"
|
||||||
|
:class="payment.icon.color"
|
||||||
|
:icon="payment.icon.icon"
|
||||||
|
fallback
|
||||||
|
/>
|
||||||
|
{{ payment.type }}
|
||||||
|
</div>
|
||||||
|
<!-- <div class="text-gray-500">{{ payment.count }}笔</div> -->
|
||||||
|
<div class="text-black">{{ payment.fee }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</Spinner>
|
||||||
|
</div>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
:deep(.ant-card-head-title) {
|
||||||
|
font-size: 16px !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,14 +1,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup></script>
|
||||||
import { ref } from 'vue';
|
|
||||||
|
|
||||||
import { TabPane, Tabs } from 'ant-design-vue';
|
|
||||||
|
|
||||||
const activeKey = ref(1);
|
|
||||||
</script>
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div></div>
|
||||||
<Tabs v-model:active-key="activeKey" :style="{ height: '200px' }">
|
|
||||||
<TabPane v-for="i in 30" :key="i" :tab="`Tab-${i}`">Content of tab {{ i }}</TabPane>
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
||||||
|
|||||||
@@ -27,19 +27,12 @@ const subMenu = useSubMenuContext();
|
|||||||
const { parentMenu, parentPaths } = useMenu();
|
const { parentMenu, parentPaths } = useMenu();
|
||||||
|
|
||||||
const active = computed(() => props.path === rootMenu?.activePath);
|
const active = computed(() => props.path === rootMenu?.activePath);
|
||||||
const menuIcon = computed(() =>
|
const menuIcon = computed(() => (active.value ? props.activeIcon || props.icon : props.icon));
|
||||||
active.value ? props.activeIcon || props.icon : props.icon,
|
|
||||||
);
|
|
||||||
|
|
||||||
const isTopLevelMenuItem = computed(
|
const isTopLevelMenuItem = computed(() => parentMenu.value?.type.name === 'Menu');
|
||||||
() => parentMenu.value?.type.name === 'Menu',
|
|
||||||
);
|
|
||||||
|
|
||||||
const collapseShowTitle = computed(
|
const collapseShowTitle = computed(
|
||||||
() =>
|
() => rootMenu.props?.collapseShowTitle && isTopLevelMenuItem.value && rootMenu.props.collapse,
|
||||||
rootMenu.props?.collapseShowTitle &&
|
|
||||||
isTopLevelMenuItem.value &&
|
|
||||||
rootMenu.props.collapse,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const showTooltip = computed(
|
const showTooltip = computed(
|
||||||
@@ -92,11 +85,7 @@ onBeforeUnmount(() => {
|
|||||||
role="menuitem"
|
role="menuitem"
|
||||||
@click.stop="handleClick"
|
@click.stop="handleClick"
|
||||||
>
|
>
|
||||||
<VbenTooltip
|
<VbenTooltip v-if="showTooltip" :content-class="[rootMenu.theme]" side="right">
|
||||||
v-if="showTooltip"
|
|
||||||
:content-class="[rootMenu.theme]"
|
|
||||||
side="right"
|
|
||||||
>
|
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<div :class="[nsMenu.be('tooltip', 'trigger')]">
|
<div :class="[nsMenu.be('tooltip', 'trigger')]">
|
||||||
<VbenIcon :class="nsMenu.e('icon')" :icon="menuIcon" fallback />
|
<VbenIcon :class="nsMenu.e('icon')" :icon="menuIcon" fallback />
|
||||||
@@ -109,11 +98,7 @@ onBeforeUnmount(() => {
|
|||||||
<slot name="title"></slot>
|
<slot name="title"></slot>
|
||||||
</VbenTooltip>
|
</VbenTooltip>
|
||||||
<div v-show="!showTooltip" :class="[e('content')]">
|
<div v-show="!showTooltip" :class="[e('content')]">
|
||||||
<MenuBadge
|
<MenuBadge v-if="rootMenu.props.mode !== 'horizontal'" class="right-2" v-bind="props" />
|
||||||
v-if="rootMenu.props.mode !== 'horizontal'"
|
|
||||||
class="right-2"
|
|
||||||
v-bind="props"
|
|
||||||
/>
|
|
||||||
<VbenIcon :class="nsMenu.e('icon')" :icon="menuIcon" />
|
<VbenIcon :class="nsMenu.e('icon')" :icon="menuIcon" />
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
<slot name="title"></slot>
|
<slot name="title"></slot>
|
||||||
|
|||||||
@@ -5,11 +5,7 @@ const props = defineProps<{ class?: any }>();
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div :class="cn('flex flex-col-reverse justify-end gap-x-2', props.class)">
|
||||||
:class="
|
|
||||||
cn('flex flex-row flex-col-reverse justify-end gap-x-2', props.class)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -5,11 +5,7 @@ const props = defineProps<{ class?: any }>();
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div :class="cn('flex flex-col-reverse justify-end gap-x-2', props.class)">
|
||||||
:class="
|
|
||||||
cn('flex flex-row flex-col-reverse justify-end gap-x-2', props.class)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -2,14 +2,7 @@
|
|||||||
import type { Props } from './types';
|
import type { Props } from './types';
|
||||||
|
|
||||||
import { preferences } from '@vben-core/preferences';
|
import { preferences } from '@vben-core/preferences';
|
||||||
import {
|
import { Card, Separator, Tabs, TabsList, TabsTrigger, VbenAvatar } from '@vben-core/shadcn-ui';
|
||||||
Card,
|
|
||||||
Separator,
|
|
||||||
Tabs,
|
|
||||||
TabsList,
|
|
||||||
TabsTrigger,
|
|
||||||
VbenAvatar,
|
|
||||||
} from '@vben-core/shadcn-ui';
|
|
||||||
|
|
||||||
import { Page } from '../../components';
|
import { Page } from '../../components';
|
||||||
|
|
||||||
@@ -29,15 +22,12 @@ const tabsValue = defineModel<string>('modelValue');
|
|||||||
<div class="flex h-full w-full">
|
<div class="flex h-full w-full">
|
||||||
<Card class="w-1/6 flex-none">
|
<Card class="w-1/6 flex-none">
|
||||||
<div class="mt-4 flex h-40 flex-col items-center justify-center gap-4">
|
<div class="mt-4 flex h-40 flex-col items-center justify-center gap-4">
|
||||||
<VbenAvatar
|
<VbenAvatar :src="userInfo?.avatar ?? preferences.app.defaultAvatar" class="size-20" />
|
||||||
:src="userInfo?.avatar ?? preferences.app.defaultAvatar"
|
|
||||||
class="size-20"
|
|
||||||
/>
|
|
||||||
<span class="text-lg font-semibold">
|
<span class="text-lg font-semibold">
|
||||||
{{ userInfo?.realName ?? '' }}
|
{{ userInfo?.name ?? '' }}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-foreground/80 text-sm">
|
<span class="text-foreground/80 text-sm">
|
||||||
{{ userInfo?.username ?? '' }}
|
{{ userInfo?.name ?? '' }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Separator class="my-4" />
|
<Separator class="my-4" />
|
||||||
|
|||||||
@@ -219,10 +219,16 @@ const headerSlots = computed(() => {
|
|||||||
:z-index="preferences.app.zIndex"
|
:z-index="preferences.app.zIndex"
|
||||||
@side-mouse-leave="handleSideMouseLeave"
|
@side-mouse-leave="handleSideMouseLeave"
|
||||||
@toggle-sidebar="toggleSidebar"
|
@toggle-sidebar="toggleSidebar"
|
||||||
@update:sidebar-collapse="(value: boolean) => updatePreferences({ sidebar: { collapsed: value } })"
|
@update:sidebar-collapse="
|
||||||
|
(value: boolean) => updatePreferences({ sidebar: { collapsed: value } })
|
||||||
|
"
|
||||||
@update:sidebar-enable="(value: boolean) => updatePreferences({ sidebar: { enable: value } })"
|
@update:sidebar-enable="(value: boolean) => updatePreferences({ sidebar: { enable: value } })"
|
||||||
@update:sidebar-expand-on-hover="(value: boolean) => updatePreferences({ sidebar: { expandOnHover: value } })"
|
@update:sidebar-expand-on-hover="
|
||||||
@update:sidebar-extra-collapse="(value: boolean) => updatePreferences({ sidebar: { extraCollapse: value } })"
|
(value: boolean) => updatePreferences({ sidebar: { expandOnHover: value } })
|
||||||
|
"
|
||||||
|
@update:sidebar-extra-collapse="
|
||||||
|
(value: boolean) => updatePreferences({ sidebar: { extraCollapse: value } })
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<!-- logo -->
|
<!-- logo -->
|
||||||
<template #logo>
|
<template #logo>
|
||||||
@@ -330,7 +336,11 @@ const headerSlots = computed(() => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #tabbar>
|
<template #tabbar>
|
||||||
<LayoutTabbar v-if="preferences.tabbar.enable" :show-icon="preferences.tabbar.showIcon" :theme="theme" />
|
<LayoutTabbar
|
||||||
|
v-if="preferences.tabbar.enable"
|
||||||
|
:show-icon="preferences.tabbar.showIcon"
|
||||||
|
:theme="theme"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 主体内容 -->
|
<!-- 主体内容 -->
|
||||||
@@ -361,7 +371,10 @@ const headerSlots = computed(() => {
|
|||||||
</Transition>
|
</Transition>
|
||||||
|
|
||||||
<template v-if="preferencesButtonPosition.fixed">
|
<template v-if="preferencesButtonPosition.fixed">
|
||||||
<Preferences class="z-100 fixed bottom-20 right-0" @clear-preferences-and-logout="clearPreferencesAndLogout" />
|
<Preferences
|
||||||
|
class="z-100 fixed bottom-20 right-0"
|
||||||
|
@clear-preferences-and-logout="clearPreferencesAndLogout"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<VbenBackTop />
|
<VbenBackTop />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user