账单导出

This commit is contained in:
liu
2026-09-13 23:42:35 +08:00
parent 5784875939
commit 80cbd452e5
12 changed files with 112 additions and 228 deletions
File diff suppressed because one or more lines are too long
+23 -38
View File
@@ -16,7 +16,7 @@ use PhpOffice\PhpSpreadsheet\Style\Fill;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
/**
* 门店账单合并导出:勾选账单跨账单按商品合并明细(可按一级分类过滤),
* 门店账单单独导出:账单内明细按商品合并(可按一级分类过滤),
* 表尾汇总商品金额/配送费/附加金额/售后金额/总金额
*/
class BillExport implements FromCollection, WithStyles
@@ -48,11 +48,11 @@ class BillExport implements FromCollection, WithStyles
private int $lastRow = 1;
/**
* @param Collection<int, BillModel> $bills 勾选账单(已按 bill_date/id 排序,with store)
* @param BillModel $bill 导出账单(with store)
* @param int $categoryId 一级分类ID(0=全部)
*/
public function __construct(
private readonly Collection $bills,
private readonly BillModel $bill,
private readonly int $categoryId = 0,
) {
}
@@ -66,35 +66,29 @@ class BillExport implements FromCollection, WithStyles
return $this->rows;
}
$items = app(BillDetailService::class)->mergedItemsOfBills(
$this->bills->pluck('id')->map(static fn ($id): int => (int) $id)->all()
);
$items = app(BillDetailService::class)->mergedItems($this->bill);
$categoryName = '全部';
if ($this->categoryId > 0) {
[$items, $categoryName] = $this->filterByRootCategory($items);
}
$bill = $this->bill;
$rows = [];
$rowIndex = 0;
// 标题行
$rows[] = ['门店账单合并导出(共 ' . $this->bills->count() . ' 张)'];
$rows[] = ['门店账单 ' . $bill->bill_no];
$this->specialRows[++$rowIndex] = 'title';
// 范围行:账单号 / 门店 / 分类 / 导出时间
$billNos = $this->bills->pluck('bill_no')->all();
$billNoText = count($billNos) > 8
? implode('、', array_slice($billNos, 0, 8)) . ' 等 ' . count($billNos) . ' 张'
: implode('、', $billNos);
$storeNames = $this->bills
->map(static fn (BillModel $bill): string => $bill->store->name ?? ('门店#' . $bill->store_id))
->unique()
->values()
->all();
$rows[] = ['账单号:' . $billNoText];
$this->specialRows[++$rowIndex] = 'scope';
$rows[] = ['门店:' . implode('、', $storeNames) . ' 分类:' . $categoryName . ' 导出时间:' . now('Asia/Shanghai')->format('Y-m-d H:i:s')];
// 范围行:门店 / 账单日期 / 分类 / 导出时间
$storeName = $bill->store->name ?? ('门店#' . $bill->store_id);
$rows[] = [
'门店:' . $storeName
. ' 账单日期:' . $bill->bill_date?->toDateString()
. ' 分类:' . $categoryName
. ' 导出时间:' . now('Asia/Shanghai')->format('Y-m-d H:i:s'),
];
$this->specialRows[++$rowIndex] = 'scope';
// 空行
@@ -106,7 +100,7 @@ class BillExport implements FromCollection, WithStyles
$this->specialRows[++$rowIndex] = 'header';
$this->headerRow = $rowIndex;
// 明细行(跨账单同商品已合并,单价为加权平均)
// 明细行(账单内同商品已合并,单价为加权平均)
$totalQuantity = 0;
$totalWeight = '0';
$totalAmount = '0';
@@ -135,32 +129,23 @@ class BillExport implements FromCollection, WithStyles
$this->fillCells[] = ['row' => $rowIndex, 'column' => 'F', 'value' => (float) $totalQuantity];
$this->fillCells[] = ['row' => $rowIndex, 'column' => 'H', 'value' => (float) $totalAmount];
// 配送费/附加金额/售后金额/总金额(账单级费用全额汇总,不受分类过滤影响)
$deliveryTotal = '0';
$addedTotal = '0';
$afterSaleTotal = '0';
$boxNum = 0;
$trayNum = 0;
foreach ($this->bills as $bill) {
$deliveryTotal = bcadd($deliveryTotal, (string) $bill->delivery_fee, 2);
$addedTotal = bcadd($addedTotal, (string) $bill->added_amount, 2);
$afterSaleTotal = bcadd($afterSaleTotal, (string) $bill->after_sale, 2);
$boxNum += (int) $bill->box_num;
$trayNum += (int) $bill->tray_num;
}
// 配送费/附加金额/售后金额/总金额(账单级费用全额计入,不受分类过滤影响)
$deliveryTotal = (string) $bill->delivery_fee;
$addedTotal = (string) $bill->added_amount;
$afterSaleTotal = (string) $bill->after_sale;
$grandTotal = bcadd(bcadd(bcadd($totalAmount, $deliveryTotal, 2), $addedTotal, 2), $afterSaleTotal, 2);
$rows[] = ['配送费合计', '', '', '', '', '', '', (float) $deliveryTotal];
$rows[] = ['配送费', '', '', '', '', '', '', (float) $deliveryTotal];
$this->specialRows[++$rowIndex] = 'summary';
$this->mergeRows[] = $rowIndex;
$this->fillCells[] = ['row' => $rowIndex, 'column' => 'H', 'value' => (float) $deliveryTotal];
$rows[] = ['附加金额合计(周转筐 ' . $boxNum . ' 个 / 周转托盘 ' . $trayNum . ' 个)', '', '', '', '', '', '', (float) $addedTotal];
$rows[] = ['附加金额(周转筐 ' . (int) $bill->box_num . ' 个 / 周转托盘 ' . (int) $bill->tray_num . ' 个)', '', '', '', '', '', '', (float) $addedTotal];
$this->specialRows[++$rowIndex] = 'summary';
$this->mergeRows[] = $rowIndex;
$this->fillCells[] = ['row' => $rowIndex, 'column' => 'H', 'value' => (float) $addedTotal];
$rows[] = ['售后金额合计(可正负)', '', '', '', '', '', '', (float) $afterSaleTotal];
$rows[] = ['售后金额(可正负)', '', '', '', '', '', '', (float) $afterSaleTotal];
$this->specialRows[++$rowIndex] = 'summary';
$this->mergeRows[] = $rowIndex;
$this->fillCells[] = ['row' => $rowIndex, 'column' => 'H', 'value' => (float) $afterSaleTotal];
@@ -301,7 +286,7 @@ class BillExport implements FromCollection, WithStyles
));
if ($filtered === []) {
throw new RepositoryException('所选账单在「' . $categoryName . '」分类下无商品明细');
throw new RepositoryException('该账单在「' . $categoryName . '」分类下无商品明细');
}
return [$filtered, $categoryName];
+9 -22
View File
@@ -87,41 +87,28 @@ class BillController extends BaseMiniController
}
/**
* 合并导出账单(静态路由放在 /bill/{id} 之前声明)
* 仅可导出本店账单(强制 store_id 过滤,张数不匹配说明混入无效账单整批拒绝);
* ?ids=1,2,3 必选(1~100 张);?category_id= 一级分类过滤(0/缺省=全部)
* 单独导出账单:仅可导出本店账单(强制 store_id 过滤);
* ?category_id= 一级分类过滤(0/缺省=全部)
*/
#[GetRoute('/bill/export', authorize: true)]
public function export(Request $request): Response
#[GetRoute('/bill/{id}/export', authorize: true, where: ['id' => '[0-9]+'])]
public function export(int $id, Request $request): Response
{
$data = $request->validate([
'ids' => 'required|string',
'category_id' => 'nullable|integer|min:0',
]);
$store = $this->currentStore($request);
$ids = array_values(array_unique(array_filter(array_map('intval', explode(',', (string) $data['ids'])))));
if ($ids === [] || count($ids) > 100) {
throw new RepositoryException('请选择 1~100 张账单');
}
$bills = BillModel::with('store:id,name')
$bill = BillModel::with('store:id,name')
->where('store_id', $store->id)
->whereIn('id', $ids)
->orderBy('bill_date')
->orderBy('id')
->get();
if ($bills->isEmpty()) {
->find($id);
if ($bill === null) {
throw new RepositoryException('账单不存在');
}
if ($bills->count() !== count($ids)) {
throw new RepositoryException('存在无效账单,请刷新后重试');
}
return Excel::download(
new BillExport($bills, (int) ($data['category_id'] ?? 0)),
'门店账单_' . now()->format('Ymd_His') . '.xlsx'
new BillExport($bill, (int) ($data['category_id'] ?? 0)),
'门店账单_' . $bill->bill_no . '.xlsx'
);
}
+8 -18
View File
@@ -61,34 +61,24 @@ class BillController extends BaseController
}
/**
* 合并导出:勾选账单跨账单按商品合并明细(可按一级分类过滤),
* 表尾汇总商品金额/配送费/附加金额/总金额
* 单独导出账单:账单内明细按商品合并(可按一级分类过滤),
* 表尾汇总商品金额/配送费/附加金额/售后金额/总金额
*/
#[GetRoute(route: '/export', authorize: 'export')]
public function export(Request $request): Response
#[GetRoute(route: '/{id}/export', authorize: 'export', where: ['id' => '[0-9]+'])]
public function export(int $id, Request $request): Response
{
$data = $request->validate([
'ids' => 'required|string',
'category_id' => 'nullable|integer|min:0',
]);
$ids = array_values(array_filter(array_map('intval', explode(',', (string) $data['ids']))));
if ($ids === [] || count($ids) > 100) {
throw new RepositoryException('请选择 1~100 张账单');
}
$bills = BillModel::with('store:id,name')
->whereIn('id', $ids)
->orderBy('bill_date')
->orderBy('id')
->get();
if ($bills->isEmpty()) {
$bill = BillModel::with('store:id,name')->find($id);
if (empty($bill)) {
throw new RepositoryException('账单不存在');
}
return Excel::download(
new BillExport($bills, (int) ($data['category_id'] ?? 0)),
'门店账单_' . now()->format('Ymd_His') . '.xlsx'
new BillExport($bill, (int) ($data['category_id'] ?? 0)),
'门店账单_' . $bill->bill_no . '.xlsx'
);
}
-18
View File
@@ -29,24 +29,6 @@ class BillDetailService
return $this->aggregateItems($items);
}
/**
* 合并后的商品明细(跨账单口径):按商品聚合多张账单的全部订单明细
* 用于账单合并导出,单价同为加权平均口径
*
* @param array<int, int> $billIds 账单ID列表
* @return array<int, array{product_id: int, product_name: string, product_spec: string, unit: string, price: string, quantity: int, weight: string, amount: string, image: string}>
*/
public function mergedItemsOfBills(array $billIds): array
{
$items = StoreOrderItemModel::query()
->whereIn('bill_id', $billIds)
->where('quantity', '>', 0)
->orderBy('id')
->get();
return $this->aggregateItems($items);
}
/**
* 按商品聚合订单明细:数量累加、重量/金额 bc 累加、单价加权平均,
* 首图取明细快照 image_ids 批量解析,按「分类sort → 商品sort」排序
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -5,7 +5,7 @@
<link rel="icon" type="image/svg+xml" href="/favicons.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>XinAdmin</title>
<script type="module" crossorigin src="/assets/index-_7W4DmEA.js"></script>
<script type="module" crossorigin src="/assets/index-B519TAEb.js"></script>
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-BgaNhQyE.js">
<link rel="modulepreload" crossorigin href="/assets/jsx-runtime-CRBytmvs.js">
<link rel="modulepreload" crossorigin href="/assets/chunk-KS7C4IRE-Zm15rq6F.js">
+44 -58
View File
@@ -12,8 +12,8 @@ use App\Models\StoreOrderItemModel;
use Maatwebsite\Excel\Facades\Excel;
/**
* 门店账单合并导出:跨账单按商品合并明细(加权平均单价)/ 一级分类过滤 /
* 配送费与附加金额全额汇总 / ids 校验 / 权限拦截 / 分类下无明细拒绝
* 门店账单单独导出:账单内按商品合并明细(加权平均单价)/ 一级分类过滤 /
* 费用行取账单自身字段 / 权限拦截 / 账单不存在 / 分类下无明细拒绝 / 小程序端本店口径
*/
class BillExportTest extends ProcurementTestCase
{
@@ -60,43 +60,44 @@ class BillExportTest extends ProcurementTestCase
]);
}
/** 跨账单合并:同商品数量累加、单价加权平均,配送费/附加/总金额全额汇总 */
public function test_export_merges_items_across_bills(): void
/** 单独导出:账单内同商品合并(数量累加、单价加权平均),费用行取账单自身字段 */
public function test_export_merges_items_within_bill(): void
{
$this->freezeTime();
$root = ProductCategoryModel::create(['name' => '蔬菜', 'parent_id' => 0, 'sort' => 0, 'status' => 1]);
$store = StoreModel::factory()->create();
$productA = ProductModel::factory()->create(['category_id' => $root->id]);
$productB = ProductModel::factory()->create(['category_id' => $root->id]);
$bill1 = $this->makeBill($store);
$bill2 = $this->makeBill($store, ['after_sale' => '-8.00']);
// 同一商品跨账单:10×2.00 + 6×3.00 → 数量 16、金额 38.00、加权单价 2.37
$this->makeItem($bill1, $productA, 10, '2.00');
$this->makeItem($bill2, $productA, 6, '3.00');
$this->makeItem($bill2, $productB, 4, '5.00');
$bill = $this->makeBill($store, ['after_sale' => '-8.00']);
// 同一商品两条明细:10×2.00 + 6×3.00 → 数量 16、金额 38.00、加权单价 2.37
$this->makeItem($bill, $productA, 10, '2.00');
$this->makeItem($bill, $productA, 6, '3.00');
$this->makeItem($bill, $productB, 4, '5.00');
$this->actingAsSysUser();
Excel::fake();
$this->get("/recon/bill/export?ids={$bill1->id},{$bill2->id}")->assertOk();
$this->get("/recon/bill/{$bill->id}/export")->assertOk();
Excel::assertDownloaded(
'门店账单_' . now()->format('Ymd_His') . '.xlsx',
static function (BillExport $export) use ($productA, $productB): bool {
'门店账单_' . $bill->bill_no . '.xlsx',
static function (BillExport $export) use ($bill, $productA, $productB): bool {
$rows = $export->collection()->values();
// 标题1 + 范围2 + 空行1 + 列头1 + 明细2 + 合计5 = 12 行
if ($rows->count() !== 12) {
// 标题1 + 范围1 + 空行1 + 列头1 + 明细2 + 合计5 = 11 行
if ($rows->count() !== 11) {
return false;
}
// 标题行含账单号
if (! str_contains((string) $rows[0][0], $bill->bill_no)) {
return false;
}
$details = $rows->slice(5, 2)->values();
$details = $rows->slice(4, 2)->values();
$rowA = $details->firstWhere(1, $productA->name);
$rowB = $details->firstWhere(1, $productB->name);
if ($rowA === null || $rowB === null) {
return false;
}
// 跨账单同商品合并:数量 16、加权单价 2.37、金额 38.00
// 账单内同商品合并:数量 16、加权单价 2.37、金额 38.00
if ((float) $rowA[4] !== 2.37 || (int) $rowA[5] !== 16 || (float) $rowA[7] !== 38.0) {
return false;
}
@@ -104,31 +105,29 @@ class BillExportTest extends ProcurementTestCase
return false;
}
$summary = $rows->slice(7)->values();
// 合计:数量 20、商品金额 58.00;配送费 20.00;附加 60.00(筐 4 / 托盘 2);售后 -8.00;总金额 130.00
$summary = $rows->slice(6)->values();
// 合计:数量 20、商品金额 58.00;配送费 10.00;附加 30.00(筐 2 / 托盘 1);售后 -8.00;总金额 90.00
if ((int) $summary[0][5] !== 20 || (float) $summary[0][7] !== 58.0) {
return false;
}
if ($summary[1][0] !== '配送费合计' || (float) $summary[1][7] !== 20.0) {
if ($summary[1][0] !== '配送费' || (float) $summary[1][7] !== 10.0) {
return false;
}
if (! str_contains((string) $summary[2][0], '筐 4 个 / 周转托盘 2 个') || (float) $summary[2][7] !== 60.0) {
if (! str_contains((string) $summary[2][0], '筐 2 个 / 周转托盘 1 个') || (float) $summary[2][7] !== 30.0) {
return false;
}
if (! str_contains((string) $summary[3][0], '售后金额合计') || (float) $summary[3][7] !== -8.0) {
if (! str_contains((string) $summary[3][0], '售后金额') || (float) $summary[3][7] !== -8.0) {
return false;
}
return (float) $summary[4][7] === 130.0;
return (float) $summary[4][7] === 90.0;
}
);
}
/** 一级分类过滤:子分类商品归入根分类,其他分类商品忽略;费用行仍全额汇总 */
/** 一级分类过滤:子分类商品归入根分类,其他分类商品忽略;费用行仍全额计入 */
public function test_export_filters_by_root_category(): void
{
$this->freezeTime();
$vegRoot = ProductCategoryModel::create(['name' => '蔬菜', 'parent_id' => 0, 'sort' => 0, 'status' => 1]);
$vegChild = ProductCategoryModel::create(['name' => '叶菜类', 'parent_id' => $vegRoot->id, 'sort' => 0, 'status' => 1]);
$meatRoot = ProductCategoryModel::create(['name' => '肉禽蛋', 'parent_id' => 0, 'sort' => 1, 'status' => 1]);
@@ -143,23 +142,23 @@ class BillExportTest extends ProcurementTestCase
$this->actingAsSysUser();
Excel::fake();
$this->get("/recon/bill/export?ids={$bill->id}&category_id={$vegRoot->id}")->assertOk();
$this->get("/recon/bill/{$bill->id}/export?category_id={$vegRoot->id}")->assertOk();
Excel::assertDownloaded(
'门店账单_' . now()->format('Ymd_His') . '.xlsx',
'门店账单_' . $bill->bill_no . '.xlsx',
static function (BillExport $export) use ($veg): bool {
$rows = $export->collection()->values();
// 范围行标注所选分类
if (! str_contains((string) $rows[2][0], '分类:蔬菜')) {
if (! str_contains((string) $rows[1][0], '分类:蔬菜')) {
return false;
}
// 明细仅剩蔬菜(子分类归入根分类),肉禽被忽略
$detail = $rows->slice(5, 1)->values()->first();
$detail = $rows->slice(4, 1)->values()->first();
if ($detail === null || $detail[1] !== $veg->name) {
return false;
}
$summary = $rows->slice(6)->values();
$summary = $rows->slice(5)->values();
// 商品金额合计 20.00(仅蔬菜);配送费 10.00 / 附加 30.00 / 售后 0.00 全额;总金额 60.00
if ((float) $summary[0][7] !== 20.0) {
return false;
@@ -186,7 +185,7 @@ class BillExportTest extends ProcurementTestCase
$this->makeItem($bill, $meat, 5, '20.00');
$this->actingAsSysUser();
$this->get("/recon/bill/export?ids={$bill->id}&category_id={$vegRoot->id}")
$this->get("/recon/bill/{$bill->id}/export?category_id={$vegRoot->id}")
->assertJsonPath('success', false);
}
@@ -202,30 +201,22 @@ class BillExportTest extends ProcurementTestCase
// 仅持有查询权限的用户
$this->actingAsSysUser(['recon.bill.query']);
$response = $this->get("/recon/bill/export?ids={$bill->id}");
$response = $this->get("/recon/bill/{$bill->id}/export");
$this->assertFalse($response->json('success'), '缺少权限点应被拦截');
}
/** ids 缺失 / 超 100 张 / 账单不存在 → 拒绝 */
public function test_export_invalid_ids_rejected(): void
/** 账单不存在 → 拒绝 */
public function test_export_bill_not_found_rejected(): void
{
$this->actingAsSysUser();
$this->get('/recon/bill/export')
->assertJsonPath('success', false);
$this->get('/recon/bill/export?ids=' . implode(',', range(1, 101)))
->assertJsonPath('success', false);
$this->get('/recon/bill/export?ids=99999')
$this->get('/recon/bill/99999/export')
->assertJsonPath('success', false);
}
/** 小程序端导出:强制本店过滤,混入他店账单整批拒绝 */
/** 小程序端导出:强制本店过滤,他店账单拒绝 */
public function test_mini_export_scoped_to_own_store(): void
{
$this->freezeTime();
$root = ProductCategoryModel::create(['name' => '蔬菜', 'parent_id' => 0, 'sort' => 0, 'status' => 1]);
$storeA = StoreModel::factory()->create();
$storeB = StoreModel::factory()->create();
@@ -234,28 +225,23 @@ class BillExportTest extends ProcurementTestCase
$billA = $this->makeBill($storeA);
$billB = $this->makeBill($storeB);
$this->makeItem($billA, $product, 10, '2.00');
$this->makeItem($billB, $product, 5, '20.00');
$this->actingAsMiniStore($storeA);
// 本店账单正常导出
Excel::fake();
$this->get("/mini/bill/export?ids={$billA->id}")->assertOk();
$this->get("/mini/bill/{$billA->id}/export")->assertOk();
Excel::assertDownloaded(
'门店账单_' . now()->format('Ymd_His') . '.xlsx',
'门店账单_' . $billA->bill_no . '.xlsx',
static function (BillExport $export): bool {
$rows = $export->collection()->values();
// 明细仅本店账单的 1 行(他店账单未混入)
return $rows->count() === 11;
// 标题1 + 范围1 + 空行1 + 列头1 + 明细1 + 合计5 = 10 行
return $rows->count() === 10;
}
);
// 混入他店账单 → 张数不匹配整批拒绝
$this->get("/mini/bill/export?ids={$billA->id},{$billB->id}")
->assertJsonPath('success', false);
// 仅他店账单 → 本店口径查无账单
$this->get("/mini/bill/export?ids={$billB->id}")
// 他店账单 → 本店口径查无账单
$this->get("/mini/bill/{$billB->id}/export")
->assertJsonPath('success', false);
}
}
+3 -7
View File
@@ -19,11 +19,7 @@ export async function payBill(id: number, data: { paid_at: string; pay_remark?:
});
}
/** 合并导出账单:勾选账单跨账单按商品合并明细,可按一级分类过滤 */
export async function exportBills(ids: number[], categoryId: number) {
return downloadBlob(
'/recon/bill/export',
{ ids: ids.join(','), category_id: categoryId },
'门店账单.xlsx'
);
/** 单独导出账单:账单内明细按商品合并为一个 Excel */
export async function exportBill(id: number) {
return downloadBlob(`/recon/bill/${id}/export`, {}, '门店账单.xlsx');
}
+20 -62
View File
@@ -8,7 +8,6 @@ import {
Input,
message,
Modal,
Select,
Table,
Tag,
Typography,
@@ -25,9 +24,8 @@ import type {
import type { IBill, IBillDetail, IBillGoodsItem, IBillOrder } from '@/domain/iBill.ts';
import { BILL_STATUS_MAP } from '@/domain/iBill.ts';
import { STORE_ORDER_STATUS_MAP } from '@/domain/iStoreOrder.ts';
import { exportBills, getBillDetail, payBill } from '@/api/recon/bill.ts';
import { exportBill, getBillDetail, payBill } from '@/api/recon/bill.ts';
import { getStoreOptions } from '@/api/customer/store.ts';
import { getCategoryTree } from '@/api/product/category.ts';
import type IStore from '@/domain/iStore.ts';
import AuthButton from '@/components/AuthButton';
@@ -55,20 +53,11 @@ const BillPage: React.FC = () => {
const [paySaving, setPaySaving] = useState(false);
const [payForm] = Form.useForm<PayFormValues>();
// 合并导出(勾选账单跨账单按商品合并明细,可按一级分类过滤)
const [selectedRowKeys, setSelectedRowKeys] = useState<number[]>([]);
const [exportOpen, setExportOpen] = useState(false);
const [exporting, setExporting] = useState(false);
const [categoryId, setCategoryId] = useState(0);
const [topCategories, setTopCategories] = useState<{ value: number; label: string }[]>([]);
// 单独导出(每行一张账单,账单内明细按商品合并)
const [exportingId, setExportingId] = useState<number | null>(null);
useEffect(() => {
getStoreOptions().then((res) => setStores(res.data.data ?? []));
getCategoryTree().then((res) =>
setTopCategories(
(res.data.data ?? []).map((c) => ({ value: c.id!, label: c.name ?? '' })),
),
);
}, []);
const openDetail = async (id: number) => {
@@ -107,17 +96,16 @@ const BillPage: React.FC = () => {
}
};
/** 提交合并导出:下载失败时 downloadBlob 内部已提示,保留勾选便于重试 */
const handleExport = async () => {
if (selectedRowKeys.length === 0) {
/** 单独导出:下载失败时 downloadBlob 内部已提示 */
const handleExport = async (record: IBill) => {
if (!record.id) {
return;
}
setExporting(true);
setExportingId(record.id);
try {
await exportBills(selectedRowKeys, categoryId);
setExportOpen(false);
await exportBill(record.id);
} finally {
setExporting(false);
setExportingId(null);
}
};
@@ -328,6 +316,16 @@ const BillPage: React.FC = () => {
icon={<UnorderedListOutlined />}
onClick={() => openDetail(record.id!)}
/>,
<AuthButton key="export" auth="recon.bill.export">
<Button
size="small"
color="orange"
variant="solid"
icon={<DownloadOutlined />}
loading={exportingId === record.id}
onClick={() => handleExport(record)}
/>
</AuthButton>,
record.status === 0 ? (
<AuthButton key="pay" auth="recon.bill.pay">
<Button size="small" variant="solid" color="green" onClick={() => openPay(record)}>
@@ -346,21 +344,7 @@ const BillPage: React.FC = () => {
operateRender,
formProps: false,
actionBarRender: (dom) => [dom.search, dom.keywordSearch],
rowSelection: {
selectedRowKeys,
onChange: (keys) => setSelectedRowKeys(keys.map(Number)),
preserveSelectedRowKeys: true,
},
toolBarRender: (dom) => [
<AuthButton key="export" auth="recon.bill.export">
<Button
icon={<DownloadOutlined />}
disabled={selectedRowKeys.length === 0}
onClick={() => setExportOpen(true)}
>
导出{selectedRowKeys.length > 0 ? `(${selectedRowKeys.length})` : ''}
</Button>
</AuthButton>,
dom.columnSetting,
dom.hideBorder,
dom.reload,
@@ -374,7 +358,7 @@ const BillPage: React.FC = () => {
<Title level={3}>门店账单</Title>
<Text type="secondary">
采购单完成后在「采购单」页生成,每个门店单独一张;账单总金额 = 商品金额 + 配送费 + 附加金额(周转筐/托盘)+ 售后金额(可正负);
勾选多张账单可合并导出为一个 Excel(明细跨账单按商品合并,可按一级分类过滤)。
每行可单独导出为一个 Excel(账单内明细按商品合并)。
</Text>
</div>
<XinTable<IBill> {...tableProps} />
@@ -484,32 +468,6 @@ const BillPage: React.FC = () => {
</Form.Item>
</Form>
</Modal>
{/* 合并导出:勾选账单跨账单按商品合并明细,可按一级分类过滤 */}
<Modal
title="导出账单"
open={exportOpen}
onCancel={() => setExportOpen(false)}
onOk={handleExport}
confirmLoading={exporting}
okText="导出"
destroyOnHidden
>
<div className="py-2 text-gray-500">
已选 <Text strong>{selectedRowKeys.length}</Text> 张账单,导出为一个
Excel:商品明细跨账单按商品合并,表尾汇总配送费、附加金额与总金额;
选择分类后仅导出该一级分类下的商品明细(配送费/附加金额仍全额汇总)。
</div>
<Form layout="vertical">
<Form.Item label="商品分类">
<Select
value={categoryId}
onChange={(value) => setCategoryId(value)}
options={[{ value: 0, label: '全部分类' }, ...topCategories]}
/>
</Form.Item>
</Form>
</Modal>
</>
);
};