forked from lxbfYeaaGbeDLMCi/deShanXiao
初始版本,目前线上可用
This commit is contained in:
498
frontEnd/src/pages/statistics/sales/sales.vue
Normal file
498
frontEnd/src/pages/statistics/sales/sales.vue
Normal file
@@ -0,0 +1,498 @@
|
||||
<template>
|
||||
<div style="background-color: #fff; padding: 15px">
|
||||
<baseTableHeader title="销售统计报表" @resetSearch="reset" @search="query">
|
||||
<template #content>
|
||||
<el-form v-model="searchForm">
|
||||
<el-row :gutter="25">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="统计日期">
|
||||
<el-date-picker
|
||||
v-model="searchForm.dateRange"
|
||||
type="datetimerange"
|
||||
range-separator="至"
|
||||
start-placeholder="选择开始日期"
|
||||
end-placeholder="选择结束日期"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
@change="dateChange" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="服务名称">
|
||||
<el-input
|
||||
v-model="searchForm.serviceName"
|
||||
placeholder="输入服务名称" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="项目分类">
|
||||
<el-select
|
||||
v-model="searchForm.categoryName"
|
||||
placeholder="选择分类"
|
||||
clearable>
|
||||
<el-option
|
||||
v-for="category in categories"
|
||||
:key="category.id"
|
||||
:label="category.name"
|
||||
:value="category.name" />
|
||||
<el-option label="其他服务" value="其他服务" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<!-- <div>
|
||||
<el-button
|
||||
type="primary"
|
||||
style="margin-left: 15px"
|
||||
@click="query"
|
||||
:loading="loading">
|
||||
查询
|
||||
</el-button>
|
||||
<el-button type="warning" @click="reset">重置</el-button>
|
||||
</div> -->
|
||||
</el-row>
|
||||
</el-form>
|
||||
</template>
|
||||
<template #operateBtns>
|
||||
<el-button size="small" v-print="print"
|
||||
><img
|
||||
src="/assets/icon/打印机.svg"
|
||||
style="margin-right: 5px"
|
||||
width="20"
|
||||
height="20" />打印单据</el-button
|
||||
>
|
||||
<el-button @click="exportTableToExcel"
|
||||
><img
|
||||
src="/assets/images/Excel.svg"
|
||||
alt=""
|
||||
width="20"
|
||||
height="20" />导出</el-button
|
||||
>
|
||||
</template>
|
||||
</baseTableHeader>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="loading-mask">
|
||||
<el-icon class="is-loading" color="#409EFC" :size="30">
|
||||
<Loading />
|
||||
</el-icon>
|
||||
<span style="margin-left: 10px">数据加载中...</span>
|
||||
</div>
|
||||
<div id="print-container">
|
||||
<h2
|
||||
style="
|
||||
color: #000;
|
||||
text-align: center;
|
||||
font-size: 20pt;
|
||||
padding: 15px;
|
||||
font-weight: bold;
|
||||
">
|
||||
销售统计报表
|
||||
</h2>
|
||||
<table
|
||||
v-show="!loading"
|
||||
style="
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-family: 宋体;
|
||||
">
|
||||
<!-- 表头 -->
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="border: 1px solid #000; padding: 8px">序号</th>
|
||||
<th style="border: 1px solid #000; padding: 8px">服务项目</th>
|
||||
<th style="border: 1px solid #000; padding: 8px">单价(元)</th>
|
||||
<th style="border: 1px solid #000; padding: 8px">数量</th>
|
||||
<th style="border: 1px solid #000; padding: 8px">小计(元)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- 新增空状态 -->
|
||||
<tr v-if="showEmpty">
|
||||
<td
|
||||
colspan="4"
|
||||
style="text-align: center; color: #999; padding: 20px">
|
||||
未查询到相关数据
|
||||
</td>
|
||||
</tr>
|
||||
<!-- 分类数据 -->
|
||||
<template
|
||||
v-for="category in statsData.categories"
|
||||
:key="category.categoryName">
|
||||
<tr>
|
||||
<td
|
||||
colspan="5"
|
||||
style="font-weight: bold; padding: 8px; text-align: center">
|
||||
{{ category.categoryName }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
v-for="(service, index) in category.services"
|
||||
:key="service.serviceName">
|
||||
<td style="padding: 8px">{{ index + 1 }}</td>
|
||||
<td style="border: 1px solid #000; padding: 8px">
|
||||
{{ service.serviceName }}
|
||||
</td>
|
||||
<td
|
||||
style="border: 1px solid #000; text-align: right; padding: 8px">
|
||||
{{ service.price.toFixed(2) }}
|
||||
</td>
|
||||
<td
|
||||
style="border: 1px solid #000; text-align: right; padding: 8px">
|
||||
{{ service.quantity }}
|
||||
</td>
|
||||
<td
|
||||
style="border: 1px solid #000; text-align: right; padding: 8px">
|
||||
{{ service.subtotal.toFixed(2) }}
|
||||
</td>
|
||||
</tr>
|
||||
<!-- <tr style="font-weight: bold">
|
||||
<td colspan="2" style="border: 1px solid #000; padding: 8px">
|
||||
分类合计
|
||||
</td>
|
||||
<td
|
||||
style="border: 1px solid #000; text-align: right; padding: 8px">
|
||||
{{ category.totalQuantity }}
|
||||
</td>
|
||||
<td
|
||||
style="border: 1px solid #000; text-align: right; padding: 8px">
|
||||
{{ category.totalAmount.toFixed(2) }}
|
||||
</td>
|
||||
</tr> -->
|
||||
</template>
|
||||
|
||||
<!-- 其他服务 -->
|
||||
<tr v-if="statsData.otherCategory.services.length">
|
||||
<td colspan="5" style="font-weight: bold; padding: 8px">
|
||||
{{ statsData.otherCategory.categoryName }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
v-if="statsData.otherCategory.services.length"
|
||||
v-for="(service, index) in statsData.otherCategory.services"
|
||||
:key="'other-' + service.serviceName">
|
||||
<td style="padding: 8px">{{ index + 1 }}</td>
|
||||
<td style="border: 1px solid #000; padding: 8px">
|
||||
{{ service.serviceName }}
|
||||
</td>
|
||||
<td style="border: 1px solid #000; text-align: right; padding: 8px">
|
||||
{{ service.price.toFixed(2) }}
|
||||
</td>
|
||||
<td style="border: 1px solid #000; text-align: right; padding: 8px">
|
||||
{{ service.quantity }}
|
||||
</td>
|
||||
<td style="border: 1px solid #000; text-align: right; padding: 8px">
|
||||
{{ service.subtotal.toFixed(2) }}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- 总计行 -->
|
||||
<tr>
|
||||
<td
|
||||
colspan="3"
|
||||
style="
|
||||
border: 1px solid #000;
|
||||
padding: 8px;
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
">
|
||||
数量和金额总计
|
||||
</td>
|
||||
<td
|
||||
style="
|
||||
border: 1px solid #000;
|
||||
text-align: right;
|
||||
padding: 8px;
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
">
|
||||
{{ statsData.grandTotalQuantity }}
|
||||
</td>
|
||||
<td
|
||||
style="
|
||||
border: 1px solid #000;
|
||||
text-align: right;
|
||||
padding: 8px;
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
">
|
||||
{{ statsData.grandTotalAmount.toFixed(2) }} 元
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { Loading } from "@element-plus/icons-vue";
|
||||
import request from "@/lib/request";
|
||||
import dayjs from "dayjs";
|
||||
import * as XLSX from "xlsx";
|
||||
|
||||
interface ServiceStatsItem {
|
||||
serviceName: string;
|
||||
price: number;
|
||||
unit: string;
|
||||
quantity: number;
|
||||
subtotal: number;
|
||||
}
|
||||
|
||||
interface CategoryStats {
|
||||
categoryName: string;
|
||||
services: ServiceStatsItem[];
|
||||
totalQuantity: number;
|
||||
totalAmount: number;
|
||||
}
|
||||
|
||||
interface StatsResponse {
|
||||
categories: CategoryStats[];
|
||||
otherCategory: CategoryStats;
|
||||
grandTotalQuantity: number;
|
||||
grandTotalAmount: number;
|
||||
}
|
||||
const print = {
|
||||
id: "print-container",
|
||||
};
|
||||
// 保持原有响应式数据结构
|
||||
const loading = ref(false);
|
||||
const searchForm = ref({
|
||||
dateRange: [
|
||||
dayjs().startOf("day").format("YYYY-MM-DD 00:00:00"),
|
||||
dayjs(new Date()).format("YYYY-MM-DD HH:mm:ss"),
|
||||
] as string[],
|
||||
serviceName: "",
|
||||
categoryName: "",
|
||||
});
|
||||
|
||||
const statsData = ref<StatsResponse>({
|
||||
categories: [],
|
||||
otherCategory: {
|
||||
categoryName: "其他服务",
|
||||
services: [],
|
||||
totalQuantity: 0,
|
||||
totalAmount: 0,
|
||||
},
|
||||
grandTotalQuantity: 0,
|
||||
grandTotalAmount: 0,
|
||||
});
|
||||
|
||||
const categories = ref<Array<{ id: number; name: string }>>([]);
|
||||
const showEmpty = computed(
|
||||
() =>
|
||||
statsData.value?.categories?.length === 0 &&
|
||||
statsData.value.otherCategory.services.length === 0
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
await fetchCategories();
|
||||
await query();
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
});
|
||||
|
||||
const fetchCategories = async () => {
|
||||
try {
|
||||
const res = await request().get("/public/service-categories");
|
||||
categories.value = res.data.map((item: any) => ({
|
||||
id: item.value,
|
||||
name: item.label,
|
||||
}));
|
||||
} catch (error) {
|
||||
ElMessage.error("分类加载失败");
|
||||
}
|
||||
};
|
||||
|
||||
const query = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
|
||||
const params = {
|
||||
startDate: searchForm.value.dateRange[0],
|
||||
endDate: searchForm.value.dateRange[1],
|
||||
serviceName: searchForm.value.serviceName,
|
||||
categoryName: searchForm.value.categoryName,
|
||||
};
|
||||
|
||||
const res = await request().get("/stats/servicesStats", { params });
|
||||
statsData.value = res.data || res;
|
||||
} catch (error) {
|
||||
ElMessage.error(`查询失败: ${(error as Error).message}`);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
searchForm.value = {
|
||||
dateRange: [
|
||||
dayjs().startOf("day").format("YYYY-MM-DD 00:00:00"),
|
||||
dayjs().format("YYYY-MM-DD 23:59:59"),
|
||||
],
|
||||
serviceName: "",
|
||||
categoryName: "",
|
||||
};
|
||||
query();
|
||||
};
|
||||
|
||||
const dateChange = (val: string[]) => {
|
||||
if (val?.length === 2) {
|
||||
searchForm.value.dateRange = val;
|
||||
}
|
||||
};
|
||||
|
||||
const handleError = (error: unknown) => {
|
||||
ElMessage({
|
||||
type: "error",
|
||||
message: "系统异常,请稍后重试",
|
||||
duration: 3000,
|
||||
});
|
||||
};
|
||||
|
||||
function exportTableToExcel() {
|
||||
const table = document.querySelector(
|
||||
"#print-container table"
|
||||
) as HTMLTableElement;
|
||||
if (!table) {
|
||||
console.error("找不到表格");
|
||||
return;
|
||||
}
|
||||
|
||||
// 将 HTML 表格转换为 worksheet
|
||||
const worksheet = XLSX.utils.table_to_sheet(table, {
|
||||
raw: true,
|
||||
});
|
||||
|
||||
// 获取范围
|
||||
const range = XLSX.utils.decode_range(worksheet["!ref"]!);
|
||||
|
||||
// 遍历每个单元格,设置样式
|
||||
for (let R = range.s.r; R <= range.e.r; ++R) {
|
||||
for (let C = range.s.c; C <= range.e.c; ++C) {
|
||||
const cellAddress = XLSX.utils.encode_cell({ r: R, c: C });
|
||||
const cell = worksheet[cellAddress];
|
||||
if (!cell || !cell.v) continue;
|
||||
|
||||
// 是否是最后一行(数量和金额总计行)
|
||||
const isLastRow = R === range.e.r;
|
||||
|
||||
cell.s = {
|
||||
alignment: {
|
||||
horizontal: "center",
|
||||
vertical: "center",
|
||||
wrapText: true,
|
||||
},
|
||||
font: {
|
||||
bold: isLastRow, // 最后一行加粗
|
||||
sz: 12,
|
||||
},
|
||||
border: {
|
||||
top: { style: "thin", color: { rgb: "000000" } },
|
||||
bottom: { style: "thin", color: { rgb: "000000" } },
|
||||
left: { style: "thin", color: { rgb: "000000" } },
|
||||
right: { style: "thin", color: { rgb: "000000" } },
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 创建工作簿并导出
|
||||
const workbook = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet, "销售统计");
|
||||
XLSX.writeFile(workbook, "销售统计报表.xlsx");
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.loading-mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.is-loading {
|
||||
animation: rotating 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotating {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
border: 1px solid #000;
|
||||
padding: 8px;
|
||||
font-size: 14px;
|
||||
text-align: center !important;
|
||||
}
|
||||
@media print {
|
||||
/* 移除全局布局影响 */
|
||||
body,
|
||||
html {
|
||||
display: block !important;
|
||||
height: auto !important;
|
||||
margin: 2mm !important; /* 添加安全边距 */
|
||||
}
|
||||
|
||||
#print-container {
|
||||
width: auto !important; /* 改为自动宽度 */
|
||||
margin: 0 auto !important; /* 居中显示 */
|
||||
font-size: 12pt !important; /* 调整字号 */
|
||||
min-height: 297mm;
|
||||
}
|
||||
|
||||
table {
|
||||
table-layout: fixed;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
/* 优化单元格内边距 */
|
||||
td,
|
||||
th {
|
||||
padding: 4px 2mm !important;
|
||||
line-height: 1.4 !important;
|
||||
}
|
||||
}
|
||||
|
||||
tr,
|
||||
td {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
#print-container {
|
||||
width: 100% !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 5mm !important;
|
||||
font-size: 14pt;
|
||||
color: #000 !important;
|
||||
font-family: Microsoft YaHei, "SimSun", serif !important;
|
||||
}
|
||||
/* 添加表格布局优化 */
|
||||
table {
|
||||
width: 100% !important;
|
||||
table-layout: fixed;
|
||||
border-collapse: collapse !important;
|
||||
|
||||
td,
|
||||
th {
|
||||
padding: 10px 2mm !important;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user