This commit is contained in:
liu
2026-08-13 00:35:09 +08:00
parent 91ea91784d
commit 79e3ed4a8a
5 changed files with 66 additions and 18 deletions
File diff suppressed because one or more lines are too long
@@ -3,6 +3,7 @@
namespace App\Http\Controllers\Purchase; namespace App\Http\Controllers\Purchase;
use App\Exceptions\RepositoryException; use App\Exceptions\RepositoryException;
use App\Exports\PurchaseOrderExport;
use App\Http\Requests\Purchase\PurchaseCellUpdateRequest; use App\Http\Requests\Purchase\PurchaseCellUpdateRequest;
use App\Http\Requests\Purchase\PurchaseRowUpdateRequest; use App\Http\Requests\Purchase\PurchaseRowUpdateRequest;
use App\Models\ProductModel; use App\Models\ProductModel;
@@ -15,11 +16,13 @@ use App\Services\PurchaseGenerateService;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Maatwebsite\Excel\Facades\Excel;
use Modules\AnnoRoute\Attribute\GetRoute; use Modules\AnnoRoute\Attribute\GetRoute;
use Modules\AnnoRoute\Attribute\PostRoute; use Modules\AnnoRoute\Attribute\PostRoute;
use Modules\AnnoRoute\Attribute\PutRoute; use Modules\AnnoRoute\Attribute\PutRoute;
use Modules\AnnoRoute\Attribute\RequestAttribute; use Modules\AnnoRoute\Attribute\RequestAttribute;
use Modules\Common\Http\Controllers\BaseController; use Modules\Common\Http\Controllers\BaseController;
use Symfony\Component\HttpFoundation\Response;
use Throwable; use Throwable;
/** /**
@@ -190,6 +193,27 @@ class PurchaseOrderController extends BaseController
); );
} }
/**
* C2/C3 导出采购单(Excel 表格):?type=all 全品类 / category 仅蔬果分类
*/
#[GetRoute(route: '/{id}/export', authorize: 'export', where: ['id' => '[0-9]+'])]
public function export(int $id, Request $request): Response
{
$purchase = PurchaseOrderModel::find($id);
if (empty($purchase)) {
throw new RepositoryException('采购单不存在');
}
$type = (string) $request->query('type', 'all');
if (! in_array($type, ['all', 'category'], true)) {
throw new RepositoryException('导出类型参数不正确(all 全品类 / category 蔬果分类)');
}
return Excel::download(
new PurchaseOrderExport($purchase, $type),
$purchase->purchase_no . '_采购单.xlsx',
);
}
/** /**
* 单元格下钻:采购单中某门店某商品的全部订货明细 * 单元格下钻:采购单中某门店某商品的全部订货明细
*/ */
+12 -15
View File
@@ -14,8 +14,8 @@ use App\Models\UserModel;
use Maatwebsite\Excel\Facades\Excel; use Maatwebsite\Excel\Facades\Excel;
/** /**
* 采购单导出:xlsx Content-Type / 蔬果分类过滤 / PDF / format 非法拒绝 / 权限拦截 / * 采购单导出(仅 Excel 表格)xlsx Content-Type / 蔬果分类过滤 / type 非法拒绝 /
* 中文文件名 RFC 5987 编码 * 权限拦截 / 中文文件名 RFC 5987 编码
*/ */
class ExportTest extends ProcurementTestCase class ExportTest extends ProcurementTestCase
{ {
@@ -56,7 +56,7 @@ class ExportTest extends ProcurementTestCase
$purchase = $this->buildPurchaseWithItems(); $purchase = $this->buildPurchaseWithItems();
$this->actingAsSysUser(); $this->actingAsSysUser();
$response = $this->get("/purchase/order/{$purchase->id}/export?type=all&format=xlsx"); $response = $this->get("/purchase/order/{$purchase->id}/export");
$response->assertOk(); $response->assertOk();
$this->assertStringContainsString( $this->assertStringContainsString(
@@ -78,7 +78,7 @@ class ExportTest extends ProcurementTestCase
$this->actingAsSysUser(); $this->actingAsSysUser();
Excel::fake(); Excel::fake();
$this->get("/purchase/order/{$purchase->id}/export?type=category&format=xlsx")->assertOk(); $this->get("/purchase/order/{$purchase->id}/export?type=category")->assertOk();
Excel::assertDownloaded( Excel::assertDownloaded(
$purchase->purchase_no . '_采购单.xlsx', $purchase->purchase_no . '_采购单.xlsx',
@@ -90,25 +90,22 @@ class ExportTest extends ProcurementTestCase
); );
} }
/** PDF 导出:application/pdf */ /** type 参数非法 → 拒绝 */
public function test_export_pdf(): void public function test_export_invalid_type_rejected(): void
{ {
$purchase = $this->buildPurchaseWithItems(); $purchase = $this->buildPurchaseWithItems();
$this->actingAsSysUser(); $this->actingAsSysUser();
$response = $this->get("/purchase/order/{$purchase->id}/export?format=pdf"); $this->get("/purchase/order/{$purchase->id}/export?type=doc")
$response->assertOk(); ->assertJsonPath('success', false);
$this->assertStringContainsString('application/pdf', (string) $response->headers->get('Content-Type'));
$this->assertStringStartsWith('%PDF', (string) $response->getContent());
} }
/** format 参数非法 → 拒绝 */ /** 采购单不存在 → 拒绝 */
public function test_export_invalid_format_rejected(): void public function test_export_missing_purchase_rejected(): void
{ {
$purchase = $this->buildPurchaseWithItems();
$this->actingAsSysUser(); $this->actingAsSysUser();
$this->get("/purchase/order/{$purchase->id}/export?format=doc") $this->get('/purchase/order/99999/export')
->assertJsonPath('success', false); ->assertJsonPath('success', false);
} }
@@ -119,7 +116,7 @@ class ExportTest extends ProcurementTestCase
// 仅持有查询权限的用户 // 仅持有查询权限的用户
$this->actingAsSysUser(['purchase.order.query']); $this->actingAsSysUser(['purchase.order.query']);
$response = $this->get("/purchase/order/{$purchase->id}/export?format=xlsx"); $response = $this->get("/purchase/order/{$purchase->id}/export");
$this->assertFalse($response->json('success'), '缺少权限点应被拦截'); $this->assertFalse($response->json('success'), '缺少权限点应被拦截');
} }
} }
+11
View File
@@ -1,8 +1,10 @@
import createAxios from '@/utils/request'; import createAxios from '@/utils/request';
import { downloadBlob } from '@/api/common/download.ts';
import type { import type {
IPurchaseCell, IPurchaseCell,
IPurchaseDetail, IPurchaseDetail,
IPurchaseStoreSummary, IPurchaseStoreSummary,
PurchaseExportType,
} from '@/domain/iPurchaseOrder.ts'; } from '@/domain/iPurchaseOrder.ts';
/** 订单商品参数修改 */ /** 订单商品参数修改 */
@@ -74,3 +76,12 @@ export async function getPurchaseStoreSummary(purchaseId: number, storeId: numbe
params: { store_id: storeId }, params: { store_id: storeId },
}); });
} }
/** C2/C3 导出采购单(Excel 表格):type=all 全品类 / category 仅蔬果分类 */
export async function exportPurchase(id: number, type: PurchaseExportType = 'all') {
return downloadBlob(
`/purchase/order/${id}/export`,
{ type },
`采购单_${id}.xlsx`,
);
}
+17 -1
View File
@@ -3,6 +3,7 @@ import {
Button, Button,
Descriptions, Descriptions,
Drawer, Drawer,
Dropdown,
Empty, Empty,
Form, Form,
Image, Image,
@@ -19,7 +20,7 @@ import {
Tag, Tag,
Typography, Typography,
} from 'antd'; } from 'antd';
import { CheckOutlined, EditOutlined } from '@ant-design/icons'; import { CheckOutlined, DownloadOutlined, EditOutlined } from '@ant-design/icons';
import type { TableProps } from 'antd'; import type { TableProps } from 'antd';
import XinTable from '@/components/XinTable'; import XinTable from '@/components/XinTable';
import type { import type {
@@ -39,6 +40,7 @@ import type {
import { PURCHASE_STATUS_MAP } from '@/domain/iPurchaseOrder.ts'; import { PURCHASE_STATUS_MAP } from '@/domain/iPurchaseOrder.ts';
import { STORE_ORDER_STATUS_MAP } from '@/domain/iStoreOrder.ts'; import { STORE_ORDER_STATUS_MAP } from '@/domain/iStoreOrder.ts';
import { import {
exportPurchase,
getPurchaseCell, getPurchaseCell,
getPurchaseDetail, getPurchaseDetail,
getPurchaseStoreSummary, getPurchaseStoreSummary,
@@ -485,7 +487,21 @@ const PurchaseOrderPage: React.FC = () => {
const operateRender: XinTableProps<IPurchaseOrder>['operateRender'] = (record) => [ const operateRender: XinTableProps<IPurchaseOrder>['operateRender'] = (record) => [
<Button key="detail" size="small" onClick={() => openDetail(record.id!)}> <Button key="detail" size="small" onClick={() => openDetail(record.id!)}>
</Button>,
<AuthButton key="export" auth="purchase.order.export">
<Dropdown
menu={{
items: [
{ key: 'all', label: '导出全品类', onClick: () => exportPurchase(record.id!, 'all') },
{ key: 'category', label: '仅导出蔬果分类', onClick: () => exportPurchase(record.id!, 'category') },
],
}}
>
<Button size="small" type="primary" ghost icon={<DownloadOutlined />}>
</Button> </Button>
</Dropdown>
</AuthButton>,
]; ];
const tableProps: XinTableProps<IPurchaseOrder> = { const tableProps: XinTableProps<IPurchaseOrder> = {