超市会议
This commit is contained in:
1 parent
30e3bf6655
commit
0ebd6737fe
16 files changed
+1397
-162
No files matched your search
@@ -39,3 +39,12 @@ export function putObj(obj?: Object) {
|
||||
})
|
||||
}
|
||||
|
||||
// 获取会议室预约详情页面数据
|
||||
export function getBookingDetailPage(roomId: string, date: string) {
|
||||
return request({
|
||||
url: `integrated/meetingRoom/app/booking-detail-page/${roomId}`,
|
||||
method: 'get',
|
||||
params: { date }
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import request from '/@/utils/request';
|
||||
|
||||
// 会议室预约统计查询参数
|
||||
export interface MeetingStatsQuery {
|
||||
startDate?: string; // 开始日期,格式:yyyy-MM-dd
|
||||
endDate?: string; // 结束日期,格式:yyyy-MM-dd
|
||||
dimension?: string; // 统计维度:dept-按部门统计,assignee-按负责人统计,time-按时间统计
|
||||
timeGranularity?: string; // 时间粒度:day-按天,week-按周,month-按月
|
||||
deptId?: number; // 部门ID,用于筛选特定部门
|
||||
roomId?: number; // 会议室ID,用于筛选特定会议室
|
||||
}
|
||||
|
||||
// 总体统计数据
|
||||
export interface OverallStats {
|
||||
totalBookings: number; // 总预约数
|
||||
approvedBookings: number; // 审核通过数
|
||||
pendingBookings: number; // 审核中数
|
||||
rejectedBookings: number; // 审核拒绝数
|
||||
cancelledBookings: number; // 已取消数
|
||||
approvalRate: number; // 通过率(百分比)
|
||||
avgDuration: number; // 平均会议时长(分钟)
|
||||
totalDuration: number; // 总会议时长(分钟)
|
||||
}
|
||||
|
||||
// 部门统计数据
|
||||
export interface DeptStats {
|
||||
deptId: number; // 部门ID
|
||||
deptName: string; // 部门名称(通过部门微服务获取)
|
||||
bookingCount: number; // 预约数量
|
||||
approvedCount: number; // 通过数量
|
||||
approvalRate: number; // 通过率
|
||||
totalDuration: number; // 总会议时长
|
||||
}
|
||||
|
||||
// 负责人统计数据
|
||||
export interface AssigneeStats {
|
||||
assigneeName: string; // 负责人姓名
|
||||
bookingCount: number; // 预约数量
|
||||
approvedCount: number; // 通过数量
|
||||
approvalRate: number; // 通过率
|
||||
totalDuration: number; // 总会议时长
|
||||
}
|
||||
|
||||
// 时间统计数据
|
||||
export interface TimeStats {
|
||||
timeLabel: string; // 时间标签(格式化后的时间显示)
|
||||
date?: string; // 日期(仅按天统计时有值)
|
||||
bookingCount: number; // 预约数量
|
||||
approvedCount: number; // 通过数量
|
||||
totalDuration: number; // 总会议时长
|
||||
}
|
||||
|
||||
// 状态统计数据
|
||||
export interface StatusStats {
|
||||
status: string; // 状态码(1-审核中,2-审核通过,3-审核拒绝,4-已取消,5-取消,0-已到访)
|
||||
statusName: string; // 状态名称
|
||||
count: number; // 数量
|
||||
percentage: number; // 占比(百分比)
|
||||
}
|
||||
|
||||
// 会议室统计数据
|
||||
export interface RoomStats {
|
||||
roomId: number; // 会议室ID
|
||||
roomName: string; // 会议室名称
|
||||
bookingCount: number; // 预约数量
|
||||
approvedCount: number; // 通过数量
|
||||
usageRate: number; // 使用率
|
||||
totalDuration: number; // 总会议时长
|
||||
}
|
||||
|
||||
// 统计结果数据
|
||||
export interface MeetingStatsData {
|
||||
overallStats: OverallStats; // 总体统计
|
||||
deptStats: DeptStats[]; // 部门统计
|
||||
assigneeStats: AssigneeStats[]; // 负责人统计
|
||||
timeStats: TimeStats[]; // 时间统计
|
||||
statusStats: StatusStats[]; // 状态统计
|
||||
roomStats: RoomStats[]; // 会议室统计
|
||||
}
|
||||
|
||||
// 统计结果响应
|
||||
export interface MeetingStatsResponse {
|
||||
code: number;
|
||||
msg: string;
|
||||
data: MeetingStatsData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会议室预约统计信息
|
||||
* @param params 查询参数
|
||||
* @returns 统计结果
|
||||
*/
|
||||
export function getMeetingStats(params?: MeetingStatsQuery): Promise<MeetingStatsResponse> {
|
||||
return request({
|
||||
url: '/integrated/meetingBooking/stats',
|
||||
method: 'get',
|
||||
params
|
||||
});
|
||||
}
|
||||
@@ -89,3 +89,5 @@ export function exportProfitMarginReport(params?: object) {
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column prop="createBy" label="申请人" show-overflow-tooltip/>
|
||||
<el-table-column prop="assigneeId" label="会议负责人" show-overflow-tooltip/>
|
||||
<el-table-column prop="bookingDate" label="开始时间" show-overflow-tooltip/>
|
||||
<el-table-column prop="bookingDate" label="预约日期" show-overflow-tooltip/>
|
||||
<el-table-column prop="startTime" label="开始时间" show-overflow-tooltip/>
|
||||
<el-table-column prop="endTime" label="结束时间" show-overflow-tooltip/>
|
||||
<el-table-column label="操作" width="150">
|
||||
|
||||
@@ -0,0 +1,337 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:model-value="visible"
|
||||
@update:model-value="(val) => emit('update:visible', val)"
|
||||
title="会议室预约详情"
|
||||
width="80%"
|
||||
:before-close="handleClose"
|
||||
>
|
||||
<div class="booking-detail-container">
|
||||
<!-- 查询条件 -->
|
||||
<el-form :model="queryForm" :inline="true" class="mb20">
|
||||
<el-form-item label="会议室">
|
||||
<el-input :value="roomName" disabled style="width: 200px;" />
|
||||
</el-form-item>
|
||||
<el-form-item label="查询日期">
|
||||
<el-date-picker
|
||||
v-model="queryForm.bookingDate"
|
||||
type="date"
|
||||
placeholder="请选择查询日期"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 200px"
|
||||
@change="loadBookingData"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="loadBookingData">查询</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 时间轴显示 -->
|
||||
<div class="time-schedule">
|
||||
<h3>{{ queryForm.bookingDate || '请选择日期' }} 的预约情况</h3>
|
||||
|
||||
<!-- 时间轴 -->
|
||||
<div class="time-axis">
|
||||
<div class="time-slots">
|
||||
<div
|
||||
v-for="timeSlot in timeSlots"
|
||||
:key="timeSlot.startTime"
|
||||
class="time-slot"
|
||||
:class="{
|
||||
'booked': timeSlot.status === 'booked',
|
||||
'reviewing': timeSlot.status === 'reviewing',
|
||||
'available': timeSlot.status === 'available',
|
||||
'current': timeSlot.isCurrent
|
||||
}"
|
||||
@click="handleTimeSlotClick(timeSlot)"
|
||||
>
|
||||
<div class="time-label">{{ timeSlot.startTime }} - {{ timeSlot.endTime }}</div>
|
||||
<div v-if="timeSlot.bookingInfo" class="booking-info">
|
||||
<div class="booking-title">{{ timeSlot.bookingInfo.title }}</div>
|
||||
<div class="booking-user">{{ timeSlot.bookingInfo.applicantName }}</div>
|
||||
<div class="booking-status">
|
||||
<el-tag :type="timeSlot.status === 'booked' ? 'success' : timeSlot.status === 'reviewing' ? 'warning' : 'info'">
|
||||
{{ timeSlot.statusText }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="available">
|
||||
{{ timeSlot.statusText }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 预约列表 -->
|
||||
<div class="booking-list" v-if="bookingList.length > 0">
|
||||
<h4>当日预约列表</h4>
|
||||
<el-table :data="bookingList" border>
|
||||
<el-table-column prop="title" label="会议主题" show-overflow-tooltip />
|
||||
<el-table-column prop="applicantName" label="申请人" show-overflow-tooltip />
|
||||
<el-table-column prop="startTime" label="开始时间" show-overflow-tooltip />
|
||||
<el-table-column prop="endTime" label="结束时间" show-overflow-tooltip />
|
||||
<el-table-column prop="status" label="状态" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
<el-tag :type="getStatusType(scope.row.status)">
|
||||
{{ getStatusText(scope.row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="description" label="会议说明" show-overflow-tooltip />
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 无预约提示 -->
|
||||
<div v-else-if="queryForm.bookingDate" class="no-booking">
|
||||
<el-empty description="当日暂无预约" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="() => emit('update:visible', false)">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, watch } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { getBookingDetailPage } from "/src/api/integrated/meetingRoom";
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
roomId: string;
|
||||
roomName: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
visible: false,
|
||||
roomId: '',
|
||||
roomName: ''
|
||||
});
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [value: boolean];
|
||||
}>();
|
||||
|
||||
// 响应式数据
|
||||
const queryForm = reactive({
|
||||
bookingDate: '',
|
||||
roomId: ''
|
||||
});
|
||||
|
||||
const bookingList = ref([]);
|
||||
const loading = ref(false);
|
||||
|
||||
// 时间槽位数据 - 直接使用API返回的数据
|
||||
const timeSlots = ref([]);
|
||||
|
||||
// 状态类型映射
|
||||
const getStatusType = (status: string) => {
|
||||
const statusMap = {
|
||||
'1': 'warning', // 审核中
|
||||
'2': 'success', // 审核通过
|
||||
'3': 'danger', // 审核拒绝
|
||||
'4': 'info' // 已取消
|
||||
};
|
||||
return statusMap[status] || 'info';
|
||||
};
|
||||
|
||||
// 状态文本映射
|
||||
const getStatusText = (status: string) => {
|
||||
const statusMap = {
|
||||
'1': '审核中',
|
||||
'2': '审核通过',
|
||||
'3': '审核拒绝',
|
||||
'4': '已取消'
|
||||
};
|
||||
return statusMap[status] || '未知状态';
|
||||
};
|
||||
|
||||
// 加载预约数据
|
||||
const loadBookingData = async () => {
|
||||
if (!queryForm.bookingDate) {
|
||||
ElMessage.warning('请选择查询日期');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
loading.value = true;
|
||||
// 调用新的API接口获取预约详情页面数据
|
||||
const response = await getBookingDetailPage(queryForm.roomId, queryForm.bookingDate);
|
||||
|
||||
// 更新时间段数据 - API返回的数据在data字段中
|
||||
timeSlots.value = response.data?.timeSlots || [];
|
||||
|
||||
// 更新预约列表数据
|
||||
bookingList.value = response.data?.dailyBookings || [];
|
||||
|
||||
// 更新房间名称(如果API返回了的话)
|
||||
if (response.data?.roomName) {
|
||||
// 可以通过emit通知父组件更新房间名称,或者直接更新props
|
||||
// emit('update:roomName', response.data.roomName);
|
||||
}
|
||||
|
||||
ElMessage.success('数据加载成功');
|
||||
} catch (error) {
|
||||
console.error('加载预约数据失败:', error);
|
||||
ElMessage.error('加载数据失败');
|
||||
|
||||
// 如果API调用失败,使用模拟数据作为备选
|
||||
const mockData = [
|
||||
{
|
||||
id: '1',
|
||||
title: '项目讨论会',
|
||||
applicantName: '张三',
|
||||
startTime: '09:00',
|
||||
endTime: '11:00',
|
||||
status: '2',
|
||||
statusText: '审核通过',
|
||||
description: '讨论项目进度'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: '技术评审',
|
||||
applicantName: '李四',
|
||||
startTime: '14:00',
|
||||
endTime: '16:00',
|
||||
status: '1',
|
||||
statusText: '审核中',
|
||||
description: '技术方案评审'
|
||||
}
|
||||
];
|
||||
|
||||
bookingList.value = mockData;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 时间槽点击事件
|
||||
const handleTimeSlotClick = (timeSlot: any) => {
|
||||
if (timeSlot.status === 'booked') {
|
||||
ElMessage.info(`该时间段已被预约: ${timeSlot.bookingInfo?.title || '未知会议'}`);
|
||||
} else if (timeSlot.status === 'reviewing') {
|
||||
ElMessage.info(`该时间段正在审核中: ${timeSlot.bookingInfo?.title || '未知会议'}`);
|
||||
} else {
|
||||
ElMessage.info(`该时间段可预约: ${timeSlot.startTime} - ${timeSlot.endTime}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭对话框
|
||||
const handleClose = () => {
|
||||
emit('update:visible', false);
|
||||
};
|
||||
|
||||
// 监听visible变化,初始化数据
|
||||
watch(() => props.visible, (newVal) => {
|
||||
if (newVal) {
|
||||
queryForm.roomId = props.roomId;
|
||||
queryForm.bookingDate = new Date().toISOString().split('T')[0]; // 默认今天
|
||||
loadBookingData();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.booking-detail-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.time-schedule {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.time-axis {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.time-slots {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.time-slot {
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
min-height: 80px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.time-slot:hover {
|
||||
border-color: #409eff;
|
||||
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.2);
|
||||
}
|
||||
|
||||
.time-slot.booked {
|
||||
background-color: #f0f9ff;
|
||||
border-color: #409eff;
|
||||
}
|
||||
|
||||
.time-slot.reviewing {
|
||||
background-color: #fefce8;
|
||||
border-color: #eab308;
|
||||
}
|
||||
|
||||
.time-slot.available {
|
||||
background-color: #f0fdf4;
|
||||
border-color: #22c55e;
|
||||
}
|
||||
|
||||
.time-slot.current {
|
||||
background-color: #fff7e6;
|
||||
border-color: #ff9500;
|
||||
}
|
||||
|
||||
.time-label {
|
||||
font-weight: bold;
|
||||
color: #606266;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.booking-info {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.booking-title {
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.booking-user {
|
||||
color: #606266;
|
||||
font-size: 12px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.available {
|
||||
color: #67c23a;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.booking-list {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.no-booking {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.mb20 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -37,8 +37,11 @@
|
||||
<!-- <el-table-column prop="availableTime" label="可用时间段" show-overflow-tooltip/>-->
|
||||
<!-- <el-table-column prop="cleanUserId" label="保洁人员" show-overflow-tooltip/>-->
|
||||
<!-- <el-table-column prop="serviceUserId" label="会务人员" show-overflow-tooltip/>-->
|
||||
<el-table-column label="操作" width="150">
|
||||
<el-table-column label="操作" width="200">
|
||||
<template #default="scope">
|
||||
<el-button icon="view" text type="primary" @click="viewBookingDetail(scope.row)">
|
||||
预约详情
|
||||
</el-button>
|
||||
<el-button icon="edit-pen" text type="primary" v-auth="'integrated_meetingRoom_edit'"
|
||||
@click="formDialogRef.openDialog(scope.row.id)">编辑</el-button>
|
||||
<el-button icon="delete" text type="primary" v-auth="'integrated_meetingRoom_del'" @click="handleDelete([scope.row.id])">删除</el-button>
|
||||
@@ -51,6 +54,13 @@
|
||||
<!-- 编辑、新增 -->
|
||||
<form-dialog ref="formDialogRef" @refresh="getDataList(false)" />
|
||||
|
||||
<!-- 预约详情 -->
|
||||
<BookingDetail
|
||||
v-model:visible="bookingDetailVisible"
|
||||
:room-id="selectedRoomId"
|
||||
:room-name="selectedRoomName"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -62,11 +72,17 @@ import { useDict } from '/src/hooks/dict';
|
||||
|
||||
// 引入组件
|
||||
const FormDialog = defineAsyncComponent(() => import('./form.vue'));
|
||||
const BookingDetail = defineAsyncComponent(() => import('./bookingDetail.vue'));
|
||||
|
||||
// 定义查询字典
|
||||
|
||||
const { meeting_status } = useDict('meeting_status')
|
||||
// 定义变量内容
|
||||
const formDialogRef = ref()
|
||||
// 预约详情相关
|
||||
const bookingDetailVisible = ref(false);
|
||||
const selectedRoomId = ref('');
|
||||
const selectedRoomName = ref('');
|
||||
// 搜索变量
|
||||
const queryRef = ref()
|
||||
const showSearch = ref(true)
|
||||
@@ -109,6 +125,13 @@ const selectionChangHandle = (objs: { id: string }[]) => {
|
||||
multiple.value = !objs.length;
|
||||
};
|
||||
|
||||
// 查看预约详情
|
||||
const viewBookingDetail = (row: any) => {
|
||||
selectedRoomId.value = row.id;
|
||||
selectedRoomName.value = row.roomName;
|
||||
bookingDetailVisible.value = true;
|
||||
};
|
||||
|
||||
// 删除操作
|
||||
const handleDelete = async (ids: string[]) => {
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,575 @@
|
||||
<template>
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<!-- 查询表单 -->
|
||||
<el-row v-show="showSearch">
|
||||
<el-form :model="queryForm" ref="queryRef" :inline="true" @keyup.enter="loadData">
|
||||
<el-form-item label="开始日期" prop="startDate">
|
||||
<el-date-picker
|
||||
v-model="queryForm.startDate"
|
||||
type="date"
|
||||
placeholder="请选择开始日期"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 160px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="结束日期" prop="endDate">
|
||||
<el-date-picker
|
||||
v-model="queryForm.endDate"
|
||||
type="date"
|
||||
placeholder="请选择结束日期"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 160px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<!-- <el-form-item label="统计维度" prop="dimension">
|
||||
<el-select v-model="queryForm.dimension" placeholder="请选择统计维度" style="width: 140px">
|
||||
<el-option label="部门统计" value="dept" />
|
||||
<el-option label="负责人统计" value="assignee" />
|
||||
<el-option label="时间统计" value="time" />
|
||||
</el-select>
|
||||
</el-form-item> -->
|
||||
<!-- <el-form-item label="时间粒度" prop="timeGranularity">
|
||||
<el-select v-model="queryForm.timeGranularity" placeholder="请选择时间粒度" style="width: 120px">
|
||||
<el-option label="按天" value="day" />
|
||||
<el-option label="按周" value="week" />
|
||||
<el-option label="按月" value="month" />
|
||||
</el-select>
|
||||
</el-form-item> -->
|
||||
<el-form-item>
|
||||
<el-button icon="search" type="primary" @click="loadData" :loading="loading">
|
||||
查询
|
||||
</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-row>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<el-row>
|
||||
<div class="mb8" style="width: 100%">
|
||||
<right-toolbar v-model:showSearch="showSearch" class="ml10 mr20" style="float: right;" />
|
||||
</div>
|
||||
</el-row>
|
||||
|
||||
<!-- 总体统计卡片 -->
|
||||
<el-row :gutter="20" class="mb20">
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">{{ statsData.overallStats?.totalBookings || 0 }}</div>
|
||||
<div class="stat-label">总预约数</div>
|
||||
</div>
|
||||
<div class="stat-icon">
|
||||
<el-icon><Calendar /></el-icon>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">{{ statsData.overallStats?.approvedBookings || 0 }}</div>
|
||||
<div class="stat-label">审核通过</div>
|
||||
</div>
|
||||
<div class="stat-icon">
|
||||
<el-icon><Check /></el-icon>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">{{ (statsData.overallStats?.approvalRate || 0).toFixed(1) }}%</div>
|
||||
<div class="stat-label">通过率</div>
|
||||
</div>
|
||||
<div class="stat-icon">
|
||||
<el-icon><TrendCharts /></el-icon>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">{{ getAvgDuration() }}</div>
|
||||
<div class="stat-label">平均时长(分钟)</div>
|
||||
</div>
|
||||
<div class="stat-icon">
|
||||
<el-icon><Timer /></el-icon>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 状态分布卡片 -->
|
||||
<el-row :gutter="20" class="mb20">
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">{{ statsData.overallStats?.pendingBookings || 0 }}</div>
|
||||
<div class="stat-label">审核中</div>
|
||||
</div>
|
||||
<div class="stat-icon">
|
||||
<el-icon><Clock /></el-icon>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">{{ statsData.overallStats?.rejectedBookings || 0 }}</div>
|
||||
<div class="stat-label">审核拒绝</div>
|
||||
</div>
|
||||
<div class="stat-icon">
|
||||
<el-icon><Close /></el-icon>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">{{ statsData.overallStats?.cancelledBookings || 0 }}</div>
|
||||
<div class="stat-label">已取消</div>
|
||||
</div>
|
||||
<div class="stat-icon">
|
||||
<el-icon><CircleClose /></el-icon>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">{{ Math.round(parseInt(statsData.overallStats?.totalDuration || '0') / 60) }}</div>
|
||||
<div class="stat-label">总时长(小时)</div>
|
||||
</div>
|
||||
<div class="stat-icon">
|
||||
<el-icon><Clock /></el-icon>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 图表展示 -->
|
||||
<el-row :gutter="20" class="mb20">
|
||||
<el-col :span="12">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>状态分布</span>
|
||||
</div>
|
||||
</template>
|
||||
<div ref="statusChartRef" style="height: 300px;"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>时间趋势</span>
|
||||
</div>
|
||||
</template>
|
||||
<div ref="timeChartRef" style="height: 300px;"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 数据表格 - 使用普通div替代el-card -->
|
||||
<div style="background: white; border: 1px solid #dcdfe6; border-radius: 4px; min-height: 400px; width: 100%; overflow-x: auto;">
|
||||
<!-- 卡片头部 -->
|
||||
<div style="padding: 18px 20px; border-bottom: 1px solid #ebeef5; background: #f5f7fa; width: 100%;">
|
||||
<div class="card-header">
|
||||
<span>详细统计</span>
|
||||
<el-radio-group v-model="currentTab" @change="handleTabChange">
|
||||
<el-radio-button label="dept">部门统计</el-radio-button>
|
||||
<!-- <el-radio-button label="assignee">负责人统计</el-radio-button> -->
|
||||
<el-radio-button label="time">时间统计</el-radio-button>
|
||||
<el-radio-button label="room">会议室统计</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 卡片内容区域 -->
|
||||
<div style="padding: 20px; background: white; width: 100%; overflow-x: auto;">
|
||||
|
||||
<!-- 部门统计表格 -->
|
||||
<div v-show="currentTab === 'dept'">
|
||||
<el-table :data="statsData.deptStats" v-loading="loading" border>
|
||||
<el-table-column prop="deptName" label="部门名称" show-overflow-tooltip />
|
||||
<el-table-column prop="bookingCount" label="预约数量" show-overflow-tooltip />
|
||||
<el-table-column prop="approvedCount" label="通过数量" show-overflow-tooltip />
|
||||
<el-table-column prop="approvalRate" label="通过率" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
{{ formatPercentage(scope.row.approvalRate) }}%
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="totalDuration" label="总时长(分钟)" show-overflow-tooltip />
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 负责人统计表格 -->
|
||||
<!-- <div v-show="currentTab === 'assignee'">
|
||||
<el-table :data="statsData.assigneeStats" v-loading="loading" border>
|
||||
<el-table-column prop="assigneeName" label="负责人姓名" show-overflow-tooltip />
|
||||
<el-table-column prop="bookingCount" label="预约数量" show-overflow-tooltip />
|
||||
<el-table-column prop="approvedCount" label="通过数量" show-overflow-tooltip />
|
||||
<el-table-column prop="approvalRate" label="通过率" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
{{ typeof scope.row.approvalRate === 'number' ? scope.row.approvalRate.toFixed(1) : parseFloat(scope.row.approvalRate || 0).toFixed(1) }}%
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="totalDuration" label="总时长(分钟)" show-overflow-tooltip />
|
||||
</el-table>
|
||||
</div> -->
|
||||
|
||||
<!-- 时间统计表格 -->
|
||||
<div v-show="currentTab === 'time'">
|
||||
<el-table :data="statsData.timeStats" v-loading="loading" border>
|
||||
<el-table-column prop="timeLabel" label="时间" show-overflow-tooltip />
|
||||
<el-table-column prop="bookingCount" label="预约数量" show-overflow-tooltip />
|
||||
<el-table-column prop="approvedCount" label="通过数量" show-overflow-tooltip />
|
||||
<el-table-column prop="totalDuration" label="总时长(分钟)" show-overflow-tooltip />
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 会议室统计表格 -->
|
||||
<div v-show="currentTab === 'room'">
|
||||
<el-table :data="statsData.roomStats" v-loading="loading" border>
|
||||
<el-table-column prop="roomName" label="会议室名称" show-overflow-tooltip />
|
||||
<el-table-column prop="bookingCount" label="预约数量" show-overflow-tooltip />
|
||||
<el-table-column prop="approvedCount" label="通过数量" show-overflow-tooltip />
|
||||
<el-table-column prop="usageRate" label="使用率" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
{{ formatPercentage(scope.row.usageRate) }}%
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="totalDuration" label="总时长(分钟)" show-overflow-tooltip />
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="MeetingSummary">
|
||||
import { ref, reactive, onMounted, nextTick } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { Calendar, Check, TrendCharts, Timer, Clock, Close, CircleClose } from '@element-plus/icons-vue';
|
||||
import * as echarts from 'echarts';
|
||||
import { getMeetingStats, type MeetingStatsQuery, type MeetingStatsData } from '/@/api/integrated/meetingStats';
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false);
|
||||
const showSearch = ref(true);
|
||||
const queryRef = ref();
|
||||
const currentTab = ref('dept');
|
||||
|
||||
// 查询表单
|
||||
const queryForm = reactive<MeetingStatsQuery>({
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
dimension: 'dept',
|
||||
timeGranularity: 'day'
|
||||
});
|
||||
|
||||
// 统计数据
|
||||
const statsData = ref<MeetingStatsData>({
|
||||
overallStats: {
|
||||
totalBookings: 0,
|
||||
approvedBookings: 0,
|
||||
pendingBookings: 0,
|
||||
rejectedBookings: 0,
|
||||
cancelledBookings: 0,
|
||||
approvalRate: 0,
|
||||
avgDuration: 0,
|
||||
totalDuration: 0
|
||||
},
|
||||
deptStats: [],
|
||||
assigneeStats: [],
|
||||
timeStats: [],
|
||||
statusStats: [],
|
||||
roomStats: []
|
||||
});
|
||||
|
||||
// 表格刷新key
|
||||
const tableKey = ref(0);
|
||||
|
||||
// 图表引用
|
||||
const statusChartRef = ref();
|
||||
const timeChartRef = ref();
|
||||
let statusChart: echarts.ECharts | null = null;
|
||||
let timeChart: echarts.ECharts | null = null;
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
const response = await getMeetingStats(queryForm);
|
||||
|
||||
if (response.code === 0) {
|
||||
// 直接使用API返回的数据,不做复杂处理
|
||||
statsData.value = response.data;
|
||||
|
||||
// 强制刷新表格
|
||||
tableKey.value++;
|
||||
|
||||
// 强制触发响应式更新
|
||||
await nextTick();
|
||||
|
||||
initCharts();
|
||||
} else {
|
||||
ElMessage.error(response.msg || '获取统计数据失败');
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('加载统计数据失败:', error);
|
||||
ElMessage.error(error.message || '加载统计数据失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 重置查询
|
||||
const resetQuery = () => {
|
||||
queryRef.value?.resetFields();
|
||||
queryForm.startDate = '';
|
||||
queryForm.endDate = '';
|
||||
queryForm.dimension = 'dept';
|
||||
queryForm.timeGranularity = 'day';
|
||||
loadData();
|
||||
};
|
||||
|
||||
// 切换标签页
|
||||
const handleTabChange = (value: string) => {
|
||||
currentTab.value = value;
|
||||
};
|
||||
|
||||
// 格式化百分比
|
||||
const formatPercentage = (value: any) => {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return '0.0';
|
||||
}
|
||||
|
||||
if (typeof value === 'number') {
|
||||
return value.toFixed(1);
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
const num = parseFloat(value);
|
||||
return isNaN(num) ? '0.0' : num.toFixed(1);
|
||||
}
|
||||
|
||||
return '0.0';
|
||||
};
|
||||
|
||||
// 获取平均时长
|
||||
const getAvgDuration = () => {
|
||||
const avgDuration = statsData.value.overallStats?.avgDuration;
|
||||
|
||||
// 处理 null 或 undefined 的情况
|
||||
if (!avgDuration) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 处理对象类型
|
||||
if (typeof avgDuration === 'object' && avgDuration !== null && avgDuration.parsedValue) {
|
||||
return Math.round(avgDuration.parsedValue);
|
||||
}
|
||||
|
||||
// 处理字符串类型
|
||||
if (typeof avgDuration === 'string') {
|
||||
return Math.round(parseFloat(avgDuration) || 0);
|
||||
}
|
||||
|
||||
// 处理数字类型
|
||||
if (typeof avgDuration === 'number') {
|
||||
return Math.round(avgDuration);
|
||||
}
|
||||
|
||||
// 默认返回0
|
||||
return 0;
|
||||
};
|
||||
|
||||
|
||||
// 初始化图表
|
||||
const initCharts = () => {
|
||||
initStatusChart();
|
||||
initTimeChart();
|
||||
};
|
||||
|
||||
// 初始化状态分布饼图
|
||||
const initStatusChart = () => {
|
||||
if (!statusChartRef.value) return;
|
||||
|
||||
if (statusChart) {
|
||||
statusChart.dispose();
|
||||
}
|
||||
|
||||
statusChart = echarts.init(statusChartRef.value);
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a} <br/>{b}: {c} ({d}%)'
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '预约状态',
|
||||
type: 'pie',
|
||||
radius: '50%',
|
||||
data: statsData.value.statusStats.map(item => ({
|
||||
value: parseInt(item.count || 0) || 0,
|
||||
name: item.statusName || '未知状态'
|
||||
})),
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
statusChart.setOption(option);
|
||||
};
|
||||
|
||||
// 初始化时间趋势折线图
|
||||
const initTimeChart = () => {
|
||||
if (!timeChartRef.value) return;
|
||||
|
||||
if (timeChart) {
|
||||
timeChart.dispose();
|
||||
}
|
||||
|
||||
timeChart = echarts.init(timeChartRef.value);
|
||||
|
||||
const timeData = statsData.value.timeStats || [];
|
||||
const dates = timeData.map(item => item.timeLabel || '未知日期');
|
||||
const bookingCounts = timeData.map(item => parseInt(item.bookingCount || 0) || 0);
|
||||
const approvedCounts = timeData.map(item => parseInt(item.approvedCount || 0) || 0);
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
legend: {
|
||||
data: ['预约数量', '通过数量']
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: dates
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '预约数量',
|
||||
type: 'line',
|
||||
data: bookingCounts,
|
||||
smooth: true
|
||||
},
|
||||
{
|
||||
name: '通过数量',
|
||||
type: 'line',
|
||||
data: approvedCounts,
|
||||
smooth: true
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
timeChart.setOption(option);
|
||||
};
|
||||
|
||||
// 组件挂载时加载数据
|
||||
onMounted(() => {
|
||||
loadData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.stat-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.stat-card :deep(.el-card__body) {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #409eff;
|
||||
line-height: 1;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
font-size: 40px;
|
||||
color: #409eff;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.mb20 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.mb8 {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.ml10 {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.mr20 {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
/* 确保页面可以滚动 */
|
||||
.layout-padding {
|
||||
overflow-x: auto !important;
|
||||
overflow-y: auto !important;
|
||||
max-width: 100% !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.layout-padding-auto {
|
||||
overflow-x: auto !important;
|
||||
overflow-y: auto !important;
|
||||
max-width: 100% !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.layout-padding-view {
|
||||
overflow-x: auto !important;
|
||||
overflow-y: auto !important;
|
||||
max-width: 100% !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
</style>
|
||||
@@ -339,3 +339,6 @@ const handleCurrentChange = (page: number) => {
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -191,3 +191,6 @@ defineExpose({
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -203,3 +203,6 @@ const trendClass = computed(() => {
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -7,3 +7,6 @@ export { default as SummaryCard } from './SummaryCard.vue';
|
||||
export { default as ProductRankingTable } from './ProductRankingTable.vue';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -207,3 +207,6 @@ export const SUCCESS_MESSAGES = {
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -254,7 +254,7 @@
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>畅销品TOP10</span>
|
||||
<!-- <el-button size="small" @click="viewAllTopProducts">查看全部</el-button> -->
|
||||
<el-button size="small" @click="viewAllTopProducts">查看全部</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-table :data="topSellingProducts" stripe v-loading="loading">
|
||||
@@ -299,7 +299,7 @@
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>滞销品TOP10</span>
|
||||
<!-- <el-button size="small" @click="viewAllSlowProducts">查看全部</el-button> -->
|
||||
<el-button size="small" @click="viewAllSlowProducts">查看全部</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-table :data="slowSellingProducts" stripe v-loading="loading">
|
||||
@@ -415,20 +415,11 @@
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- 商品排名详情弹窗 -->
|
||||
<!-- <ProductRankingDialog
|
||||
:visible="dialogVisible"
|
||||
:type="dialogType"
|
||||
:query-params="queryForm"
|
||||
@close="handleDialogClose"
|
||||
@view-detail="handleViewProductDetail"
|
||||
/> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="RealtimeSalesReport">
|
||||
import { ref, reactive, onMounted, nextTick, watch } from 'vue';
|
||||
import { ref, reactive, onMounted, nextTick } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import {
|
||||
Refresh,
|
||||
@@ -451,21 +442,15 @@ import {
|
||||
getCustomerAnalysis,
|
||||
refreshRealtimeData,
|
||||
} from '/@/api/supermarketerp/realtimeSalesReport';
|
||||
// import ProductRankingDialog from './components/ProductRankingDialog.vue';
|
||||
|
||||
// 响应式数据
|
||||
const refreshing = ref(false);
|
||||
const loading = ref(false);
|
||||
const isInitialized = ref(false);
|
||||
const debounceTimer = ref<number | null>(null);
|
||||
const debounceTimer = ref<NodeJS.Timeout | null>(null);
|
||||
const salesTrendChart = ref<HTMLElement>();
|
||||
const customerAnalysisChart = ref<HTMLElement>();
|
||||
|
||||
// 弹窗相关数据(已注释)
|
||||
// const dialogVisible = ref(false);
|
||||
// const dialogType = ref<'top' | 'slow'>('top');
|
||||
// const selectedProduct = ref(null);
|
||||
|
||||
// 查询表单
|
||||
const queryForm = reactive({
|
||||
queryType: 'day',
|
||||
@@ -479,16 +464,16 @@ const queryForm = reactive({
|
||||
});
|
||||
|
||||
// 高级选项显示状态
|
||||
const advancedOptionsVisible = ref<string[]>([]);
|
||||
const advancedOptionsVisible = ref([]);
|
||||
|
||||
// 数据
|
||||
const salesSummary = ref<any>();
|
||||
const topSellingProducts = ref<any[]>([]);
|
||||
const slowSellingProducts = ref<any[]>([]);
|
||||
const timePeriodSales = ref<any[]>([]);
|
||||
const customerAnalysis = ref<any>();
|
||||
const salesTrend = ref<any[]>([]);
|
||||
const expandedRows = ref<string[]>([]);
|
||||
const salesSummary = ref();
|
||||
const topSellingProducts = ref([]);
|
||||
const slowSellingProducts = ref([]);
|
||||
const timePeriodSales = ref([]);
|
||||
const customerAnalysis = ref();
|
||||
const salesTrend = ref([]);
|
||||
const expandedRows = ref([]);
|
||||
|
||||
// 图表实例
|
||||
let salesTrendChartInstance: echarts.ECharts | null = null;
|
||||
@@ -507,11 +492,9 @@ const initQueryForm = () => {
|
||||
queryForm.startDate = today.toISOString().slice(0, 10);
|
||||
queryForm.endDate = now.toISOString().slice(0, 10);
|
||||
} else if (queryForm.queryType === 'month') {
|
||||
// 确保按月查询时设置正确的默认值
|
||||
const thisMonth = now.toISOString().slice(0, 7);
|
||||
queryForm.startDate = thisMonth;
|
||||
queryForm.endDate = thisMonth;
|
||||
console.log('初始化按月查询默认值:', { startDate: queryForm.startDate, endDate: queryForm.endDate });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -613,99 +596,20 @@ watch(
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// 处理查询参数,确保日期格式正确
|
||||
const processQueryParams = (params: any) => {
|
||||
const processedParams = { ...params };
|
||||
|
||||
// 如果是按月查询,需要将YYYY-MM格式转换为完整的日期范围
|
||||
if (params.queryType === 'month') {
|
||||
if (params.startDate) {
|
||||
try {
|
||||
// 确保日期格式正确,处理可能的格式问题
|
||||
let dateStr = params.startDate;
|
||||
if (typeof dateStr === 'string' && dateStr.length === 7 && dateStr.includes('-')) {
|
||||
// 已经是 YYYY-MM 格式
|
||||
const startDate = new Date(dateStr + '-01');
|
||||
if (isNaN(startDate.getTime())) {
|
||||
throw new Error('Invalid start date');
|
||||
}
|
||||
processedParams.startDate = startDate.toISOString().slice(0, 10);
|
||||
console.log(`按月查询 - 开始日期转换: ${params.startDate} -> ${processedParams.startDate}`);
|
||||
} else {
|
||||
console.warn('按月查询 - 开始日期格式不正确:', params.startDate);
|
||||
// 如果格式不正确,尝试使用当前月份
|
||||
const now = new Date();
|
||||
const currentMonth = now.toISOString().slice(0, 7);
|
||||
const fallbackDate = new Date(currentMonth + '-01');
|
||||
processedParams.startDate = fallbackDate.toISOString().slice(0, 10);
|
||||
console.log(`按月查询 - 使用默认开始日期: ${processedParams.startDate}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('按月查询 - 开始日期转换失败:', error);
|
||||
// 使用当前月份作为默认值
|
||||
const now = new Date();
|
||||
const currentMonth = now.toISOString().slice(0, 7);
|
||||
const fallbackDate = new Date(currentMonth + '-01');
|
||||
processedParams.startDate = fallbackDate.toISOString().slice(0, 10);
|
||||
}
|
||||
}
|
||||
|
||||
if (params.endDate) {
|
||||
try {
|
||||
// 确保日期格式正确,处理可能的格式问题
|
||||
let dateStr = params.endDate;
|
||||
if (typeof dateStr === 'string' && dateStr.length === 7 && dateStr.includes('-')) {
|
||||
// 已经是 YYYY-MM 格式
|
||||
const endDate = new Date(dateStr + '-01');
|
||||
if (isNaN(endDate.getTime())) {
|
||||
throw new Error('Invalid end date');
|
||||
}
|
||||
endDate.setMonth(endDate.getMonth() + 1, 0); // 设置为下个月的第0天,即当前月的最后一天
|
||||
processedParams.endDate = endDate.toISOString().slice(0, 10);
|
||||
console.log(`按月查询 - 结束日期转换: ${params.endDate} -> ${processedParams.endDate}`);
|
||||
} else {
|
||||
console.warn('按月查询 - 结束日期格式不正确:', params.endDate);
|
||||
// 如果格式不正确,尝试使用当前月份
|
||||
const now = new Date();
|
||||
const currentMonth = now.toISOString().slice(0, 7);
|
||||
const fallbackDate = new Date(currentMonth + '-01');
|
||||
fallbackDate.setMonth(fallbackDate.getMonth() + 1, 0);
|
||||
processedParams.endDate = fallbackDate.toISOString().slice(0, 10);
|
||||
console.log(`按月查询 - 使用默认结束日期: ${processedParams.endDate}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('按月查询 - 结束日期转换失败:', error);
|
||||
// 使用当前月份作为默认值
|
||||
const now = new Date();
|
||||
const currentMonth = now.toISOString().slice(0, 7);
|
||||
const fallbackDate = new Date(currentMonth + '-01');
|
||||
fallbackDate.setMonth(fallbackDate.getMonth() + 1, 0);
|
||||
processedParams.endDate = fallbackDate.toISOString().slice(0, 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('处理后的查询参数:', processedParams);
|
||||
return processedParams;
|
||||
};
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
// 处理查询参数
|
||||
const processedParams = processQueryParams(queryForm);
|
||||
|
||||
// 加载完整报表数据
|
||||
const reportData = await getRealtimeSalesReport(processedParams);
|
||||
const reportData = await getRealtimeSalesReport(queryForm);
|
||||
|
||||
if (reportData.code === 0 && reportData.data) {
|
||||
// 使用后端返回的数据
|
||||
salesSummary.value = reportData.data.salesSummary || {};
|
||||
salesSummary.value = reportData.data.salesSummary || getMockSalesSummary();
|
||||
topSellingProducts.value = reportData.data.topSellingProducts || [];
|
||||
slowSellingProducts.value = reportData.data.slowSellingProducts || [];
|
||||
timePeriodSales.value = reportData.data.timePeriodSales || [];
|
||||
customerAnalysis.value = reportData.data.customerAnalysis || {};
|
||||
customerAnalysis.value = reportData.data.customerAnalysis || getMockCustomerAnalysis();
|
||||
salesTrend.value = reportData.data.salesTrend || [];
|
||||
|
||||
ElMessage.success('数据加载成功');
|
||||
@@ -718,31 +622,245 @@ const loadData = async () => {
|
||||
renderCharts();
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error);
|
||||
// 清空数据
|
||||
salesSummary.value = {};
|
||||
topSellingProducts.value = [];
|
||||
slowSellingProducts.value = [];
|
||||
timePeriodSales.value = [];
|
||||
customerAnalysis.value = {};
|
||||
salesTrend.value = [];
|
||||
// 使用模拟数据
|
||||
salesSummary.value = getMockSalesSummary();
|
||||
topSellingProducts.value = getMockTopProducts();
|
||||
slowSellingProducts.value = getMockSlowProducts();
|
||||
timePeriodSales.value = getMockTimePeriodSales();
|
||||
customerAnalysis.value = getMockCustomerAnalysis();
|
||||
salesTrend.value = []; // 空数组,不显示模拟数据
|
||||
|
||||
await nextTick();
|
||||
renderCharts();
|
||||
|
||||
// 根据错误类型显示不同的提示信息
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
if (errorMessage.includes('权限')) {
|
||||
if (error.message && error.message.includes('权限')) {
|
||||
ElMessage.error('权限不足,无法访问即时销售报表');
|
||||
} else if (errorMessage.includes('网络')) {
|
||||
} else if (error.message && error.message.includes('网络')) {
|
||||
ElMessage.error('网络连接异常,请检查网络设置');
|
||||
} else {
|
||||
ElMessage.error('数据加载失败,请稍后重试');
|
||||
ElMessage.warning('数据加载失败,使用模拟数据展示');
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 模拟数据生成函数 - 根据后端API文档调整
|
||||
const getMockSalesSummary = () => ({
|
||||
totalSalesAmount: 12500.50,
|
||||
totalSalesCount: 45,
|
||||
averageCustomerValue: 277.79,
|
||||
productVarietyCount: 23,
|
||||
totalSalesQuantity: 156.5,
|
||||
salesGrowthRate: 15.5,
|
||||
salesGrowthRateMonth: 8.2,
|
||||
realtimeStatus: 'MEDIUM',
|
||||
});
|
||||
|
||||
const getMockTopProducts = () => [
|
||||
{
|
||||
productId: 1001,
|
||||
productName: '苹果',
|
||||
productCode: 'P001',
|
||||
salesAmount: 1250.00,
|
||||
salesQuantity: 50.0,
|
||||
salesCount: 12,
|
||||
averagePrice: 25.00,
|
||||
ranking: 1,
|
||||
stockQuantity: 200.0,
|
||||
stockStatus: 'SUFFICIENT',
|
||||
growthRate: 12.5
|
||||
},
|
||||
{
|
||||
productId: 1002,
|
||||
productName: '牛奶',
|
||||
productCode: 'M002',
|
||||
salesAmount: 980.00,
|
||||
salesQuantity: 49.0,
|
||||
salesCount: 10,
|
||||
averagePrice: 20.00,
|
||||
ranking: 2,
|
||||
stockQuantity: 150.0,
|
||||
stockStatus: 'SUFFICIENT',
|
||||
growthRate: 8.3
|
||||
},
|
||||
{
|
||||
productId: 1003,
|
||||
productName: '面包',
|
||||
productCode: 'B003',
|
||||
salesAmount: 850.00,
|
||||
salesQuantity: 34.0,
|
||||
salesCount: 8,
|
||||
averagePrice: 25.00,
|
||||
ranking: 3,
|
||||
stockQuantity: 5.0,
|
||||
stockStatus: 'LOW',
|
||||
growthRate: 15.2
|
||||
},
|
||||
{
|
||||
productId: 1004,
|
||||
productName: '矿泉水',
|
||||
productCode: 'W004',
|
||||
salesAmount: 720.00,
|
||||
salesQuantity: 36.0,
|
||||
salesCount: 7,
|
||||
averagePrice: 20.00,
|
||||
ranking: 4,
|
||||
stockQuantity: 0.0,
|
||||
stockStatus: 'OUT',
|
||||
growthRate: 6.7
|
||||
},
|
||||
{
|
||||
productId: 1005,
|
||||
productName: '鸡蛋',
|
||||
productCode: 'E005',
|
||||
salesAmount: 680.00,
|
||||
salesQuantity: 68.0,
|
||||
salesCount: 6,
|
||||
averagePrice: 10.00,
|
||||
ranking: 5,
|
||||
stockQuantity: 100.0,
|
||||
stockStatus: 'SUFFICIENT',
|
||||
growthRate: 4.2
|
||||
},
|
||||
];
|
||||
|
||||
const getMockSlowProducts = () => [
|
||||
{
|
||||
productId: 2001,
|
||||
productName: '高级红酒',
|
||||
productCode: 'W001',
|
||||
salesAmount: 120.00,
|
||||
salesQuantity: 1.2,
|
||||
salesCount: 1,
|
||||
averagePrice: 100.00,
|
||||
ranking: 1,
|
||||
stockQuantity: 50.0,
|
||||
stockStatus: 'SUFFICIENT',
|
||||
growthRate: -15.3
|
||||
},
|
||||
{
|
||||
productId: 2002,
|
||||
productName: '进口巧克力',
|
||||
productCode: 'C002',
|
||||
salesAmount: 80.00,
|
||||
salesQuantity: 2.0,
|
||||
salesCount: 2,
|
||||
averagePrice: 40.00,
|
||||
ranking: 2,
|
||||
stockQuantity: 8.0,
|
||||
stockStatus: 'LOW',
|
||||
growthRate: -8.7
|
||||
},
|
||||
{
|
||||
productId: 2003,
|
||||
productName: '有机蔬菜',
|
||||
productCode: 'V003',
|
||||
salesAmount: 60.00,
|
||||
salesQuantity: 3.0,
|
||||
salesCount: 1,
|
||||
averagePrice: 20.00,
|
||||
ranking: 3,
|
||||
stockQuantity: 15.0,
|
||||
stockStatus: 'SUFFICIENT',
|
||||
growthRate: -12.1
|
||||
},
|
||||
{
|
||||
productId: 2004,
|
||||
productName: '高端茶叶',
|
||||
productCode: 'T004',
|
||||
salesAmount: 40.00,
|
||||
salesQuantity: 0.8,
|
||||
salesCount: 1,
|
||||
averagePrice: 50.00,
|
||||
ranking: 4,
|
||||
stockQuantity: 0.0,
|
||||
stockStatus: 'OUT',
|
||||
growthRate: -20.5
|
||||
},
|
||||
{
|
||||
productId: 2005,
|
||||
productName: '进口咖啡',
|
||||
productCode: 'C005',
|
||||
salesAmount: 30.00,
|
||||
salesQuantity: 0.6,
|
||||
salesCount: 1,
|
||||
averagePrice: 50.00,
|
||||
ranking: 5,
|
||||
stockQuantity: 2.0,
|
||||
stockStatus: 'LOW',
|
||||
growthRate: -25.8
|
||||
},
|
||||
];
|
||||
|
||||
const getMockTimePeriodSales = () => {
|
||||
const data = [];
|
||||
const products = [
|
||||
{ name: '苹果', code: 'P001', category: '水果', unit: 'kg', unitName: 'kg' },
|
||||
{ name: '牛奶', code: 'M002', category: '乳制品', unit: '瓶', unitName: '瓶' },
|
||||
{ name: '面包', code: 'B003', category: '烘焙', unit: '个', unitName: '个' },
|
||||
{ name: '矿泉水', code: 'W004', category: '饮料', unit: '瓶', unitName: '瓶' },
|
||||
{ name: '鸡蛋', code: 'E005', category: '蛋类', unit: '个', unitName: '个' },
|
||||
];
|
||||
|
||||
for (let i = 0; i < 24; i++) {
|
||||
const productCount = Math.floor(Math.random() * 3) + 1;
|
||||
const productDetails = [];
|
||||
let totalAmount = 0;
|
||||
let totalQuantity = 0;
|
||||
|
||||
for (let j = 0; j < productCount; j++) {
|
||||
const product = products[Math.floor(Math.random() * products.length)];
|
||||
const quantity = Math.floor(Math.random() * 5) + 1;
|
||||
const unitPrice = Math.floor(Math.random() * 50) + 10;
|
||||
const amount = quantity * unitPrice;
|
||||
|
||||
productDetails.push({
|
||||
productId: `mock_${i}_${j}`,
|
||||
productName: product.name,
|
||||
productCode: product.code,
|
||||
categoryId: `cat_${j}`,
|
||||
categoryName: product.category,
|
||||
unit: product.unit,
|
||||
unitName: product.unitName,
|
||||
quantity: quantity,
|
||||
unitPrice: unitPrice,
|
||||
totalAmount: amount,
|
||||
salesCount: 1,
|
||||
});
|
||||
|
||||
totalAmount += amount;
|
||||
totalQuantity += quantity;
|
||||
}
|
||||
|
||||
data.push({
|
||||
timeLabel: `${i.toString().padStart(2, '0')}:00`,
|
||||
salesAmount: totalAmount,
|
||||
salesCount: productCount,
|
||||
customerValue: totalAmount / productCount,
|
||||
salesQuantity: totalQuantity,
|
||||
growthRate: Math.floor(Math.random() * 20) - 10,
|
||||
productDetails: productDetails,
|
||||
});
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
const getMockCustomerAnalysis = () => ({
|
||||
averageCustomerValue: 277.79,
|
||||
maxCustomerValue: 850.00,
|
||||
minCustomerValue: 15.50,
|
||||
medianCustomerValue: 250.00,
|
||||
highValueOrderRatio: 25.5,
|
||||
valueRanges: [
|
||||
{ rangeLabel: '0-50元', orderCount: 8, percentage: 17.78 },
|
||||
{ rangeLabel: '50-100元', orderCount: 12, percentage: 26.67 },
|
||||
{ rangeLabel: '100-200元', orderCount: 15, percentage: 33.33 },
|
||||
{ rangeLabel: '200-500元', orderCount: 8, percentage: 17.78 },
|
||||
{ rangeLabel: '500+元', orderCount: 2, percentage: 4.44 },
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
// 刷新数据
|
||||
@@ -774,48 +892,30 @@ const refreshCharts = async () => {
|
||||
// 刷新时段数据
|
||||
const refreshTimePeriodData = async () => {
|
||||
try {
|
||||
const processedParams = processQueryParams(queryForm);
|
||||
const timePeriodData = await getTimePeriodSales({
|
||||
...processedParams,
|
||||
...queryForm,
|
||||
current: 1,
|
||||
size: 50,
|
||||
});
|
||||
timePeriodSales.value = timePeriodData.data?.records || [];
|
||||
timePeriodSales.value = timePeriodData.data?.records || getMockTimePeriodSales();
|
||||
ElMessage.success('时段数据刷新成功');
|
||||
} catch (error) {
|
||||
console.error('刷新时段数据失败:', error);
|
||||
timePeriodSales.value = [];
|
||||
ElMessage.error('时段数据刷新失败');
|
||||
timePeriodSales.value = getMockTimePeriodSales();
|
||||
ElMessage.warning('使用模拟数据');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 查看全部畅销品(已注释)
|
||||
// const viewAllTopProducts = () => {
|
||||
// dialogType.value = 'top';
|
||||
// dialogVisible.value = true;
|
||||
// };
|
||||
|
||||
// 查看全部滞销品(已注释)
|
||||
// const viewAllSlowProducts = () => {
|
||||
// dialogType.value = 'slow';
|
||||
// dialogVisible.value = true;
|
||||
// };
|
||||
|
||||
// 关闭弹窗(已注释)
|
||||
// const handleDialogClose = () => {
|
||||
// dialogVisible.value = false;
|
||||
// selectedProduct.value = null;
|
||||
// };
|
||||
|
||||
// 查看商品详情(已注释)
|
||||
// const handleViewProductDetail = (product: any) => {
|
||||
// selectedProduct.value = product;
|
||||
// ElMessage.info(`查看商品详情: ${product.productName}`);
|
||||
// // 这里可以打开商品详情弹窗或跳转到详情页面
|
||||
// console.log('商品详情:', product);
|
||||
// };
|
||||
// 查看全部畅销品
|
||||
const viewAllTopProducts = () => {
|
||||
ElMessage.info('查看全部畅销品功能开发中...');
|
||||
};
|
||||
|
||||
// 查看全部滞销品
|
||||
const viewAllSlowProducts = () => {
|
||||
ElMessage.info('查看全部滞销品功能开发中...');
|
||||
};
|
||||
|
||||
// 渲染销售趋势图表
|
||||
const renderSalesTrendChart = () => {
|
||||
@@ -826,9 +926,9 @@ const renderSalesTrendChart = () => {
|
||||
}
|
||||
|
||||
// 使用后端返回的销售趋势数据
|
||||
let xAxisData: string[] = [];
|
||||
let salesData: number[] = [];
|
||||
let countData: number[] = [];
|
||||
let xAxisData = [];
|
||||
let salesData = [];
|
||||
let countData = [];
|
||||
|
||||
if (salesTrend.value && salesTrend.value.length > 0) {
|
||||
// 使用后端返回的真实数据
|
||||
@@ -920,22 +1020,28 @@ const renderCustomerAnalysisChart = () => {
|
||||
}
|
||||
|
||||
// 使用后端数据结构
|
||||
let data: any[] = [];
|
||||
let data = [];
|
||||
if (customerAnalysis.value && customerAnalysis.value.valueRanges) {
|
||||
data = customerAnalysis.value.valueRanges.map((range: any) => ({
|
||||
data = customerAnalysis.value.valueRanges.map(range => ({
|
||||
name: range.rangeLabel,
|
||||
value: range.orderCount,
|
||||
percentage: range.percentage
|
||||
}));
|
||||
} else {
|
||||
// 没有数据时显示空数组
|
||||
data = [];
|
||||
// 模拟数据
|
||||
data = [
|
||||
{ name: '0-50元', value: 25, percentage: 15.5 },
|
||||
{ name: '50-100元', value: 35, percentage: 21.7 },
|
||||
{ name: '100-200元', value: 20, percentage: 12.4 },
|
||||
{ name: '200-500元', value: 15, percentage: 9.3 },
|
||||
{ name: '500+元', value: 5, percentage: 3.1 },
|
||||
];
|
||||
}
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: function(params: any) {
|
||||
formatter: function(params) {
|
||||
return `${params.name}<br/>订单数: ${params.value}<br/>占比: ${params.data.percentage}%`;
|
||||
},
|
||||
},
|
||||
@@ -1065,6 +1171,68 @@ const getNumericValue = (value: any) => {
|
||||
};
|
||||
|
||||
|
||||
// 根据类型生成模拟时段数据
|
||||
const getMockTimePeriodSalesByType = (type: string) => {
|
||||
const data = [];
|
||||
|
||||
if (type === 'hour') {
|
||||
// 按小时生成24小时数据
|
||||
for (let i = 0; i < 24; i++) {
|
||||
data.push({
|
||||
timePeriod: `${i}:00`,
|
||||
timePeriodName: `${i}:00-${i + 1}:00`,
|
||||
salesAmount: Math.floor(Math.random() * 5000) + 1000,
|
||||
salesCount: Math.floor(Math.random() * 50) + 10,
|
||||
salesQuantity: Math.floor(Math.random() * 100) + 20,
|
||||
averageCustomerPrice: Math.floor(Math.random() * 50) + 50,
|
||||
customerCount: Math.floor(Math.random() * 20) + 5,
|
||||
productVarietyCount: Math.floor(Math.random() * 30) + 10,
|
||||
yearOverYearSalesAmount: Math.floor(Math.random() * 1000) - 500,
|
||||
monthOverMonthSalesAmount: Math.floor(Math.random() * 500) - 250,
|
||||
});
|
||||
}
|
||||
} else if (type === 'day') {
|
||||
// 按天生成7天数据
|
||||
for (let i = 6; i >= 0; i--) {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - i);
|
||||
const dateStr = date.toISOString().slice(0, 10);
|
||||
data.push({
|
||||
timePeriod: dateStr,
|
||||
timePeriodName: dateStr,
|
||||
salesAmount: Math.floor(Math.random() * 20000) + 5000,
|
||||
salesCount: Math.floor(Math.random() * 200) + 50,
|
||||
salesQuantity: Math.floor(Math.random() * 500) + 100,
|
||||
averageCustomerPrice: Math.floor(Math.random() * 100) + 50,
|
||||
customerCount: Math.floor(Math.random() * 50) + 20,
|
||||
productVarietyCount: Math.floor(Math.random() * 50) + 30,
|
||||
yearOverYearSalesAmount: Math.floor(Math.random() * 5000) - 2500,
|
||||
monthOverMonthSalesAmount: Math.floor(Math.random() * 2000) - 1000,
|
||||
});
|
||||
}
|
||||
} else if (type === 'month') {
|
||||
// 按月生成12个月数据
|
||||
for (let i = 11; i >= 0; i--) {
|
||||
const date = new Date();
|
||||
date.setMonth(date.getMonth() - i);
|
||||
const monthStr = date.toISOString().slice(0, 7);
|
||||
data.push({
|
||||
timePeriod: monthStr,
|
||||
timePeriodName: monthStr,
|
||||
salesAmount: Math.floor(Math.random() * 100000) + 50000,
|
||||
salesCount: Math.floor(Math.random() * 1000) + 500,
|
||||
salesQuantity: Math.floor(Math.random() * 2000) + 1000,
|
||||
averageCustomerPrice: Math.floor(Math.random() * 200) + 100,
|
||||
customerCount: Math.floor(Math.random() * 200) + 100,
|
||||
productVarietyCount: Math.floor(Math.random() * 100) + 50,
|
||||
yearOverYearSalesAmount: Math.floor(Math.random() * 20000) - 10000,
|
||||
monthOverMonthSalesAmount: Math.floor(Math.random() * 10000) - 5000,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
// 组件挂载
|
||||
onMounted(async () => {
|
||||
|
||||
@@ -136,3 +136,5 @@ export function formatTimePeriodDataForExport(data: any[]) {
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -196,3 +196,6 @@ export const TABLE_CONFIG = {
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -366,3 +366,5 @@ class ProfitMarginReportServiceTest {
|
||||
|
||||
这样重构后的代码将更加清晰、可维护,并且性能更好。
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user