批量删除
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();
|
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 服务端分页),
|
* A2 价格矩阵:行=商品(支持 category_id / keyword 过滤 + page/pageSize 服务端分页),
|
||||||
* 列=全部启用等级,值=按等级上浮比例换算的售价(成本价未设置为 null);仅成本价可编辑
|
* 列=全部启用等级,值=按等级上浮比例换算的售价(成本价未设置为 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);
|
* A2 价格矩阵:行=商品,列=启用等级,值=按等级上浮比例换算的售价(成本价未设置 null);
|
||||||
* 仅成本价可编辑
|
* 仅成本价可编辑
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
Upload,
|
Upload,
|
||||||
} from 'antd';
|
} 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 type { TableProps } from 'antd';
|
||||||
import XinTable from '@/components/XinTable';
|
import XinTable from '@/components/XinTable';
|
||||||
import AuthButton from '@/components/AuthButton';
|
import AuthButton from '@/components/AuthButton';
|
||||||
@@ -28,7 +28,7 @@ import type {IProductCategoryTree} from '@/domain/iProductCategory.ts';
|
|||||||
import { getSupplierOptions } from '@/api/customer/supplier.ts';
|
import { getSupplierOptions } from '@/api/customer/supplier.ts';
|
||||||
import type ISupplier from '@/domain/iSupplier.ts';
|
import type ISupplier from '@/domain/iSupplier.ts';
|
||||||
import { getCategoryTree } from '@/api/product/category.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 type { IImportError } from '@/api/product/goods.ts';
|
||||||
import { downloadProductTemplate, exportProducts, importProducts } 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 [activeCategory, setActiveCategory] = useState<number | undefined>(undefined);
|
||||||
const tableRef = useRef<XinTableInstance<IProduct> | null>(null);
|
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);
|
const [matrixOpen, setMatrixOpen] = useState(false);
|
||||||
@@ -489,6 +517,17 @@ const ProductGoodsPage: React.FC = () => {
|
|||||||
actionBarRender: (dom) => [
|
actionBarRender: (dom) => [
|
||||||
dom.add,
|
dom.add,
|
||||||
dom.search,
|
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 key="matrix" icon={<TableOutlined />} onClick={openMatrix}>
|
||||||
价格矩阵
|
价格矩阵
|
||||||
</Button>,
|
</Button>,
|
||||||
@@ -514,6 +553,10 @@ const ProductGoodsPage: React.FC = () => {
|
|||||||
...params,
|
...params,
|
||||||
category_id: activeCategory
|
category_id: activeCategory
|
||||||
}),
|
}),
|
||||||
|
rowSelection: {
|
||||||
|
selectedRowKeys,
|
||||||
|
onChange: (keys) => setSelectedRowKeys(keys.map(Number)),
|
||||||
|
},
|
||||||
modalProps: {
|
modalProps: {
|
||||||
width: 800,
|
width: 800,
|
||||||
centered: true,
|
centered: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user