初始版本,目前线上可用

This commit is contained in:
2025-11-19 12:49:16 +08:00
commit cb7f1c45e8
178 changed files with 30336 additions and 0 deletions

View File

@@ -0,0 +1,713 @@
<template>
<div style="background-color: #fff; padding: 15px">
<baseTableHeader
title="公司日收入统计"
@resetSearch="reset"
@search="query">
<template #content>
<el-form v-model="searchForm">
<el-row :gutter="15">
<el-col>
<el-form-item label="购买日期">
<el-date-picker
v-model="searchForm.purchaseDate"
type="datetimerange"
range-separator=""
start-placeholder="选择日期"
end-placeholder="选择日期"
@change="dataChange" />
</el-form-item>
<!-- <div>
<el-button
type="primary"
style="margin-left: 15px"
@click="query"
>查询</el-button
><el-button
type="warning"
style="margin-left: 15px"
@click="reset"
>重置</el-button
>
<el-button v-print="print">打印单据</el-button>
</div> -->
</el-col>
</el-row>
</el-form>
</template>
<template #operateBtns>
<el-button v-print="print"
><img
src="/assets/icon/打印机.svg"
style="margin-right: 5px"
width="20"
height="20" />打印单据</el-button
>
<el-button @click="exportFile"
><img
src="/assets/images/Excel.svg"
alt=""
width="20"
height="20" />导出</el-button
>
</template>
</baseTableHeader>
<div id="print-container">
<h2
style="
color: #000;
text-align: center;
font-size: 20pt;
padding: 15px;
font-weight: bold;
">
殡仪服务公司日收入统计表
</h2>
<table
id="print-table"
class=""
style="
font-family: 仿宋;
table-layout: fixed;
width: 100%;
border-collapse: collapse;
">
<tbody>
<tr>
<td
colspan="4"
class="l"
style="
text-align: left;
font-weight: bold;
font-size: 25px;
font-family: 宋体;
">
<span>
统计时间{{
dayjs(searchForm.startDate).format("YYYY-MM-DD HH:mm:ss")
}}
{{ dayjs(searchForm.endDate).format("YYYY-MM-DD HH:mm:ss") }}
</span>
<!-- <span v-else>
开始时间{{ dayjs(searchForm.startDate).format("YYYY-MM-DD") }}
</span> -->
</td>
<td
colspan="2"
class="l"
style="
text-align: left;
font-weight: bold;
font-size: 25px;
font-family: 宋体;
">
单位
</td>
</tr>
<tr>
<td
style="
height: 30px;
border: 1px solid black;
text-align: center;
">
<b></b>
</td>
<td
style="
height: 30px;
border: 1px solid black;
text-align: center;
">
<b>小计</b>
</td>
<td
colspan="2"
style="height: 30px; border: 1px solid black; text-align: center">
<b>总计</b>
</td>
<td
colspan="2"
style="
height: 30px;
border: 1px solid black;
text-align: center;
">
<b>备注</b>
</td>
</tr>
<tr>
<td
style="
height: 30px;
border: 1px solid black;
text-align: center;
">
现金
</td>
<td
style="
height: 30px;
border: 1px solid black;
text-align: center;
">
{{ statsData.service.cashAmount + statsData.retail.cashAmount }}
</td>
<td
rowspan="6"
colspan="2"
style="
height: 30px;
border: 1px solid black;
text-align: center;
">
{{
statsData.service.cashAmount +
statsData.service.unionPayAmount +
statsData.service.cardAmount +
statsData.service.publicTransferAmount +
statsData.service.workshopPayment +
statsData.retail.cashAmount +
statsData.retail.unionPayAmount +
statsData.retail.cardAmount +
statsData.retail.publicTransferAmount +
statsData.retail.workshopPayment
}}
</td>
<td
colspan="2"
style="
height: 30px;
border: 1px solid black;
text-align: center;
"></td>
</tr>
<tr>
<td
style="
height: 30px;
border: 1px solid black;
text-align: center;
">
银联
</td>
<td
style="
height: 30px;
border: 1px solid black;
text-align: center;
">
{{
statsData.service.unionPayAmount +
statsData.retail.unionPayAmount
}}
</td>
<td
colspan="2"
style="
height: 30px;
border: 1px solid black;
text-align: center;
"></td>
</tr>
<tr>
<td
style="
height: 30px;
border: 1px solid black;
text-align: center;
">
银行卡
</td>
<td
style="
height: 30px;
border: 1px solid black;
text-align: center;
">
{{ statsData.service.cardAmount + statsData.retail.cardAmount }}
</td>
<td
colspan="2"
style="
height: 30px;
border: 1px solid black;
text-align: center;
"></td>
</tr>
<tr>
<td
style="
height: 30px;
border: 1px solid black;
text-align: center;
">
车间支付
</td>
<td
style="
height: 30px;
border: 1px solid black;
text-align: center;
">
{{
statsData.service.workshopPayment +
statsData.retail.workshopPayment
}}
</td>
<td
colspan="2"
style="
height: 30px;
border: 1px solid black;
text-align: center;
"></td>
</tr>
<tr>
<td
style="
height: 30px;
border: 1px solid black;
text-align: center;
">
对公转账
</td>
<td
style="
height: 30px;
border: 1px solid black;
text-align: center;
">
{{
statsData.service.publicTransferAmount +
statsData.retail.publicTransferAmount
}}
</td>
<td
colspan="2"
style="
height: 30px;
border: 1px solid black;
text-align: center;
"></td>
</tr>
<tr>
<td
style="
height: 30px;
border: 1px solid black;
text-align: center;
"></td>
<td
style="
height: 30px;
border: 1px solid black;
text-align: center;
"></td>
<td
colspan="2"
style="
height: 30px;
border: 1px solid black;
text-align: center;
"></td>
</tr>
<tr>
<td
style="
height: 30px;
border: 1px solid black;
text-align: center;
">
<b>现金合计</b>
</td>
<td
style="
height: 30px;
border: 1px solid black;
text-align: center;
">
<b>微信合计</b>
</td>
<td
colspan="2"
style="
height: 30px;
border: 1px solid black;
text-align: center;
">
<b>银行卡合计</b>
</td>
<td
style="
height: 30px;
border: 1px solid black;
text-align: center;
">
<b>车间支付合计</b>
</td>
<td
style="
height: 30px;
border: 1px solid black;
text-align: center;
">
<b>对公转账合计</b>
</td>
</tr>
<tr>
<td
style="
height: 30px;
border: 1px solid black;
text-align: center;
">
{{ statsData.total.cashAmount }}
</td>
<td
style="
height: 30px;
border: 1px solid black;
text-align: center;
">
{{ statsData.total.unionPayAmount }}
</td>
<td
colspan="2"
style="
height: 30px;
border: 1px solid black;
text-align: center;
">
{{ statsData.total.cardAmount }}
</td>
<td
style="
height: 30px;
border: 1px solid black;
text-align: center;
">
{{ statsData.total.workshopPayment }}
</td>
<td
style="
height: 30px;
border: 1px solid black;
text-align: center;
">
{{ statsData.total.publicTransferAmount }}
</td>
</tr>
<tr>
<td
style="
height: 30px;
border: 1px solid black;
text-align: center;
">
<b>总合计</b>
</td>
<td
colspan="5"
style="
height: 30px;
border: 1px solid black;
text-align: center;
">
{{
statsData.total.cashAmount +
statsData.total.unionPayAmount +
statsData.total.cardAmount +
statsData.total.workshopPayment +
statsData.total.publicTransferAmount
}}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script lang="ts" setup>
import { nextTick, ref } from "vue";
import request from "@/lib/request";
import dayjs from "dayjs";
import ExcelJS from "exceljs";
export type PaymentStats = {
cashAmount: number;
unionPayAmount: number;
cardAmount: number;
publicTransferAmount: number;
workshopPayment: number;
};
// 完整响应类型
export type StatsResponse = {
retail: PaymentStats;
service: PaymentStats;
total: PaymentStats;
};
let searchForm = ref({
startDate: dayjs().startOf("day").toDate(),
endDate: new Date(),
purchaseDate: [dayjs().startOf("day").toDate(), new Date()],
});
let statsData = ref<StatsResponse>({
retail: {
cashAmount: 0,
unionPayAmount: 0,
cardAmount: 0,
publicTransferAmount: 0,
workshopPayment: 0,
},
service: {
cashAmount: 0,
unionPayAmount: 0,
cardAmount: 0,
publicTransferAmount: 0,
workshopPayment: 0,
},
total: {
cashAmount: 0,
unionPayAmount: 0,
cardAmount: 0,
publicTransferAmount: 0,
workshopPayment: 0,
},
});
let searched = ref(false);
request()
.get("/stats/dayIncome", {
params: {
startDate: dayjs().startOf("day").toDate(),
endDate: new Date(),
},
})
.then((res) => {
statsData.value = res.data;
});
const print = {
id: "print-container",
};
function query() {
searched.value = false;
request()
.get("/stats/dayIncome", {
params: {
startDate: dayjs(searchForm.value.startDate).format(
"YYYY-MM-DD HH:mm:ss"
),
endDate: dayjs(searchForm.value.endDate).format("YYYY-MM-DD HH:mm:ss"),
},
})
.then((res) => {
statsData.value = res.data;
});
}
function reset() {
searchForm.value = {
startDate: dayjs().startOf("day").toDate(),
endDate: new Date(),
purchaseDate: [dayjs().startOf("day").toDate(), new Date()],
};
query();
}
function dataChange(val: Date[]) {
searchForm.value.startDate = val[0];
searchForm.value.endDate = val[1];
}
async function exportFile() {
try {
// 获取模板文件
const response = await fetch("/excelTemple/日收入统计.xlsx");
const buffer = await response.arrayBuffer();
const workbook = new ExcelJS.Workbook();
await workbook.xlsx.load(buffer);
const sheet = workbook.getWorksheet(1); // 默认第一个工作表
const startDate = dayjs(searchForm.value.startDate).format(
"YYYY-MM-DD HH:mm:ss"
);
const endDate = dayjs(searchForm.value.endDate).format(
"YYYY-MM-DD HH:mm:ss"
);
// 工具函数:填充值并保留样式
const setCellValueWithStyle = (cellAddress, value) => {
const cell = sheet.getCell(cellAddress);
const style = { ...cell.style };
cell.value = value;
cell.style = style;
};
// 填充各项内容(保持样式)
setCellValueWithStyle("C2", `${startDate}${endDate}`);
setCellValueWithStyle("B4", statsData.value.total.cashAmount);
setCellValueWithStyle("B5", statsData.value.total.unionPayAmount);
setCellValueWithStyle("B6", statsData.value.total.cardAmount);
setCellValueWithStyle("B7", statsData.value.total.workshopPayment);
setCellValueWithStyle("B8", statsData.value.total.publicTransferAmount);
const total =
statsData.value.service.cashAmount +
statsData.value.service.unionPayAmount +
statsData.value.service.cardAmount +
statsData.value.service.publicTransferAmount +
statsData.value.service.workshopPayment +
statsData.value.retail.cashAmount +
statsData.value.retail.unionPayAmount +
statsData.value.retail.cardAmount +
statsData.value.retail.publicTransferAmount +
statsData.value.retail.workshopPayment;
setCellValueWithStyle("C4", total);
setCellValueWithStyle("A11", statsData.value.total.cashAmount);
setCellValueWithStyle("B11", statsData.value.total.unionPayAmount);
setCellValueWithStyle("C11", statsData.value.total.cardAmount);
setCellValueWithStyle("D11", statsData.value.total.workshopPayment);
setCellValueWithStyle("E11", statsData.value.total.publicTransferAmount);
const grandTotal =
statsData.value.total.cashAmount +
statsData.value.total.unionPayAmount +
statsData.value.total.cardAmount +
statsData.value.total.workshopPayment +
statsData.value.total.publicTransferAmount;
setCellValueWithStyle("B12", grandTotal);
// 导出文件
const newBuffer = await workbook.xlsx.writeBuffer();
const blob = new Blob([newBuffer], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "日收入统计.xlsx";
a.click();
URL.revokeObjectURL(url);
} catch (error) {
console.error("导出 Excel 文件时出错:", error);
}
}
</script>
<style lang="scss" scoped>
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: 5px 2mm !important;
word-wrap: break-word;
}
}
}
@media print {
/* 重置body和html的布局方式 */
body,
html {
display: block !important;
height: auto !important;
margin: 0 !important;
padding: 0 !important;
}
@page {
margin: 15pt;
}
#print-container {
min-height: 297mm; /* A4纸高度 */
width: 100% !important;
vertical-align: top !important;
position: relative;
top: 0;
transform: none !important;
}
/* 隐藏不需要打印的元素 */
.el-form,
.el-button {
display: none !important;
}
}
</style>

