订单详情

This commit is contained in:
liu
2026-08-11 23:26:34 +08:00
parent 5d26f04ba2
commit ce958d690f
10 changed files with 996 additions and 70 deletions
File diff suppressed because one or more lines are too long
@@ -83,12 +83,19 @@ class OrderController extends BaseMiniController
$rows[] = [
'store_id' => $store->id,
'product_id' => $productId,
'category_id' => (int) $product->category_id,
'supplier_id' => (int) $product->supplier_id,
'product_name' => $product->name,
'product_spec' => $product->spec,
'unit' => (string) $product->unit,
'price' => $price,
'image_ids' => implode(',', (array) $product->image_ids),
'content' => (string) $product->content,
'shelf_life' => (int) $product->shelf_life,
'quantity' => $quantity,
'weight' => 0,
'amount' => $amount,
'cost_price' => (string) $product->cost_price,
'remark' => '',
'created_at' => $now,
'updated_at' => $now,
@@ -5,6 +5,7 @@ namespace App\Http\Controllers\Order;
use App\Exceptions\RepositoryException;
use App\Models\NoticeModel;
use App\Models\ProductModel;
use App\Models\ProductPriceModel;
use App\Models\StoreOrderItemModel;
use App\Models\StoreOrderModel;
use App\Models\UserModel;
@@ -15,9 +16,10 @@ use Modules\AnnoRoute\Attribute\GetRoute;
use Modules\AnnoRoute\Attribute\PutRoute;
use Modules\AnnoRoute\Attribute\RequestAttribute;
use Modules\Common\Http\Controllers\BaseController;
use Modules\SystemTool\Models\SysFileModel;
/**
* 门店订单管理(订单只读 + 状态管理;创建/取消在小程序端)
* 门店订单管理(订单只读 + 状态管理 + 明细修改/同步;创建/取消在小程序端)
*/
#[RequestAttribute('/order/store', 'order.store')]
class StoreOrderController extends BaseController
@@ -32,6 +34,14 @@ class StoreOrderController extends BaseController
StoreOrderModel::STATUS_CANCELLED => '已取消',
];
/** 明细可编辑状态:待接单、已接单、采购中、配送中(已完成/已取消锁定) */
private const array ITEM_EDITABLE_STATUS = [
StoreOrderModel::STATUS_PENDING,
StoreOrderModel::STATUS_SUMMARIZED,
StoreOrderModel::STATUS_DELIVERING,
StoreOrderModel::STATUS_DISTRIBUTION,
];
protected array $searchField = [
'store_id' => '=',
'status' => '=',
@@ -39,7 +49,7 @@ class StoreOrderController extends BaseController
'order_date' => 'betweenDate',
];
/** 订单列表 */
/** 订单列表(支持按包含的商品名称搜索 ?product_name= */
#[GetRoute(authorize: 'query')]
public function query(Request $request): JsonResponse
{
@@ -47,14 +57,36 @@ class StoreOrderController extends BaseController
$pageSize = $params['pageSize'] ?? 10;
$query = StoreOrderModel::query()->with([
'store:id,name,address,contact,phone',
'items:id,order_id,product_id,product_name,product_spec,price,quantity,amount',
'items.product'
'items:id,order_id,product_id,product_name,product_spec,unit,price,quantity,amount,image_ids',
]);
// 按包含的商品名称搜索:任一明细品名包含关键字即命中
$productName = trim((string) ($params['product_name'] ?? ''));
if ($productName !== '') {
$keyword = '%' . str_replace('%', '\%', $productName) . '%';
$query->whereHas('items', static function ($itemQuery) use ($keyword) {
$itemQuery->where('product_name', 'like', $keyword);
});
}
$data = $this->buildSearch($params, $query)
->orderBy('order_date', 'desc')
->orderBy('id', 'desc')
->paginate($pageSize)
->toArray();
// Paginator::toArray() 的 data 仍为模型,先显式转数组(明细/关系一并转换)
$data['data'] = array_map(
static fn ($order) => is_object($order) ? $order->toArray() : $order,
$data['data']
);
// 明细封面图:从快照 image_ids 批量解析(不再关联商品档案表)
foreach ($data['data'] as &$order) {
$this->resolveItemImages($order['items']);
}
unset($order);
$box_amount = site_config('services.box_amount');
$tray_amount = site_config('services.tray_amount');
foreach ($data['data'] as &$item) {
@@ -62,21 +94,13 @@ class StoreOrderController extends BaseController
$item['tray_price'] = number_format($tray_amount, 2);
$item['box_amount'] = number_format($box_amount * $item['box_num'], 2);
$item['tray_amount'] = number_format($tray_amount * $item['tray_num'], 2);
if(count($item['items']) > 0) {
foreach ($item['items'] as &$product) {
if($product['product'] && $product['product']['images_arr'] > 0) {
$product['image'] = $product['product']['images_arr'][0]['preview_url'] ?? '';
unset($product['product']);
}
}
}
}
return $this->success($data);
}
/**
* 已接单预览:聚合所有已接单订单明细(按商品分组),
* 供生成采购单前确认(C1 前置)
* 供生成采购单前确认(C1 前置);单位取明细快照
*/
#[GetRoute('/summary', 'query')]
public function summary(): JsonResponse
@@ -85,6 +109,7 @@ class StoreOrderController extends BaseController
->select('product_id')
->selectRaw('MAX(product_name) as product_name')
->selectRaw('MAX(product_spec) as product_spec')
->selectRaw('MAX(unit) as unit')
->selectRaw('SUM(quantity) as total_quantity')
->selectRaw('COUNT(DISTINCT store_id) as store_count')
->whereHas('order', function ($query) {
@@ -95,26 +120,205 @@ class StoreOrderController extends BaseController
->get()
->toArray();
// 补充计价单位(商品档案含已下架/软删除)
$units = ProductModel::withTrashed()
->whereIn('id', array_column($rows, 'product_id'))
->pluck('unit', 'id');
foreach ($rows as &$row) {
$row['unit'] = $units[$row['product_id']] ?? '';
// 快照无单位的历史明细兜底:从商品档案含已下架/软删除)补齐
$emptyUnitProductIds = array_column(array_filter(
$rows,
static fn (array $row) => ($row['unit'] ?? '') === ''
), 'product_id');
if ($emptyUnitProductIds !== []) {
$units = ProductModel::withTrashed()
->whereIn('id', $emptyUnitProductIds)
->pluck('unit', 'id');
foreach ($rows as &$row) {
if (($row['unit'] ?? '') === '') {
$row['unit'] = $units[$row['product_id']] ?? '';
}
}
}
return $this->success($rows);
}
/** 订单详情:订单头 + 明细(含商品快照) */
/** 订单详情:订单头 + 明细(含商品快照、首图;后台侧成本价可见、附供应商名 */
#[GetRoute(route: '/{id}', authorize: 'query', where: ['id' => '[0-9]+'])]
public function detail(int $id): JsonResponse
{
$order = StoreOrderModel::with(['store:id,name', 'items'])->find($id);
$order = StoreOrderModel::with([
'store:id,name,address,contact,phone',
'items' => static fn ($query) => $query->with('supplier:id,name'),
])->find($id);
if (empty($order)) {
throw new RepositoryException('订单不存在');
}
return $this->success($order->toArray());
// 成本价默认对序列化隐藏(防泄漏到小程序端),后台恢复可见
$order->items->each->makeVisible('cost_price');
$data = $order->toArray();
// 明细首图 + 附加金额(与列表接口一致)
$this->resolveItemImages($data['items']);
$boxAmount = site_config('services.box_amount');
$trayAmount = site_config('services.tray_amount');
$data['box_price'] = number_format($boxAmount, 2);
$data['tray_price'] = number_format($trayAmount, 2);
$data['box_amount'] = number_format($boxAmount * $data['box_num'], 2);
$data['tray_amount'] = number_format($trayAmount * $data['tray_num'], 2);
return $this->success($data);
}
/**
* 明细首图解析:从快照 image_ids 批量解析文件 URL,未找到时置空字符串(列表/详情共用)
*
* @param array<int, array<string, mixed>> $items 订单明细数组(引用修改)
*/
private function resolveItemImages(array &$items): void
{
$fileIds = [];
foreach ($items as $line) {
foreach ((array) ($line['image_ids'] ?? []) as $fileId) {
if ($fileId !== '' && $fileId !== null) {
$fileIds[] = (int) $fileId;
}
}
}
$fileUrls = [];
if ($fileIds !== []) {
foreach (SysFileModel::query()->whereIn('id', array_unique($fileIds))->get() as $file) {
$fileUrls[$file->id] = $file->preview_url;
}
}
foreach ($items as &$product) {
$product['image'] = '';
foreach ((array) ($product['image_ids'] ?? []) as $fileId) {
if ($fileId !== '' && $fileId !== null && isset($fileUrls[(int) $fileId])) {
$product['image'] = $fileUrls[(int) $fileId];
break;
}
}
unset($product['image_ids']);
}
}
/**
* 修改订单明细(商品快照 + 订货量/重量),事务内重算单品金额与订单总价。
* 可改字段:供应商、品名、规格、单位、单价、成本价、订货量、重量
*/
#[PutRoute(route: '/item/{id}', authorize: 'update', where: ['id' => '[0-9]+'])]
public function updateItem(int $id, Request $request): JsonResponse
{
$data = $request->validate([
'supplier_id' => 'required|integer|min:0',
'product_name' => 'required|string|max:100',
'product_spec' => 'nullable|string|max:100',
'unit' => 'required|string|max:20',
'price' => 'required|numeric|min:0',
'cost_price' => 'required|numeric|min:0',
'quantity' => 'required|integer|min:1',
'weight' => 'required|numeric|min:0',
'remark' => 'nullable|string'
], [
'supplier_id.required' => '供应商不能为空',
'supplier_id.integer' => '供应商ID必须为整数',
'supplier_id.min' => '供应商ID不正确',
'product_name.required' => '品名不能为空',
'product_name.max' => '品名最长 100 个字符',
'product_spec.max' => '规格最长 100 个字符',
'unit.required' => '计价单位不能为空',
'unit.max' => '计价单位最长 20 个字符',
'price.required' => '单价不能为空',
'price.numeric' => '单价必须为数字',
'price.min' => '单价不能小于 0',
'cost_price.required' => '成本价不能为空',
'cost_price.numeric' => '成本价必须为数字',
'cost_price.min' => '成本价不能小于 0',
'quantity.required' => '订货量不能为空',
'quantity.integer' => '订货量必须为整数',
'quantity.min' => '订货量必须大于 0',
'weight.required' => '重量不能为空',
'weight.numeric' => '重量必须为数字',
'weight.min' => '重量不能小于 0',
]);
return DB::transaction(function () use ($id, $data) {
$item = StoreOrderItemModel::query()->lockForUpdate()->find($id);
if (empty($item)) {
throw new RepositoryException('订单明细不存在');
}
$order = StoreOrderModel::query()->lockForUpdate()->find($item->order_id);
if (empty($order)) {
throw new RepositoryException('订单不存在');
}
$this->assertItemEditable($order);
$item->fill($data);
$item->product_spec = (string) ($data['product_spec'] ?? '');
$item->amount = bcmul(
bcadd((string) $data['price'], '0', 2),
(string) $data['quantity'],
2
);
$item->save();
$this->recalculateOrderTotals($order);
return $this->success($item->load('supplier:id,name')->makeVisible('cost_price')->toArray());
});
}
/**
* 一键同步明细商品快照:按商品ID同步最新商品档案的
* 供应商、品名、规格、单位、成本价;单价按门店当前等级价重算
* (未设置等级价时保留原单价),并重算订单总价
*/
#[PutRoute(route: '/item/{id}/sync', authorize: 'update', where: ['id' => '[0-9]+'])]
public function syncItem(int $id): JsonResponse
{
return DB::transaction(function () use ($id) {
$item = StoreOrderItemModel::query()->lockForUpdate()->find($id);
if (empty($item)) {
throw new RepositoryException('订单明细不存在');
}
$order = StoreOrderModel::query()->lockForUpdate()->find($item->order_id);
if (empty($order)) {
throw new RepositoryException('订单不存在');
}
$this->assertItemEditable($order);
// 软删除商品无法同步(下架商品仍可同步最新档案)
$product = ProductModel::find($item->product_id);
if (empty($product)) {
throw new RepositoryException('商品不存在或已被删除,无法同步');
}
$item->supplier_id = (int) $product->supplier_id;
$item->product_name = $product->name;
$item->product_spec = (string) $product->spec;
$item->unit = (string) $product->unit;
$item->cost_price = $product->cost_price;
// 单价:按订货门店当前客户等级价重算(百分比计价按最新成本价换算)
$levelId = (int) ($order->store->level_id ?? 0);
$priceRow = ProductPriceModel::query()
->forProductLevel((int) $item->product_id, $levelId)
->first(['price', 'price_type', 'percent']);
if ($priceRow !== null) {
$item->price = ProductPriceModel::calcActualPrice(
(int) $priceRow->price_type,
$priceRow->price,
$priceRow->percent,
$product->cost_price,
);
}
$item->amount = bcmul((string) $item->price, (string) $item->quantity, 2);
$item->save();
$this->recalculateOrderTotals($order);
return $this->success($item->load('supplier:id,name')->makeVisible('cost_price')->toArray());
});
}
/**
@@ -274,6 +478,39 @@ class StoreOrderController extends BaseController
return $this->success();
}
/**
* 明细编辑状态校验:已完成/已取消订单锁定
*/
private function assertItemEditable(StoreOrderModel $order): void
{
if (! in_array($order->status, self::ITEM_EDITABLE_STATUS, true)) {
throw new RepositoryException(
'订单当前状态为「' . (self::STATUS_NAMES[$order->status] ?? $order->status) . '」,不允许修改商品明细'
);
}
}
/**
* 重算订单汇总:明细金额合计 → 商品总金额;订货量/重量合计 → 订货总量/总重量;
* 订单总金额 = 商品总金额 + 附加金额(恒成立)
*/
private function recalculateOrderTotals(StoreOrderModel $order): void
{
$totals = StoreOrderItemModel::query()
->where('order_id', $order->id)
->selectRaw('COALESCE(SUM(quantity), 0) as total_quantity')
->selectRaw('COALESCE(SUM(weight), 0) as total_weight')
->selectRaw('COALESCE(SUM(amount), 0) as product_amount')
->first();
$productAmount = bcadd((string) $totals->product_amount, '0', 2);
$order->total_quantity = (int) $totals->total_quantity;
$order->total_weight = bcadd((string) $totals->total_weight, '0', 3);
$order->product_amount = $productAmount;
$order->total_amount = bcadd($productAmount, (string) $order->added_amount, 2);
$order->save();
}
/**
* 状态流转后通知门店用户
*/
+47 -1
View File
@@ -2,12 +2,14 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* 门店订单明细模型(快照下单时的商品名/规格/等级价,历史单据不受调价影响)
* 门店订单明细模型(下单时快照商品档案:品名/规格/单位/供应商/图文/成本价 + 等级单价,
* 商品调价或档案变更不影响历史单据)
*/
class StoreOrderItemModel extends Model
{
@@ -20,12 +22,19 @@ class StoreOrderItemModel extends Model
'order_id',
'store_id',
'product_id',
'category_id',
'supplier_id',
'product_name',
'product_spec',
'unit',
'price',
'image_ids',
'content',
'shelf_life',
'quantity',
'weight',
'amount',
'cost_price',
'remark',
];
@@ -33,12 +42,33 @@ class StoreOrderItemModel extends Model
'order_id' => 'integer',
'store_id' => 'integer',
'product_id' => 'integer',
'category_id' => 'integer',
'supplier_id' => 'integer',
'price' => 'decimal:2',
'shelf_life' => 'integer',
'quantity' => 'integer',
'weight' => 'decimal:3',
'amount' => 'decimal:2',
'cost_price' => 'decimal:2',
];
/**
* 成本价属商业敏感数据,默认不随 toArray 输出(防止泄漏到小程序端);
* 后台管理接口需在查询结果上调用 makeVisible('cost_price') 恢复。
*/
protected $hidden = ['cost_price'];
/**
* 商品图片ID(逗号分隔字符串 ↔ 数组)
*/
public function imageIds(): Attribute
{
return Attribute::make(
get: fn ($value) => $value === '' || $value === null ? [] : explode(',', (string) $value),
set: fn ($value) => is_array($value) ? implode(',', $value) : $value,
);
}
/**
* 所属订单
*/
@@ -62,4 +92,20 @@ class StoreOrderItemModel extends Model
{
return $this->belongsTo(ProductModel::class, 'product_id', 'id');
}
/**
* 快照供应商
*/
public function supplier(): BelongsTo
{
return $this->belongsTo(SupplierModel::class, 'supplier_id', 'id');
}
/**
* 快照商品分类
*/
public function category(): BelongsTo
{
return $this->belongsTo(ProductCategoryModel::class, 'category_id', 'id');
}
}
@@ -21,12 +21,19 @@ class StoreOrderItemModelFactory extends Factory
'order_id' => 0,
'store_id' => 0,
'product_id' => 0,
'category_id' => 0,
'supplier_id' => 0,
'product_name' => '测试商品',
'product_spec' => '500g/袋',
'unit' => '斤',
'price' => number_format(random_int(100, 5000) / 100, 2, '.', ''),
'image_ids' => '',
'content' => '',
'shelf_life' => 0,
'quantity' => number_format(random_int(100, 10000) / 100, 2, '.', ''),
'weight' => 0,
'amount' => 0,
'cost_price' => 0,
'remark' => '',
];
}
@@ -42,14 +42,21 @@ return new class extends Migration
Schema::create('store_order_item', function (Blueprint $table) {
$table->increments('id')->comment('明细ID');
$table->integer('order_id')->comment('订单ID');
$table->integer('store_id')->comment('门店ID(冗余,便于按门店筛选)');
$table->integer('store_id')->comment('门店ID');
$table->integer('product_id')->comment('商品ID');
$table->string('product_name', 100)->comment('品名(快照)');
$table->string('product_spec', 100)->default('')->comment('规格/包规(快照)');
$table->decimal('price', 10, 2)->default(0)->comment('单价(下单时客户等级价快照)');
$table->integer('category_id')->default(0)->comment('分类ID');
$table->integer('supplier_id')->default(0)->comment('供应商ID');
$table->string('product_name', 100)->comment('品名');
$table->string('product_spec', 100)->default('')->comment('规格/包规');
$table->string('unit', 20)->default('斤')->comment('计价单位(斤/件/箱等)');
$table->decimal('price', 10, 2)->default(0)->comment('单价');
$table->string('image_ids', 255)->default('')->comment('商品图片');
$table->text('content')->comment('商品图文详情');
$table->integer('shelf_life')->default(0)->comment('保质期');
$table->integer('quantity')->default(0)->comment('订货量');
$table->decimal('weight', 10, 3)->default(0)->comment('重量');
$table->decimal('amount', 10, 2)->default(0)->comment('单品金额');
$table->decimal('cost_price', 10, 2)->default(0)->comment('成本价');
$table->string('remark', 255)->default('')->comment('门店下单备注');
$table->timestamps();
$table->index(['order_id'], 'store_order_item_order_index');
+330
View File
@@ -0,0 +1,330 @@
<?php
namespace Tests\Feature;
use App\Models\CustomerLevelModel;
use App\Models\ProductModel;
use App\Models\ProductPriceModel;
use App\Models\StoreModel;
use App\Models\StoreOrderModel;
use App\Models\SupplierModel;
use App\Models\UserModel;
/**
* 门店订单明细(商品快照):
* 下单快照完整商品档案、成本价防泄漏、后台明细修改重算总价、一键同步商品档案、按商品名称搜索订单
*/
class StoreOrderItemTest extends ProcurementTestCase
{
/**
* 造门店 + 上架商品(指定成本价)+ 固定等级价 + 门店用户
*
* @return array{0: StoreModel, 1: ProductModel, 2: UserModel}
*/
private function makeStoreWithProduct(string $price = '5.00', string $costPrice = '4.00'): array
{
$level = CustomerLevelModel::factory()->create();
$store = StoreModel::factory()->create(['level_id' => $level->id]);
$product = ProductModel::factory()->create([
'status' => ProductModel::STATUS_ON,
'cost_price' => $costPrice,
]);
ProductPriceModel::factory()->create([
'product_id' => $product->id,
'level_id' => $level->id,
'price' => $price,
]);
return [$store, $product, UserModel::factory()->forStore($store->id)->create()];
}
/** 以小程序身份下一单并返回订单(可指定初始状态) */
private function placeOrder(ProductModel $product, UserModel $user, int $quantity = 2, int $status = StoreOrderModel::STATUS_PENDING): StoreOrderModel
{
$this->actingAsMiniUser($user);
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => $quantity]]])
->assertOk()->assertJsonPath('success', true);
$order = StoreOrderModel::where('store_id', $user->store_id)->latest('id')->first();
$this->assertNotNull($order);
if ($status !== StoreOrderModel::STATUS_PENDING) {
$order->update(['status' => $status]);
}
return $order;
}
/** 下单时明细快照完整商品档案(分类/供应商/单位/图片/图文/保质期/成本价) */
public function test_place_order_snapshots_full_product_info(): void
{
$supplier = SupplierModel::factory()->create();
$level = CustomerLevelModel::factory()->create();
$store = StoreModel::factory()->create(['level_id' => $level->id]);
$product = ProductModel::factory()->create([
'status' => ProductModel::STATUS_ON,
'supplier_id' => $supplier->id,
'category_id' => 7,
'unit' => '箱',
'image_ids' => '1,2',
'content' => '图文详情',
'shelf_life' => 30,
'cost_price' => '4.00',
]);
ProductPriceModel::factory()->create([
'product_id' => $product->id,
'level_id' => $level->id,
'price' => '5.50',
]);
$user = UserModel::factory()->forStore($store->id)->create();
$this->actingAsMiniUser($user);
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 3]]])
->assertOk()->assertJsonPath('success', true);
$item = StoreOrderModel::where('store_id', $store->id)->first()->items->first();
$this->assertSame($supplier->id, $item->supplier_id, '快照供应商');
$this->assertSame(7, $item->category_id, '快照分类');
$this->assertSame('箱', $item->unit, '快照单位');
$this->assertSame('1,2', implode(',', $item->image_ids), '快照图片');
$this->assertSame('图文详情', $item->content, '快照图文详情');
$this->assertSame(30, $item->shelf_life, '快照保质期');
$this->assertSame('4.00', (string) $item->cost_price, '快照成本价');
$this->assertSame('5.50', (string) $item->price, '快照等级单价');
}
/** 小程序订单详情不泄漏成本价 */
public function test_mini_detail_hides_cost_price(): void
{
[, $product, $user] = $this->makeStoreWithProduct('5.00', '4.00');
$order = $this->placeOrder($product, $user);
$this->actingAsMiniUser($user);
$this->getJson("/mini/order/{$order->id}")
->assertOk()
->assertJsonPath('success', true)
->assertJsonMissingPath('data.items.0.cost_price');
}
/** 后台订单详情:成本价可见、附供应商名与单位 */
public function test_admin_detail_shows_cost_price_and_supplier(): void
{
$supplier = SupplierModel::factory()->create();
[, $product, $user] = $this->makeStoreWithProduct('5.00', '4.00');
$product->update(['supplier_id' => $supplier->id]);
$order = $this->placeOrder($product, $user);
$this->actingAsSysUser();
$this->getJson("/order/store/{$order->id}")
->assertOk()
->assertJsonPath('success', true)
->assertJsonPath('data.items.0.cost_price', '4.00')
->assertJsonPath('data.items.0.unit', $product->unit)
->assertJsonPath('data.items.0.supplier.name', $supplier->name);
}
/** 修改明细:重算单品金额与订单总量/总额(总额 = 商品 + 附加) */
public function test_update_item_recalculates_order_totals(): void
{
[$store, $product, $user] = $this->makeStoreWithProduct('5.00');
$order = $this->placeOrder($product, $user, 2, StoreOrderModel::STATUS_SUMMARIZED);
$order->update(['added_amount' => '6.00']); // 已有附加金额
$item = $order->items->first();
$supplier = SupplierModel::factory()->create();
$this->actingAsSysUser();
$this->putJson("/order/store/item/{$item->id}", [
'supplier_id' => $supplier->id,
'product_name' => '改名后的商品',
'product_spec' => '新规格',
'unit' => '箱',
'price' => '6.00',
'cost_price' => '3.50',
'quantity' => 5,
'weight' => '2.5',
])->assertOk()->assertJsonPath('success', true);
$item->refresh();
$this->assertSame($supplier->id, $item->supplier_id);
$this->assertSame('改名后的商品', $item->product_name);
$this->assertSame('新规格', $item->product_spec);
$this->assertSame('箱', $item->unit);
$this->assertSame('6.00', (string) $item->price);
$this->assertSame('3.50', (string) $item->cost_price);
$this->assertSame(5, $item->quantity);
$this->assertSame('2.500', (string) $item->weight);
$this->assertSame('30.00', (string) $item->amount, '6.00 × 5');
$order->refresh();
$this->assertSame(5, $order->total_quantity, '订货总量 = 明细合计');
$this->assertSame('2.500', (string) $order->total_weight, '总重量 = 明细合计');
$this->assertSame('30.00', (string) $order->product_amount, '商品总金额 = 明细金额合计');
$this->assertSame('36.00', (string) $order->total_amount, '订单总金额 = 商品 30 + 附加 6');
}
/** 已完成/已取消订单不允许修改明细 */
public function test_update_item_rejected_when_completed_or_cancelled(): void
{
[, $product, $user] = $this->makeStoreWithProduct('5.00');
$order = $this->placeOrder($product, $user, 1);
$item = $order->items->first();
$payload = [
'supplier_id' => 0,
'product_name' => '不应生效',
'product_spec' => '',
'unit' => '斤',
'price' => '1.00',
'cost_price' => '1.00',
'quantity' => 1,
'weight' => 0,
];
$this->actingAsSysUser();
foreach ([StoreOrderModel::STATUS_COMPLETED, StoreOrderModel::STATUS_CANCELLED] as $status) {
$order->update(['status' => $status]);
$this->putJson("/order/store/item/{$item->id}", $payload)
->assertOk()->assertJsonPath('success', false);
}
$this->assertNotSame('不应生效', $item->fresh()->product_name, '被拒绝后明细不变');
$this->assertSame('5.00', (string) $order->fresh()->total_amount, '被拒绝后总金额不变');
}
/** 修改明细参数校验:负单价被拦截 */
public function test_update_item_validates_negative_price(): void
{
[, $product, $user] = $this->makeStoreWithProduct('5.00');
$order = $this->placeOrder($product, $user, 1, StoreOrderModel::STATUS_SUMMARIZED);
$item = $order->items->first();
$this->actingAsSysUser();
$this->putJson("/order/store/item/{$item->id}", [
'supplier_id' => 0,
'product_name' => $item->product_name,
'unit' => '斤',
'price' => -1,
'cost_price' => 0,
'quantity' => 1,
'weight' => 0,
])->assertOk()
->assertJsonPath('success', false)
->assertJsonPath('msg', '单价不能小于 0');
}
/** 一键同步:按商品ID同步最新档案(固定价等级保留原单价) */
public function test_sync_item_updates_snapshot_from_product(): void
{
[$store, $product, $user] = $this->makeStoreWithProduct('5.50', '4.00');
$order = $this->placeOrder($product, $user, 3, StoreOrderModel::STATUS_SUMMARIZED);
$item = $order->items->first();
$this->assertSame('16.50', (string) $item->amount);
// 下单后商品档案变更:改名/改规格/改单位/换供应商/调成本价
$supplier = SupplierModel::factory()->create();
$product->update([
'name' => '同步后的品名',
'spec' => '同步后的规格',
'unit' => '箱',
'supplier_id' => $supplier->id,
'cost_price' => '9.99',
]);
$this->actingAsSysUser();
$this->putJson("/order/store/item/{$item->id}/sync")
->assertOk()->assertJsonPath('success', true);
$item->refresh();
$this->assertSame('同步后的品名', $item->product_name);
$this->assertSame('同步后的规格', $item->product_spec);
$this->assertSame('箱', $item->unit);
$this->assertSame($supplier->id, $item->supplier_id);
$this->assertSame('9.99', (string) $item->cost_price);
$this->assertSame('5.50', (string) $item->price, '固定价等级单价不随档案变化');
$this->assertSame('16.50', (string) $item->amount, '单价未变,金额不变');
}
/** 一键同步:百分比计价等级按最新成本价重算单价与订单总价 */
public function test_sync_item_recalculates_percent_price_with_latest_cost(): void
{
$level = CustomerLevelModel::factory()->create();
$store = StoreModel::factory()->create(['level_id' => $level->id]);
$product = ProductModel::factory()->create([
'status' => ProductModel::STATUS_ON,
'cost_price' => '10.00',
]);
ProductPriceModel::factory()->create([
'product_id' => $product->id,
'level_id' => $level->id,
'price_type' => ProductPriceModel::PRICE_TYPE_PERCENT,
'percent' => 30,
]);
$user = UserModel::factory()->forStore($store->id)->create();
$order = $this->placeOrder($product, $user, 2, StoreOrderModel::STATUS_SUMMARIZED);
$item = $order->items->first();
$this->assertSame('13.00', (string) $item->price, '下单时 10 元上浮 30%');
$this->assertSame('26.00', (string) $order->total_amount);
// 成本价上调后同步:单价应按最新成本重算
$product->update(['cost_price' => '20.00']);
$this->actingAsSysUser();
$this->putJson("/order/store/item/{$item->id}/sync")
->assertOk()->assertJsonPath('success', true);
$item->refresh();
$this->assertSame('20.00', (string) $item->cost_price);
$this->assertSame('26.00', (string) $item->price, '20 元上浮 30% = 26.00');
$this->assertSame('52.00', (string) $item->amount, '26.00 × 2');
$order->refresh();
$this->assertSame('52.00', (string) $order->product_amount);
$this->assertSame('52.00', (string) $order->total_amount);
}
/** 一键同步:商品已删除时拒绝同步 */
public function test_sync_item_rejected_when_product_deleted(): void
{
[, $product, $user] = $this->makeStoreWithProduct('5.00');
$order = $this->placeOrder($product, $user, 1, StoreOrderModel::STATUS_SUMMARIZED);
$item = $order->items->first();
$product->delete(); // 软删除
$this->actingAsSysUser();
$this->putJson("/order/store/item/{$item->id}/sync")
->assertOk()
->assertJsonPath('success', false)
->assertJsonPath('msg', '商品不存在或已被删除,无法同步');
}
/** 订单列表按包含的商品名称搜索 */
public function test_order_list_search_by_product_name(): void
{
$level = CustomerLevelModel::factory()->create();
$store = StoreModel::factory()->create(['level_id' => $level->id]);
$user = UserModel::factory()->forStore($store->id)->create();
$cabbage = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'name' => '大白菜A']);
$potato = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'name' => '土豆B']);
foreach ([$cabbage, $potato] as $p) {
ProductPriceModel::factory()->create([
'product_id' => $p->id,
'level_id' => $level->id,
'price' => '5.00',
]);
}
$this->placeOrder($cabbage, $user);
$this->placeOrder($potato, $user);
$this->actingAsSysUser();
$response = $this->getJson('/order/store?product_name=' . urlencode('白菜'));
$response->assertOk()->assertJsonPath('success', true);
$this->assertSame(1, $response->json('data.total'), '仅命中包含「白菜」的订单');
$this->assertSame('大白菜A', $response->json('data.data.0.items.0.product_name'));
// 无匹配关键字 → 空列表
$this->getJson('/order/store?product_name=' . urlencode('不存在的商品'))
->assertOk()->assertJsonPath('data.total', 0);
}
}
+18 -1
View File
@@ -1,6 +1,6 @@
import createAxios from '@/utils/request';
import type IStoreOrder from '@/domain/iStoreOrder.ts';
import type { IOrderSummaryRow } from '@/domain/iStoreOrder.ts';
import type { IOrderSummaryRow, IStoreOrderItem, IStoreOrderItemUpdate } from '@/domain/iStoreOrder.ts';
/** 订单详情(头 + 明细) */
export async function getStoreOrder(id: number) {
@@ -44,3 +44,20 @@ export async function getOrderSummary() {
method: 'get',
});
}
/** 修改订单明细(供应商/品名/规格/单位/单价/成本价/订货量/重量,后端重算订单总价) */
export async function updateOrderItem(itemId: number, data: IStoreOrderItemUpdate) {
return createAxios<IStoreOrderItem>({
url: `/order/store/item/${itemId}`,
method: 'put',
data,
});
}
/** 一键同步明细商品快照为最新商品档案信息(供应商/品名/规格/单位/单价/成本价) */
export async function syncOrderItem(itemId: number) {
return createAxios<IStoreOrderItem>({
url: `/order/store/item/${itemId}/sync`,
method: 'put',
});
}
+24 -1
View File
@@ -1,19 +1,42 @@
/** 门店订单明细(商品快照) */
/** 门店订单明细(下单时商品档案快照) */
export interface IStoreOrderItem {
id?: number;
order_id?: number;
store_id?: number;
product_id?: number;
category_id?: number;
supplier_id?: number;
/** 详情接口附带的供应商信息 */
supplier?: { id: number; name: string };
product_name?: string;
product_spec?: string;
/** 计价单位(斤/件/箱等) */
unit?: string;
price?: string;
/** 列表接口附带的封面图 */
image?: string;
image_ids?: string;
shelf_life?: number;
quantity?: number;
weight?: string;
amount?: string;
/** 成本价(仅后台接口可见) */
cost_price?: string;
remark?: string;
}
/** 修改订单明细入参(保存后后端重算单品金额与订单总价) */
export interface IStoreOrderItemUpdate {
supplier_id: number;
product_name: string;
product_spec?: string;
unit: string;
price: number;
cost_price: number;
quantity: number;
weight: number;
}
/** 门店订单 */
export default interface IStoreOrder {
id?: number;
+292 -40
View File
@@ -4,20 +4,21 @@ import {
DatePicker,
Descriptions,
Drawer,
Empty,
Form,
Input,
InputNumber,
message,
Modal,
Popconfirm,
Radio,
Select,
Space,
Table,
Tag,
Typography,
Image,
} from 'antd';
import dayjs from 'dayjs';
import type { TableProps } from 'antd';
import XinTable from '@/components/XinTable';
import type {
XinTableColumn,
@@ -25,20 +26,33 @@ import type {
XinTableProps,
} from '@/components/XinTable/typings.ts';
import type IStoreOrder from '@/domain/iStoreOrder.ts';
import type { IStoreOrderItem } from '@/domain/iStoreOrder.ts';
import type { IStoreOrderItem, IStoreOrderItemUpdate } from '@/domain/iStoreOrder.ts';
import { STORE_ORDER_STATUS_MAP } from '@/domain/iStoreOrder.ts';
import { getStoreOrder, updateOrderContainer, updateOrderStatus, batchUpdateOrderStatus } from '@/api/order/store.ts';
import {
getStoreOrder,
updateOrderContainer,
updateOrderStatus,
batchUpdateOrderStatus,
updateOrderItem,
syncOrderItem,
} from '@/api/order/store.ts';
import { generatePurchase } from '@/api/purchase/order.ts';
import { getStoreOptions } from '@/api/customer/store.ts';
import { getSupplierOptions } from '@/api/customer/supplier.ts';
import type IStore from '@/domain/iStore.ts';
import type ISupplier from '@/domain/iSupplier.ts';
import AuthButton from '@/components/AuthButton';
import {UnorderedListOutlined} from "@ant-design/icons";
import { EditOutlined, SyncOutlined, UnorderedListOutlined } from '@ant-design/icons';
import TextArea from "antd/es/input/TextArea";
const { Title, Text } = Typography;
/** 允许修改周转框/托盘数量的订单状态:已接单、采购中、配送中 */
const CONTAINER_EDITABLE_STATUS = [1, 2, 3];
/** 允许修改/同步商品明细的订单状态:待接单、已接单、采购中、配送中(已完成/已取消锁定) */
const ITEM_EDITABLE_STATUS = [0, 1, 2, 3];
/**
* 状态流转合法路径:待接单→已接单/已取消;采购中→配送中/已完成;配送中→已完成
* (已接单→采购中 通过「生成采购单」完成,不在流转按钮内)
@@ -90,8 +104,18 @@ const StoreOrderPage: React.FC = () => {
const [generateLoading, setGenerateLoading] = useState(false);
const [generateForm] = Form.useForm<{ purchase_date: dayjs.Dayjs }>();
/** 供应商选项(明细编辑用) */
const [suppliers, setSuppliers] = useState<ISupplier[]>([]);
/** 商品明细编辑(弹窗表单,保存后后端重算订单总价) */
const [itemEditOpen, setItemEditOpen] = useState(false);
const [itemEditTarget, setItemEditTarget] = useState<IStoreOrderItem | null>(null);
const [itemSaving, setItemSaving] = useState(false);
const [itemForm] = Form.useForm<IStoreOrderItemUpdate>();
useEffect(() => {
getStoreOptions().then((res) => setStores(res.data.data ?? []));
getSupplierOptions().then((res) => setSuppliers(res.data.data ?? []));
}, []);
const openDetail = async (id: number) => {
@@ -165,6 +189,46 @@ const StoreOrderPage: React.FC = () => {
}
};
/** 打开明细编辑弹窗(商品快照 + 订货量/重量) */
const openItemEdit = (item: IStoreOrderItem) => {
setItemEditTarget(item);
itemForm.setFieldsValue({
supplier_id: item.supplier_id ?? 0,
product_name: item.product_name ?? '',
product_spec: item.product_spec ?? '',
unit: item.unit ?? '',
price: Number(item.price ?? 0),
cost_price: Number(item.cost_price ?? 0),
quantity: Number(item.quantity ?? 0),
weight: Number(item.weight ?? 0),
});
setItemEditOpen(true);
};
/** 保存明细修改:后端重算单品金额与订单总价 */
const handleItemSave = async (values: IStoreOrderItemUpdate) => {
if (!itemEditTarget?.id || !detail?.id) return;
setItemSaving(true);
try {
await updateOrderItem(itemEditTarget.id, values);
message.success('明细已更新,订单总价已重算');
setItemEditOpen(false);
await openDetail(detail.id);
await tableRef.current?.reload();
} finally {
setItemSaving(false);
}
};
/** 一键同步:按商品ID拉取最新商品档案(供应商/品名/规格/单位/单价/成本价) */
const handleItemSync = async (item: IStoreOrderItem) => {
if (!item.id || !detail?.id) return;
await syncOrderItem(item.id);
message.success('已同步最新商品信息,订单总价已重算');
await openDetail(detail.id);
await tableRef.current?.reload();
};
// 弹窗内实时预览:附加金额 = 框×单价 + 托盘×单价;订单总金额 = 商品金额 + 附加金额
const watchBoxNum = Number(Form.useWatch('box_num', containerForm) ?? 0);
const watchTrayNum = Number(Form.useWatch('tray_num', containerForm) ?? 0);
@@ -172,14 +236,8 @@ const StoreOrderPage: React.FC = () => {
+ watchTrayNum * Number(containerOrder?.tray_price ?? 0);
const previewTotal = Number(containerOrder?.product_amount ?? 0) + previewAdded;
const itemColumns: TableProps<IStoreOrderItem>['columns'] = [
{ title: '品名', dataIndex: 'product_name' },
{ title: '规格', dataIndex: 'product_spec', render: (v) => v || '-' },
{ title: '单价', dataIndex: 'price', align: 'right', render: (v) => `¥${v}` },
{ title: '订货量', dataIndex: 'quantity', align: 'right' },
{ title: '金额', dataIndex: 'amount', align: 'right', render: (v) => `¥${v}` },
{ title: '备注', dataIndex: 'remark', render: (v) => v || '-' },
];
/** 明细行编辑/同步按钮(仅未完成、未取消订单可见) */
const itemEditable = ITEM_EDITABLE_STATUS.includes(detail?.status ?? -1);
const columns: XinTableColumn<IStoreOrder>[] = [
{
@@ -189,6 +247,13 @@ const StoreOrderPage: React.FC = () => {
valueType: 'text',
hideInForm: true
},
{
title: '商品名称',
hideInTable: true,
dataIndex: 'product_name',
valueType: 'text',
hideInForm: true
},
{
title: '商品信息',
dataIndex: 'items',
@@ -352,7 +417,7 @@ const StoreOrderPage: React.FC = () => {
hideInSearch: true,
render: (_, record) => (
<Space orientation={'vertical'}>
<Button key="detail" type={'primary'} icon={<UnorderedListOutlined />} onClick={() => openDetail(record.id!)}>
<Button key="detail" type={'link'} icon={<UnorderedListOutlined />} onClick={() => openDetail(record.id!)}>
</Button>
{
@@ -362,7 +427,7 @@ const StoreOrderPage: React.FC = () => {
title={`确认将订单状态更新为「${action.label}」?`}
onConfirm={() => handleStatusChange(record.id!, action.status)}
>
<Button type={action.danger ? undefined : 'primary'} danger={action.danger}>
<Button type={action.danger ? undefined : 'link'} danger={action.danger}>
{action.label}
</Button>
</Popconfirm>
@@ -372,7 +437,7 @@ const StoreOrderPage: React.FC = () => {
{
CONTAINER_EDITABLE_STATUS.includes(record.status ?? -1) ? (
<AuthButton key="container" auth="order.store.update">
<Button type={'primary'} onClick={() => openContainer(record)}>
<Button type={'link'} onClick={() => openContainer(record)}>
</Button>
</AuthButton>
@@ -429,7 +494,7 @@ const StoreOrderPage: React.FC = () => {
title="订单详情"
open={detailOpen}
onClose={() => setDetailOpen(false)}
width={720}
size={1000}
loading={detailLoading}
footer={
detail && NEXT_STATUS[detail.status ?? -1] ? (
@@ -452,9 +517,21 @@ const StoreOrderPage: React.FC = () => {
>
{detail ? (
<>
<Descriptions column={2} size="small" bordered>
{/* 门店信息 */}
<Descriptions title="门店信息" column={3} size="small" bordered>
<Descriptions.Item label="门店名称">
{detail.store?.name ?? `门店#${detail.store_id}`}
</Descriptions.Item>
<Descriptions.Item label="联系人">{detail.store?.contact ?? '-'}</Descriptions.Item>
<Descriptions.Item label="联系电话">{detail.store?.phone ?? '-'}</Descriptions.Item>
<Descriptions.Item label="门店地址" span={3}>
{detail.store?.address ?? '-'}
</Descriptions.Item>
</Descriptions>
{/* 订单信息 */}
<Descriptions title="订单信息" column={3} size="small" bordered className="mt-4!">
<Descriptions.Item label="订单号">{detail.order_no}</Descriptions.Item>
<Descriptions.Item label="门店">{detail.store?.name}</Descriptions.Item>
<Descriptions.Item label="订货日期">{detail.order_date}</Descriptions.Item>
<Descriptions.Item label="状态">
<Tag color={STORE_ORDER_STATUS_MAP[detail.status ?? 0]?.color}>
@@ -462,34 +539,128 @@ const StoreOrderPage: React.FC = () => {
</Tag>
</Descriptions.Item>
<Descriptions.Item label="订货总量">{detail.total_quantity}</Descriptions.Item>
<Descriptions.Item label="订单金额">¥{detail.total_amount}</Descriptions.Item>
<Descriptions.Item label="周转框数量">{detail.box_num}</Descriptions.Item>
<Descriptions.Item label="周转托盘数量">{detail.tray_num}</Descriptions.Item>
{detail.remark ? (
<Descriptions.Item label="备注" span={2}>
<Descriptions.Item label="备注" span={3}>
{detail.remark}
</Descriptions.Item>
) : null}
</Descriptions>
<Title level={5} className="!mt-6 !mb-3">
{/* 商品明细(商城模式:首图 + 单价 × 订货量 + 金额) */}
<Title level={5} className="mt-6! mb-3!">
</Title>
<Table<IStoreOrderItem>
rowKey="id"
size="small"
columns={itemColumns}
dataSource={detail.items ?? []}
pagination={false}
summary={() => (
<Table.Summary.Row>
<Table.Summary.Cell index={0} colSpan={4} align="right">
</Table.Summary.Cell>
<Table.Summary.Cell index={1} align="right">
<Text strong>¥{detail.total_amount}</Text>
</Table.Summary.Cell>
<Table.Summary.Cell index={2} />
</Table.Summary.Row>
)}
/>
<div className="overflow-hidden rounded border border-gray-200">
<div className="flex bg-gray-50 px-4 py-2 text-sm text-gray-500">
<div className="flex-1"></div>
<div className="w-30 shrink-0 text-center"></div>
<div className="w-30 shrink-0 text-center"></div>
<div className="w-26 shrink-0 text-center"></div>
<div className="w-33 shrink-0 text-center"></div>
{itemEditable ? <div className="w-40 shrink-0 text-center"></div> : null}
</div>
{(detail.items ?? []).map((item) => (
<div key={item.id} className="flex items-center border-t border-gray-100 px-4 py-3">
<div className="flex min-w-0 flex-1 items-center">
<Image.PreviewGroup>
{item.image ? (
<Image
src={item.image}
width={64}
height={64}
style={{ objectFit: 'cover', borderRadius: 4 }}
/>
) : (
<div className="flex h-16 w-16 shrink-0 items-center justify-center rounded bg-gray-100 text-xs text-gray-400">
</div>
)}
</Image.PreviewGroup>
<div className="ml-3 min-w-0">
<div className="text-sm font-medium">{item.product_name}</div>
<div className="mt-0.5 text-xs text-gray-500">
{item.product_spec || '-'} / {item.unit || '-'} · ¥
{item.cost_price ?? '0.00'}
{Number(item.weight ?? 0) > 0 ? ` · 重量:${item.weight}` : ''}
</div>
{item.remark ? (
<div className="mt-0.5 truncate text-xs text-gray-500">{item.remark}</div>
) : null}
</div>
</div>
<div className="w-30 shrink-0 text-center">{item.supplier?.name ?? '-'}</div>
<div className="w-30 shrink-0 text-center">¥{item.price}</div>
<div className="w-26 shrink-0 text-center">{item.quantity}</div>
<div className="w-33 shrink-0 text-center">
<Text strong>¥{item.amount}</Text>
</div>
{itemEditable ? (
<div className="w-40 shrink-0 text-center">
<Space size={0}>
<AuthButton auth="order.store.update">
<Button
type="link"
size="small"
icon={<EditOutlined />}
onClick={() => openItemEdit(item)}
>
</Button>
</AuthButton>
<AuthButton auth="order.store.update">
<Popconfirm
title="确认同步最新商品信息?"
description="将按商品ID同步供应商、品名、规格、单位、成本价,并按门店等级价重算单价"
onConfirm={() => handleItemSync(item)}
>
<Button type="link" size="small" icon={<SyncOutlined />}>
</Button>
</Popconfirm>
</AuthButton>
</Space>
</div>
) : null}
</div>
))}
{(detail.items ?? []).length === 0 ? (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description="暂无商品明细"
className="py-8!"
/>
) : null}
</div>
{/* 附加信息 */}
<div className="mt-3! flex justify-end">
<div className="w-full rounded bg-gray-50 p-4 text-sm">
<div className="flex justify-between py-1">
<Text type="secondary">
{detail.box_num} × ¥{detail.box_price ?? '0.00'}
</Text>
<span>¥{detail.box_amount ?? '0.00'}</span>
</div>
<div className="flex justify-between py-1">
<Text type="secondary">
{detail.tray_num} × ¥{detail.tray_price ?? '0.00'}
</Text>
<span>¥{detail.tray_amount ?? '0.00'}</span>
</div>
<div className="flex justify-between py-1">
<Text type="secondary"></Text>
<span>¥{detail.product_amount}</span>
</div>
<div className="mt-1! flex justify-between border-t border-gray-200 pt-2">
<Text strong></Text>
<Text strong type="danger">
¥{detail.total_amount}
</Text>
</div>
</div>
</div>
</>
) : null}
</Drawer>
@@ -593,6 +764,87 @@ const StoreOrderPage: React.FC = () => {
</div>
</Space>
</Modal>
{/* 编辑商品明细(保存后后端重算单品金额与订单总价) */}
<Modal
title="编辑商品明细"
open={itemEditOpen}
onCancel={() => setItemEditOpen(false)}
onOk={() => itemForm.submit()}
confirmLoading={itemSaving}
okText="保存"
width={640}
destroyOnHidden
>
<div className="py-2 text-gray-500">
{detail?.order_no}
</div>
<Form form={itemForm} layout="vertical" onFinish={handleItemSave}>
<div className="grid grid-cols-2 gap-x-4">
<Form.Item
label="供应商"
name="supplier_id"
rules={[{ required: true, message: '请选择供应商' }]}
>
<Select
showSearch={{
optionFilterProp: 'label'
}}
placeholder="请选择供应商"
options={suppliers.map((s) => ({ label: s.name, value: s.id }))}
/>
</Form.Item>
<Form.Item
label="品名"
name="product_name"
rules={[{ required: true, message: '请输入品名' }]}
>
<Input placeholder="请输入品名" maxLength={100} />
</Form.Item>
<Form.Item label="规格/包规" name="product_spec">
<Input placeholder="请输入规格/包规" maxLength={100} />
</Form.Item>
<Form.Item
label="计价单位"
name="unit"
rules={[{ required: true, message: '请输入计价单位' }]}
>
<Input placeholder="斤/件/箱等" maxLength={20} />
</Form.Item>
<Form.Item
label="单价(元)"
name="price"
rules={[{ required: true, message: '请输入单价' }]}
>
<InputNumber className="w-full" min={0} precision={2} placeholder="请输入单价" />
</Form.Item>
<Form.Item
label="成本价(元)"
name="cost_price"
rules={[{ required: true, message: '请输入成本价' }]}
>
<InputNumber className="w-full" min={0} precision={2} placeholder="请输入成本价" />
</Form.Item>
<Form.Item
label="订货量"
name="quantity"
rules={[{ required: true, message: '请输入订货量' }]}
>
<InputNumber className="w-full" min={1} precision={0} placeholder="请输入订货量" />
</Form.Item>
<Form.Item
label="重量"
name="weight"
rules={[{ required: true, message: '请输入重量' }]}
>
<InputNumber className="w-full" min={0} precision={3} placeholder="请输入重量" />
</Form.Item>
<Form.Item label="备注" name="remark">
<TextArea className="w-full" placeholder="请输入备注" />
</Form.Item>
</div>
</Form>
</Modal>
</>
);
};