批量删除
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -166,6 +166,23 @@ class ProductController extends BaseController
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
/** 批量删除商品(软删除),复用 delete 权限点 */
|
||||
#[DeleteRoute('/batch', 'delete')]
|
||||
public function batchDelete(Request $request): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'ids' => 'required|array|min:1',
|
||||
'ids.*' => 'integer|distinct',
|
||||
], [
|
||||
'ids.required' => '请选择要删除的商品',
|
||||
'ids.min' => '请选择要删除的商品',
|
||||
'ids.*.integer' => '商品 ID 格式错误',
|
||||
'ids.*.distinct' => '存在重复的商品',
|
||||
]);
|
||||
ProductModel::whereIn('id', $data['ids'])->delete();
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
/**
|
||||
* A2 价格矩阵:行=商品(支持 category_id / keyword 过滤 + page/pageSize 服务端分页),
|
||||
* 列=全部启用等级,值=按等级上浮比例换算的售价(成本价未设置为 null);仅成本价可编辑
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\ProductModel;
|
||||
|
||||
/**
|
||||
* 商品批量删除(软删除):正常批量删除、空 ids 校验失败、无 delete 权限点拦截
|
||||
*/
|
||||
class ProductBatchDeleteTest extends ProcurementTestCase
|
||||
{
|
||||
/** 批量删除成功:选中商品软删除,未选中的保留 */
|
||||
public function test_batch_delete_soft_deletes_selected_products(): void
|
||||
{
|
||||
$this->actingAsSysUser();
|
||||
$ids = ProductModel::factory()->count(3)->create()->pluck('id')->all();
|
||||
$keep = ProductModel::factory()->create();
|
||||
|
||||
$this->deleteJson('/product/goods/batch', ['ids' => $ids])
|
||||
->assertOk()
|
||||
->assertJsonPath('success', true);
|
||||
|
||||
foreach ($ids as $id) {
|
||||
$this->assertSoftDeleted('product', ['id' => $id]);
|
||||
}
|
||||
$this->assertDatabaseHas('product', ['id' => $keep->id, 'deleted_at' => null]);
|
||||
}
|
||||
|
||||
/** ids 为空数组 → 校验失败,不删除任何商品 */
|
||||
public function test_batch_delete_requires_non_empty_ids(): void
|
||||
{
|
||||
$this->actingAsSysUser();
|
||||
$product = ProductModel::factory()->create();
|
||||
|
||||
$this->deleteJson('/product/goods/batch', ['ids' => []])
|
||||
->assertOk()
|
||||
->assertJsonPath('success', false)
|
||||
->assertJsonPath('msg', '请选择要删除的商品');
|
||||
|
||||
$this->assertDatabaseHas('product', ['id' => $product->id, 'deleted_at' => null]);
|
||||
}
|
||||
|
||||
/** 无 product.goods.delete 权限点 → 拦截 */
|
||||
public function test_batch_delete_forbidden_without_permission(): void
|
||||
{
|
||||
// 先建占位用户:每个测试方法内首个系统用户自增 id=1,超管旁路会绕过 abilities 校验
|
||||
$this->actingAsSysUser();
|
||||
$this->actingAsSysUser(['product.goods.query']);
|
||||
|
||||
$product = ProductModel::factory()->create();
|
||||
|
||||
$this->deleteJson('/product/goods/batch', ['ids' => [$product->id]])
|
||||
->assertOk()
|
||||
->assertJsonPath('success', false)
|
||||
->assertJsonPath('msg', 'No Permission');
|
||||
|
||||
$this->assertDatabaseHas('product', ['id' => $product->id, 'deleted_at' => null]);
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,15 @@ export async function importProducts(file: File) {
|
||||
});
|
||||
}
|
||||
|
||||
/** 批量删除商品(软删除) */
|
||||
export async function batchDeleteProducts(ids: number[]) {
|
||||
return createAxios({
|
||||
url: '/product/goods/batch',
|
||||
method: 'delete',
|
||||
data: { ids },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A2 价格矩阵:行=商品,列=启用等级,值=按等级上浮比例换算的售价(成本价未设置 null);
|
||||
* 仅成本价可编辑
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
Typography,
|
||||
Upload,
|
||||
} from 'antd';
|
||||
import { DownloadOutlined, InboxOutlined, TableOutlined, UploadOutlined } from '@ant-design/icons';
|
||||
import { DeleteOutlined, DownloadOutlined, InboxOutlined, TableOutlined, UploadOutlined } from '@ant-design/icons';
|
||||
import type { TableProps } from 'antd';
|
||||
import XinTable from '@/components/XinTable';
|
||||
import AuthButton from '@/components/AuthButton';
|
||||
@@ -28,7 +28,7 @@ import type {IProductCategoryTree} from '@/domain/iProductCategory.ts';
|
||||
import { getSupplierOptions } from '@/api/customer/supplier.ts';
|
||||
import type ISupplier from '@/domain/iSupplier.ts';
|
||||
import { getCategoryTree } from '@/api/product/category.ts';
|
||||
import { batchPrice, getPriceMatrix } from '@/api/product/goods.ts';
|
||||
import { batchDeleteProducts, batchPrice, getPriceMatrix } from '@/api/product/goods.ts';
|
||||
import type { IImportError } from '@/api/product/goods.ts';
|
||||
import { downloadProductTemplate, exportProducts, importProducts } from '@/api/product/goods.ts';
|
||||
|
||||
@@ -70,7 +70,35 @@ const ProductGoodsPage: React.FC = () => {
|
||||
// ===== 分类侧栏 =====
|
||||
const [activeCategory, setActiveCategory] = useState<number | undefined>(undefined);
|
||||
const tableRef = useRef<XinTableInstance<IProduct> | null>(null);
|
||||
useEffect(() => { tableRef.current?.reset() }, [activeCategory]);
|
||||
useEffect(() => {
|
||||
setSelectedRowKeys([]);
|
||||
tableRef.current?.reset();
|
||||
}, [activeCategory]);
|
||||
|
||||
// ===== 批量删除 =====
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<number[]>([]);
|
||||
const [batchDeleting, setBatchDeleting] = useState(false);
|
||||
|
||||
/** 批量删除(软删除,成功后清空勾选并刷新列表) */
|
||||
const handleBatchDelete = () => {
|
||||
window.$modal?.confirm({
|
||||
title: `确定要删除选中的 ${selectedRowKeys.length} 件商品吗?`,
|
||||
okText: '删除',
|
||||
okButtonProps: { danger: true },
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
setBatchDeleting(true);
|
||||
try {
|
||||
await batchDeleteProducts(selectedRowKeys);
|
||||
message.success('批量删除成功');
|
||||
setSelectedRowKeys([]);
|
||||
await tableRef.current?.reload();
|
||||
} finally {
|
||||
setBatchDeleting(false);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// ===== 价格矩阵抽屉 =====
|
||||
const [matrixOpen, setMatrixOpen] = useState(false);
|
||||
@@ -489,6 +517,17 @@ const ProductGoodsPage: React.FC = () => {
|
||||
actionBarRender: (dom) => [
|
||||
dom.add,
|
||||
dom.search,
|
||||
<AuthButton key="batchDelete" auth="product.goods.delete">
|
||||
<Button
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
disabled={selectedRowKeys.length === 0}
|
||||
loading={batchDeleting}
|
||||
onClick={handleBatchDelete}
|
||||
>
|
||||
批量删除{selectedRowKeys.length > 0 ? ` (${selectedRowKeys.length})` : ''}
|
||||
</Button>
|
||||
</AuthButton>,
|
||||
<Button key="matrix" icon={<TableOutlined />} onClick={openMatrix}>
|
||||
价格矩阵
|
||||
</Button>,
|
||||
@@ -514,6 +553,10 @@ const ProductGoodsPage: React.FC = () => {
|
||||
...params,
|
||||
category_id: activeCategory
|
||||
}),
|
||||
rowSelection: {
|
||||
selectedRowKeys,
|
||||
onChange: (keys) => setSelectedRowKeys(keys.map(Number)),
|
||||
},
|
||||
modalProps: {
|
||||
width: 800,
|
||||
centered: true,
|
||||
|
||||
Reference in New Issue
Block a user