View File

@@ -0,0 +1,6 @@
<template>
<div>引导员销售统计</div>
</template>
<script lang="ts" setup></script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,452 @@
<template>
<div style="background-color: #fff; padding: 15px">
<baseTableHeader title="公司销售明细" @resetSearch="reset" @search="query">
<template #content>
<el-form v-model="searchForm" label-width="100">
<el-row :gutter="25">
<el-col :span="10">
<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.deceasedName"
placeholder="输入逝者名称" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="购买人">
<el-input
v-model="searchForm.familyName"
placeholder="输入购买人" />
</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" label="引导员">
<el-form-item label="引导员">
<guideList v-model="searchForm.guide"></guideList>
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
<template #operateBtns>
<el-button @click="exportSalesDetailsExcel('当前')"
><img
src="/assets/images/Excel.svg"
alt=""
width="20"
height="20" />导出当前</el-button
>
<el-button @click="exportSalesDetailsExcel('所有')"
><img
src="/assets/images/Excel.svg"
alt=""
width="20"
height="20" />导出所有</el-button
>
</template>
</baseTableHeader>
<el-card style="margin-top: 8px">
<base-table
:option="tableOption"
ref="table"
border
show-summary
:summary-method="getSummaries">
<template #colunm>
<el-table-column
type="index"
width="80"
fixed="left"
label="序号"
align="center"></el-table-column>
<el-table-column
width="120"
fixed="left"
prop="deceasedRetail.checkoutDate"
label="结账日期"
align="center">
<template #default="{ row }">
{{ row.deceasedRetail.checkoutDate.split(" ")[0] }}
</template>
</el-table-column>
<el-table-column
prop="deceased.name"
label="逝者姓名"
width="120"
fixed="left"
align="center">
<template #default="{ row }">
<span>{{
row.deceased.name ||
row.deceased.familyName ||
row.deceasedRetail.deceasedName
}}</span>
</template>
</el-table-column>
<el-table-column
prop="deceasedRetail.guide"
label="引导员"
fixed="left"
align="center" />
<el-table-column prop="deceased.gender" label="性别" align="center" />
<el-table-column prop="deceased.age" label="年龄" align="center" />
<el-table-column
prop="deceased.idNumber"
label="身份证"
align="center"
width="180" />
<el-table-column label="地址" align="center" width="280">
<template #default="{ row }">
<div style="text-align: left">
<span
v-if="row.deceased.province || row.deceasedRetail.province"
>{{
row.deceased.province || row.deceasedRetail.province
}}/</span
>
<span v-if="row.deceased.city || row.deceasedRetail.city"
>{{ row.deceased.city || row.deceasedRetail.city }}/</span
>
<span
v-if="row.deceased.address || row.deceasedRetail.address"
>{{
row.deceased.address || row.deceasedRetail.address
}}</span
>
</div>
</template>
</el-table-column>
<el-table-column
prop="deceased.name"
label="购买人姓名"
align="center"
width="120">
<template #default="{ row }">
<span>{{
row.deceased.familyName || row.deceasedRetail.deceasedName
}}</span>
</template>
</el-table-column>
<el-table-column
prop="deceased.familyPhone"
label="购买人电话"
align="center"
width="150" />
<el-table-column
prop="name"
label="项目名称"
align="center"
width="150" />
<el-table-column
prop="price"
label="单价"
align="center"
width="150" />
<el-table-column
prop="quantity"
label="数量"
align="center"
width="150" />
<el-table-column prop="sum" label="金额" align="center" width="150">
</el-table-column>
<el-table-column
prop="remark"
label="备注"
width="280"
align="center" />
</template>
</base-table>
</el-card>
</div>
</template>
<script lang="ts" setup>
import request from "@/lib/request";
import { tableOptionType } from "@/types/table";
import { ElMessage } from "element-plus";
import { nextTick, onMounted, ref } from "vue";
import ExcelJS from "exceljs";
const categories = ref<Array<{ id: number; name: string }>>([]);
let searchForm = ref({
dateRange: [],
serviceName: "",
categoryName: "",
categories: "",
startDate: "",
endDate: "",
deceasedName: "",
guide: "",
familyName: "",
});
let tableOption = ref<tableOptionType>({
url: "/stats/salesDetails",
searchUrl: "/stats/salesDetails",
searchParams: searchForm,
executeType: "list",
});
let table = ref();
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 getSummaries = (param: { columns: any[]; data: any[] }) => {
const { columns, data } = param;
const sums: any[] = [];
// 需要统计的字段列表
const sumKeys = ["price", "quantity", "sum"];
columns.forEach((column, index) => {
if (index === 0) {
sums[index] = "合计";
return;
}
if (sumKeys.includes(column.property)) {
const values = data.map((item) => {
const keys = column.property.split(".");
let value = item;
for (const key of keys) {
value = value?.[key] || 0;
}
return Number(value) || 0;
});
if (!values.every((value) => isNaN(value))) {
const sum = values.reduce((prev, curr) => {
return prev + curr;
}, 0);
sums[index] = `${sum.toFixed(2)}`;
} else {
sums[index] = "0.00 元";
}
} else {
sums[index] = "";
}
});
return sums;
};
const dateChange = (val: string[]) => {
if (val?.length === 2) {
searchForm.value.dateRange = val;
searchForm.value.startDate = val[0];
searchForm.value.endDate = val[1];
}
};
function reset() {
searchForm.value = {
dateRange: [],
serviceName: "",
categoryName: "",
categories: "",
startDate: "",
endDate: "",
deceasedName: "",
familyName: "",
};
table.value.methods.setDataType("reset");
}
function query() {
table.value.methods.setDataType("search");
}
onMounted(() => {
fetchCategories();
});
async function exportSalesDetailsExcel(type: "当前" | "所有") {
try {
let list = [];
if (type === "当前") {
list = table.value.tableData.data;
} else {
// 1. 获取所有数据
const res = await request().post("/stats/salesDetails", {
pageSize: 999999999,
pageNumber: 1,
dateRange: [],
serviceName: "",
categoryName: "",
categories: "",
startDate: "",
endDate: "",
deceasedName: "",
guide: "",
familyName: "",
});
list = res.data?.list;
}
// 2. 获取模板文件
const templateRes = await fetch("/excelTemple/销售明细.xlsx");
const buffer = await templateRes.arrayBuffer();
// 3. 加载 Excel 模板
const workbook = new ExcelJS.Workbook();
await workbook.xlsx.load(buffer);
const sheet = workbook.getWorksheet(1); // 默认取第一个 sheet
// 4. 从第 3 行开始填充数据
list.forEach((item, index) => {
const row = sheet.getRow(index + 2); // 第3行开始
const deceased = item.deceased || {};
const retail = item.deceasedRetail || {};
row.getCell(1).value = index + 1;
row.getCell(2).value = retail.checkoutDate || "";
row.getCell(3).value = deceased.name || "";
row.getCell(4).value = retail.guide || "";
row.getCell(5).value = deceased.gender || "";
row.getCell(6).value = deceased.age || "";
row.getCell(7).value = deceased.idNumber || "";
row.getCell(8).value = deceased.address || "";
row.getCell(9).value = deceased.familyName || "";
row.getCell(10).value = deceased.familyPhone || "";
row.getCell(11).value = item.name || "";
row.getCell(12).value = item.price || "";
row.getCell(13).value = item.quantity || "";
row.getCell(14).value = item.sum || "";
row.getCell(15).value = item.remark || "";
row.eachCell((cell) => {
cell.alignment = { vertical: "middle", horizontal: "center" };
});
row.commit(); // 应用修改
});
// 5. 导出为 blob
const blob = await workbook.xlsx.writeBuffer();
const file = new Blob([blob], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
});
// 6. 触发下载
const url = URL.createObjectURL(file);
const a = document.createElement("a");
a.href = url;
a.download = "销售明细.xlsx";
a.click();
URL.revokeObjectURL(url);
} catch (error) {
console.error("导出销售明细失败:", error);
}
}
</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>

View 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>