添加市场

This commit is contained in:
liu
2026-08-20 15:39:39 +08:00
parent cd7007bc49
commit 22237a47e7
20 changed files with 134 additions and 239 deletions
File diff suppressed because one or more lines are too long
+4 -3
View File
@@ -20,7 +20,7 @@ use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class ProductExport implements FromCollection, WithStrictNullComparison, WithStyles class ProductExport implements FromCollection, WithStrictNullComparison, WithStyles
{ {
/** 列头(与 ProductImport 的解析顺序一一对应,改动需同步) */ /** 列头(与 ProductImport 的解析顺序一一对应,改动需同步) */
public const array HEADERS = ['品名', '分类', '供应商', '规格/包规', '单位', '成本价', '排序', '库存', '保质期', '状态', '备注']; public const array HEADERS = ['品名', '分类', '供应商', '市场', '规格/包规', '单位', '成本价', '排序', '库存', '保质期', '状态', '备注'];
private ?Collection $rows = null; private ?Collection $rows = null;
@@ -46,7 +46,7 @@ class ProductExport implements FromCollection, WithStrictNullComparison, WithSty
$rows = [self::HEADERS]; $rows = [self::HEADERS];
if ($this->template) { if ($this->template) {
$rows[] = ['西红柿', '蔬菜/茄果类', '示例供应商', '10斤/箱', '斤', 2.5, 0, 100, 3, '上架', '示例行,导入前请删除']; $rows[] = ['西红柿', '蔬菜/茄果类', '示例供应商', '新发地', '10斤/箱', '斤', 2.5, 0, 100, 3, '上架', '示例行,导入前请删除'];
return $this->rows = collect($rows); return $this->rows = collect($rows);
} }
@@ -65,6 +65,7 @@ class ProductExport implements FromCollection, WithStrictNullComparison, WithSty
$product->name, $product->name,
$categoryPaths[(int) $product->category_id] ?? '', $categoryPaths[(int) $product->category_id] ?? '',
$supplierNames[(int) $product->supplier_id] ?? '', $supplierNames[(int) $product->supplier_id] ?? '',
$product->market,
$product->spec, $product->spec,
$product->unit, $product->unit,
(float) $product->cost_price, (float) $product->cost_price,
@@ -87,7 +88,7 @@ class ProductExport implements FromCollection, WithStrictNullComparison, WithSty
$this->collection(); $this->collection();
$sheet->freezePane('A2'); $sheet->freezePane('A2');
$widths = [20, 16, 14, 14, 8, 10, 8, 8, 8, 8, 24]; $widths = [20, 16, 14, 12, 14, 8, 10, 8, 8, 8, 8, 24];
foreach ($widths as $index => $width) { foreach ($widths as $index => $width) {
$sheet->getColumnDimension(Coordinate::stringFromColumnIndex($index + 1))->setWidth($width); $sheet->getColumnDimension(Coordinate::stringFromColumnIndex($index + 1))->setWidth($width);
} }
+6 -4
View File
@@ -126,6 +126,7 @@ class PurchaseOrderExport implements FromCollection, WithStrictNullComparison, W
'category' => $categoryNames[(int) $productId] ?? '', 'category' => $categoryNames[(int) $productId] ?? '',
'product_name' => (string) ($snapshot->product_name ?? $product->name), 'product_name' => (string) ($snapshot->product_name ?? $product->name),
'supplier' => $supplierNames[$rowSupplierId] ?? '', 'supplier' => $supplierNames[$rowSupplierId] ?? '',
'market' => (string) ($product->market ?? ''),
'product_spec' => $spec, 'product_spec' => $spec,
'unit' => (string) ($snapshot->unit ?? $product->unit), 'unit' => (string) ($snapshot->unit ?? $product->unit),
'cost_price' => (float) ($snapshot->cost_price ?? $product->cost_price), 'cost_price' => (float) ($snapshot->cost_price ?? $product->cost_price),
@@ -172,7 +173,7 @@ class PurchaseOrderExport implements FromCollection, WithStrictNullComparison, W
// 列头 // 列头
$rows[] = array_merge( $rows[] = array_merge(
['序号', '分类', '品名', '供应商', '包规', '单位', '成本', '参考零售价', '数量', '实际称重', '金额'], ['序号', '分类', '品名', '供应商', '市场', '包规', '单位', '成本', '参考零售价', '数量', '实际称重', '金额'],
array_values($this->storeNames), array_values($this->storeNames),
); );
$this->specialRows[++$rowIndex] = 'header'; $this->specialRows[++$rowIndex] = 'header';
@@ -189,6 +190,7 @@ class PurchaseOrderExport implements FromCollection, WithStrictNullComparison, W
$item['category'], $item['category'],
$item['product_name'], $item['product_name'],
$item['supplier'], $item['supplier'],
$item['market'],
$item['product_spec'], $item['product_spec'],
$item['unit'], $item['unit'],
$item['cost_price'], $item['cost_price'],
@@ -208,7 +210,7 @@ class PurchaseOrderExport implements FromCollection, WithStrictNullComparison, W
// 合计行(行合计 + 门店列合计) // 合计行(行合计 + 门店列合计)
$rows[] = array_merge( $rows[] = array_merge(
['', '', '合计', '', '', '', '', '', (float) $totalQuantity, (float) $totalWeight, (float) $totalAmount], ['', '', '合计', '', '', '', '', '', '', (float) $totalQuantity, (float) $totalWeight, (float) $totalAmount],
array_values($storeTotals), array_values($storeTotals),
); );
$this->specialRows[++$rowIndex] = 'summary'; $this->specialRows[++$rowIndex] = 'summary';
@@ -225,7 +227,7 @@ class PurchaseOrderExport implements FromCollection, WithStrictNullComparison, W
$this->collection(); $this->collection();
$sheet->freezePane('A' . ($this->headerRow + 1)); $sheet->freezePane('A' . ($this->headerRow + 1));
$widths = [6, 10, 20, 12, 12, 8, 10, 12, 10, 12, 12]; $widths = [6, 10, 20, 12, 12, 12, 8, 10, 12, 10, 12, 12];
foreach ($widths as $index => $width) { foreach ($widths as $index => $width) {
$sheet->getColumnDimension(Coordinate::stringFromColumnIndex($index + 1))->setWidth($width); $sheet->getColumnDimension(Coordinate::stringFromColumnIndex($index + 1))->setWidth($width);
} }
@@ -233,7 +235,7 @@ class PurchaseOrderExport implements FromCollection, WithStrictNullComparison, W
// 门店列整列(列头 → 合计行)填充突出颜色 // 门店列整列(列头 → 合计行)填充突出颜色
$storeCount = count($this->storeNames); $storeCount = count($this->storeNames);
for ($i = 0; $i < $storeCount; $i++) { for ($i = 0; $i < $storeCount; $i++) {
$column = Coordinate::stringFromColumnIndex(12 + $i); $column = Coordinate::stringFromColumnIndex(13 + $i);
$sheet->getColumnDimension($column)->setWidth(12); $sheet->getColumnDimension($column)->setWidth(12);
$sheet->getStyle($column . $this->headerRow . ':' . $column . $this->lastRow) $sheet->getStyle($column . $this->headerRow . ':' . $column . $this->lastRow)
->getFill() ->getFill()
+4 -3
View File
@@ -55,7 +55,7 @@ class PurchaseStoreSheet implements FromCollection, WithStrictNullComparison, Wi
$rowIndex++; $rowIndex++;
// 列头 // 列头
$rows[] = ['序号', '品名', '包规', '单位', '单价', '数量', '重量(斤)', '预计金额']; $rows[] = ['序号', '品名', '市场', '包规', '单位', '单价', '数量', '重量(斤)', '预计金额'];
$this->specialRows[++$rowIndex] = 'header'; $this->specialRows[++$rowIndex] = 'header';
$this->headerRow = $rowIndex; $this->headerRow = $rowIndex;
@@ -67,6 +67,7 @@ class PurchaseStoreSheet implements FromCollection, WithStrictNullComparison, Wi
$rows[] = [ $rows[] = [
$sort + 1, $sort + 1,
$item['product_name'], $item['product_name'],
$item['market'],
$item['product_spec'], $item['product_spec'],
$item['unit'], $item['unit'],
(float) $item['price'], (float) $item['price'],
@@ -81,7 +82,7 @@ class PurchaseStoreSheet implements FromCollection, WithStrictNullComparison, Wi
} }
// 合计行 // 合计行
$rows[] = ['', '合计', '', '', '', $totalQuantity, (float) $totalWeight, (float) $totalAmount]; $rows[] = ['', '合计', '', '', '', '', $totalQuantity, (float) $totalWeight, (float) $totalAmount];
$this->specialRows[++$rowIndex] = 'summary'; $this->specialRows[++$rowIndex] = 'summary';
return $this->rows = collect($rows); return $this->rows = collect($rows);
@@ -100,7 +101,7 @@ class PurchaseStoreSheet implements FromCollection, WithStrictNullComparison, Wi
$this->collection(); $this->collection();
$sheet->freezePane('A' . ($this->headerRow + 1)); $sheet->freezePane('A' . ($this->headerRow + 1));
$widths = [6, 24, 14, 8, 10, 10, 12, 12]; $widths = [6, 24, 12, 14, 8, 10, 10, 12, 12];
foreach ($widths as $index => $width) { foreach ($widths as $index => $width) {
$sheet->getColumnDimension(\PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($index + 1)) $sheet->getColumnDimension(\PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($index + 1))
->setWidth($width); ->setWidth($width);
+4 -3
View File
@@ -56,7 +56,7 @@ class PurchaseSupplierSheet implements FromCollection, WithStrictNullComparison,
$rowIndex++; $rowIndex++;
// 列头 // 列头
$rows[] = ['序号', '品名', '包规', '单位', '成本价', '数量', '重量(斤)', '金额']; $rows[] = ['序号', '品名', '市场', '包规', '单位', '成本价', '数量', '重量(斤)', '金额'];
$this->specialRows[++$rowIndex] = 'header'; $this->specialRows[++$rowIndex] = 'header';
$this->headerRow = $rowIndex; $this->headerRow = $rowIndex;
@@ -68,6 +68,7 @@ class PurchaseSupplierSheet implements FromCollection, WithStrictNullComparison,
$rows[] = [ $rows[] = [
$sort + 1, $sort + 1,
$item['product_name'], $item['product_name'],
$item['market'],
$item['product_spec'], $item['product_spec'],
$item['unit'], $item['unit'],
(float) $item['cost_price'], (float) $item['cost_price'],
@@ -82,7 +83,7 @@ class PurchaseSupplierSheet implements FromCollection, WithStrictNullComparison,
} }
// 合计行 // 合计行
$rows[] = ['', '合计', '', '', '', $totalQuantity, (float) $totalWeight, (float) $totalAmount]; $rows[] = ['', '合计', '', '', '', '', $totalQuantity, (float) $totalWeight, (float) $totalAmount];
$this->specialRows[++$rowIndex] = 'summary'; $this->specialRows[++$rowIndex] = 'summary';
return $this->rows = collect($rows); return $this->rows = collect($rows);
@@ -101,7 +102,7 @@ class PurchaseSupplierSheet implements FromCollection, WithStrictNullComparison,
$this->collection(); $this->collection();
$sheet->freezePane('A' . ($this->headerRow + 1)); $sheet->freezePane('A' . ($this->headerRow + 1));
$widths = [6, 24, 14, 8, 10, 10, 12, 12]; $widths = [6, 24, 12, 14, 8, 10, 10, 12, 12];
foreach ($widths as $index => $width) { foreach ($widths as $index => $width) {
$sheet->getColumnDimension(Coordinate::stringFromColumnIndex($index + 1))->setWidth($width); $sheet->getColumnDimension(Coordinate::stringFromColumnIndex($index + 1))->setWidth($width);
} }
@@ -35,6 +35,7 @@ class ProductController extends BaseController
'name' => 'like', 'name' => 'like',
'category_id' => '=', 'category_id' => '=',
'supplier_id' => '=', 'supplier_id' => '=',
'market' => 'like',
'status' => '=', 'status' => '=',
]; ];
@@ -134,6 +134,7 @@ class PurchaseOrderController extends BaseController
'unit' => $first->unit, // 单位 'unit' => $first->unit, // 单位
'supplier_id' => (int) $first->supplier_id, // 供应商id 'supplier_id' => (int) $first->supplier_id, // 供应商id
'supplier' => $supplier, // 供应商 'supplier' => $supplier, // 供应商
'market' => (string) ($product?->market ?? ''), // 市场(取商品档案)
'cost_price' => $first->cost_price, // 成本价 'cost_price' => $first->cost_price, // 成本价
'quantity' => $quantity, 'quantity' => $quantity,
'weight' => (float) $weight, 'weight' => (float) $weight,
@@ -22,6 +22,7 @@ class ProductFormRequest extends BaseFormRequest
return [ return [
'category_id' => ['required','integer', new Exists(ProductCategoryModel::class,'id')], 'category_id' => ['required','integer', new Exists(ProductCategoryModel::class,'id')],
'supplier_id' => ['nullable','integer', 'exclude_if:supplier_id,0', new Exists(SupplierModel::class,'id')], 'supplier_id' => ['nullable','integer', 'exclude_if:supplier_id,0', new Exists(SupplierModel::class,'id')],
'market' => 'nullable|string|max:50',
'name' => 'required|string|max:100', 'name' => 'required|string|max:100',
'spec' => 'nullable|string|max:100', 'spec' => 'nullable|string|max:100',
'unit' => 'nullable|string|max:20', 'unit' => 'nullable|string|max:20',
+8 -2
View File
@@ -92,8 +92,8 @@ class ProductImport implements ToCollection
*/ */
private function parseRow(array $cells, Collection $categories, array $leafIds): array private function parseRow(array $cells, Collection $categories, array $leafIds): array
{ {
[$name, $categoryName, $supplierName, $spec, $unit, $costPrice, $sort, $stock, $shelfLife, $statusText, $remark] = [$name, $categoryName, $supplierName, $market, $spec, $unit, $costPrice, $sort, $stock, $shelfLife, $statusText, $remark] =
array_pad(array_slice($cells, 0, 11), 11, null); array_pad(array_slice($cells, 0, 12), 12, null);
$errors = []; $errors = [];
@@ -120,6 +120,11 @@ class ProductImport implements ToCollection
$errors[] = '供应商名称最长 100 个字符'; $errors[] = '供应商名称最长 100 个字符';
} }
$market = (string) $market;
if (mb_strlen($market) > 50) {
$errors[] = '市场最长 50 个字符';
}
$spec = (string) $spec; $spec = (string) $spec;
if (mb_strlen($spec) > 100) { if (mb_strlen($spec) > 100) {
$errors[] = '规格/包规最长 100 个字符'; $errors[] = '规格/包规最长 100 个字符';
@@ -169,6 +174,7 @@ class ProductImport implements ToCollection
'category_id' => $categoryId, 'category_id' => $categoryId,
'supplier_id' => 0, // 写入阶段按 supplier_name 解析(匹配或自动创建) 'supplier_id' => 0, // 写入阶段按 supplier_name 解析(匹配或自动创建)
'supplier_name' => $supplierName, // 写入阶段解析,不入 product 表 'supplier_name' => $supplierName, // 写入阶段解析,不入 product 表
'market' => $market,
'name' => $name, 'name' => $name,
'spec' => $spec, 'spec' => $spec,
'unit' => $unit, 'unit' => $unit,
+1
View File
@@ -27,6 +27,7 @@ class ProductModel extends Model
protected $fillable = [ protected $fillable = [
'category_id', 'category_id',
'supplier_id', 'supplier_id',
'market',
'name', 'name',
'spec', 'spec',
'unit', 'unit',
+5 -2
View File
@@ -80,6 +80,7 @@ class PurchaseItemService
'product_name' => $first->product_name, 'product_name' => $first->product_name,
'product_spec' => $first->product_spec, 'product_spec' => $first->product_spec,
'unit' => $first->unit, 'unit' => $first->unit,
'market' => (string) data_get($first, 'market', ''),
// 加权平均单价(保证 单价×数量=预计金额) // 加权平均单价(保证 单价×数量=预计金额)
'price' => $quantity > 0 'price' => $quantity > 0
? bcdiv($amount, (string) $quantity, 2) ? bcdiv($amount, (string) $quantity, 2)
@@ -124,6 +125,7 @@ class PurchaseItemService
'product_name' => $first->product_name, 'product_name' => $first->product_name,
'product_spec' => $first->product_spec, 'product_spec' => $first->product_spec,
'unit' => $first->unit, 'unit' => $first->unit,
'market' => (string) data_get($first, 'market', ''),
'cost_price' => (string) $first->cost_price, 'cost_price' => (string) $first->cost_price,
'quantity' => $quantity, 'quantity' => $quantity,
'weight' => $weight, 'weight' => $weight,
@@ -151,17 +153,18 @@ class PurchaseItemService
->get() ->get()
->makeVisible('cost_price'); ->makeVisible('cost_price');
// 排序键:商品分类 sort → 商品 sort(商品含软删除,保证历史单据可导出) // 排序键:商品分类 sort → 商品 sort;市场取商品档案(商品含软删除,保证历史单据可导出)
$products = ProductModel::withTrashed() $products = ProductModel::withTrashed()
->with('category:id,sort') ->with('category:id,sort')
->whereIn('id', $items->pluck('product_id')->unique()) ->whereIn('id', $items->pluck('product_id')->unique())
->get(['id', 'category_id', 'sort']) ->get(['id', 'category_id', 'sort', 'market'])
->keyBy('id'); ->keyBy('id');
foreach ($items as $item) { foreach ($items as $item) {
$product = $products->get((int) $item->product_id); $product = $products->get((int) $item->product_id);
$item->setAttribute('category_sort', (int) ($product?->category?->sort ?? 9999)); $item->setAttribute('category_sort', (int) ($product?->category?->sort ?? 9999));
$item->setAttribute('product_sort', (int) ($product?->sort ?? 9999)); $item->setAttribute('product_sort', (int) ($product?->sort ?? 9999));
$item->setAttribute('market', (string) ($product?->market ?? ''));
} }
return $items; return $items;
@@ -27,6 +27,7 @@ class ProductModelFactory extends Factory
return [ return [
'category_id' => 0, 'category_id' => 0,
'supplier_id' => 0, 'supplier_id' => 0,
'market' => '',
'name' => self::NAMES[$seq % count(self::NAMES)] . $seq, 'name' => self::NAMES[$seq % count(self::NAMES)] . $seq,
'spec' => self::SPECS[$seq % count(self::SPECS)], 'spec' => self::SPECS[$seq % count(self::SPECS)],
'unit' => self::UNITS[$seq % count(self::UNITS)], 'unit' => self::UNITS[$seq % count(self::UNITS)],
@@ -32,6 +32,7 @@ return new class extends Migration
$table->increments('id')->comment('商品ID'); $table->increments('id')->comment('商品ID');
$table->integer('category_id')->default(0)->comment('商品分类ID'); $table->integer('category_id')->default(0)->comment('商品分类ID');
$table->integer('supplier_id')->default(0)->comment('默认供应商ID'); $table->integer('supplier_id')->default(0)->comment('默认供应商ID');
$table->string('market', 50)->default('')->comment('市场(如:新发地、岳各庄)');
$table->string('name', 100)->comment('品名'); $table->string('name', 100)->comment('品名');
$table->string('spec', 100)->default('')->comment('规格/包规'); $table->string('spec', 100)->default('')->comment('规格/包规');
$table->string('unit', 20)->default('斤')->comment('计价单位(斤/件/箱等)'); $table->string('unit', 20)->default('斤')->comment('计价单位(斤/件/箱等)');
-172
View File
@@ -1,172 +0,0 @@
# 小程序端修改文档(门店端)
> 版本:2026-08-17
> 后端变更:用户表与门店表合并(一个用户就是一个门店),门店登录由「微信静默登录」改为「账号 + 密码登录」。
> 本文档面向小程序(门店端)开发者,说明需要配合修改的内容。**未列出的接口均无变化**。
---
## 一、变更总览
| 事项 | 旧 | 新 |
|------|-----|-----|
| 登录方式 | `wx.login` 拿 code,调 `/mini/auth/login` 静默登录 | 账号 + 密码登录(账号由商家后台分配) |
| 注册 | `POST /mini/auth/register`(微信 code + 手机号 + 门店编码) | **接口下线**,门店账号由商家在 PC 后台创建 |
| 登录主体 | 微信用户(user),再绑定门店(store) | **门店即用户**:登录成功返回的就是门店本身 |
| 修改密码 | 无 | 新增 `PUT /mini/auth/password` |
| Token 鉴权 | Bearer Token | **不变** |
| 其他业务接口 | 购物车 / 订单 / 账单 / 支付 / 通知 / 商品 / 首页 | **全部不变**(路径、入参、出参) |
### 需要小程序端配合的改动
1. **重做登录页**:去掉 `wx.login` 流程,改为账号 + 密码表单(账号密码由商家线下告知门店)。
2. **删除注册页/绑定门店页**:注册接口已下线,「尚未绑定门店」的场景已不存在。
3. **用户信息结构调整**:登录与 `auth/info` 返回的对象字段变化(见下文)。
4. **新增「修改密码」入口**(建议放在「我的」页面)。
5. **重新登录**:旧 token 全部失效,需引导用户用账号密码重新登录一次。
---
## 二、登录接口(变更)
### `POST /mini/auth/login`
**请求参数(变更):**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| username | string | 是 | 登录账号(商家后台分配,4~20 位) |
| password | string | 是 | 登录密码 |
> 旧参数 `code`(wx.login 凭证)已废弃,无需再传,也无需调用 `wx.login`。
**成功响应:**
```json
{
"success": true,
"msg": "登录成功",
"data": {
"token": "1|xxxxxxxxxxxxxxxx",
"user": {
"id": 3,
"name": "好又多超市",
"code": "S000003",
"username": "hyd001",
"avatar": "",
"level_id": 1,
"level": { "id": 1, "name": "一级客户" },
"contact": "张三",
"phone": "13800000000",
"address": "xx路 1 号",
"payment_cycle_days": 2,
"status": 1,
"last_login_at": "2026-08-17 10:00:00",
"created_at": "2026-08-01 09:00:00"
}
}
}
```
**user 字段变化要点:**
- `user` 现在**就是门店对象**(扁平结构),不再有 `user.store` 嵌套,也不再有 `openid``unionid``nickname``email``store_id` 字段。
- 门店名称取 `user.name`(原 `user.store.name`);客户等级取 `user.level`
- `password` 字段永不返回。
**失败响应(`success: false``msg` 提示):**
| 场景 | msg |
|------|-----|
| 账号不存在或密码错误 | 账号或密码错误 |
| 门店已停用 | 账号已被停用,请联系客服处理 |
| 参数缺失 | 请输入登录账号 / 请输入登录密码 |
**Token 使用(不变):** 后续所有请求携带请求头 `Authorization: Bearer {token}`
---
## 三、注册接口(下线)
### ~~`POST /mini/auth/register`~~ —— 已删除
- 门店账号全部由商家在 PC 后台「客户管理 → 门店管理」创建并分配(含登录账号、初始密码)。
- 小程序端请移除注册页、「输入门店编码绑定」页及相关逻辑。
- 同步移除 `wx.getPhoneNumber` 手机号授权流程(后端已不再使用)。
---
## 四、当前用户信息(结构变化)
### `GET /mini/auth/info`(需登录)
返回当前登录门店完整信息(含 `level` 等级嵌套),字段与登录接口的 `user` 一致(见上文示例)。原来读取 `info.store.xxx` 的地方改为直接读 `info.xxx`
---
## 五、修改密码(新增)
### `PUT /mini/auth/password`(需登录)
**请求参数:**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| oldPassword | string | 是 | 原密码 |
| newPassword | string | 是 | 新密码(6~20 位) |
| rePassword | string | 是 | 确认新密码(须与 newPassword 一致) |
**失败场景:** 原密码不正确 / 两次输入的密码不一致 / 新密码至少 6 位。
> 修改密码成功后现有 token 仍然有效,无需强制重新登录。
---
## 六、门店信息(不变)
### `GET /mini/store/info`(需登录)
返回:`{ id, name, code, contact, phone, address, payment_cycle_days }`(门店名称/编码/回款周期只读,由后台维护)。
### `PUT /mini/store/info`(需登录)
可改字段不变:`contact`(联系人)、`phone`(联系电话)、`address`(地址)。
---
## 七、以下接口完全不变
路径、入参、出参均无变化,仅内部归属从「用户」变为「门店」(对小程序透明):
| 模块 | 接口 |
|------|------|
| 首页 | `GET /mini/home/*`(轮播/导航/促销) |
| 商品 | `GET /mini/product/categories``GET /mini/product/list``GET /mini/product/{id}`(登录后按本店等级显示价格) |
| 购物车 | `POST /mini/cart``GET /mini/cart``PUT /mini/cart/{id}``DELETE /mini/cart/{id}``DELETE /mini/cart` |
| 订单 | `POST /mini/order``GET /mini/order``GET /mini/order/summary``GET /mini/order/{id}``PUT /mini/order/{id}/cancel` |
| 账单 | `GET /mini/bill``GET /mini/bill/{id}``GET /mini/bill/export` |
| 支付 | `GET /mini/payment/config``GET /mini/payment``POST /mini/payment``GET /mini/payment/{id}` |
| 通知 | `GET /mini/notice``PUT /mini/notice/{id}/read` |
| 上传 | `POST /mini/upload` |
> 唯一语义差异:数据隔离现在天然按门店划分(一个门店一个账号),原「同一门店多个微信账号各自购物车」的合并场景不再存在。
---
## 八、错误语义(不变)
- 未携带/无效 tokenHTTP `401`
- 业务错误:HTTP `200` + `{ success: false, msg: "..." }`,直接 toast `msg` 即可。
- 「门店未设置客户等级,无法加购/下单,请联系客服」等提示文案不变。
- 「尚未绑定门店」提示已移除(该场景不存在)。
---
## 九、上线 Checklist(小程序端)
- [ ] 登录页改为账号 + 密码表单,移除 `wx.login` / `wx.getPhoneNumber` 调用
- [ ] 移除注册页、门店绑定页及路由
- [ ] 全局用户信息读取点从 `user.store.*` 调整为 `user.*`(门店名、等级、回款周期等)
- [ ] 「我的」页面新增修改密码入口
- [ ] 旧版本缓存的 token 失效处理:401 时引导重新登录
- [ ] 等级价格展示逻辑不变(接口返回的 `price` 字段含义不变)
+20 -17
View File
@@ -108,12 +108,14 @@ class ExportTest extends ProcurementTestCase
'cost_price' => '5.00', 'cost_price' => '5.00',
'spec' => '10斤/箱', 'spec' => '10斤/箱',
'supplier_id' => $supplierA->id, 'supplier_id' => $supplierA->id,
'market' => '新发地',
]); ]);
$meat = ProductModel::factory()->create([ $meat = ProductModel::factory()->create([
'status' => ProductModel::STATUS_ON, 'status' => ProductModel::STATUS_ON,
'cost_price' => '20.00', 'cost_price' => '20.00',
'spec' => '20斤/箱', 'spec' => '20斤/箱',
'supplier_id' => $supplierB->id, 'supplier_id' => $supplierB->id,
'market' => '岳各庄',
]); ]);
$this->actingAsMiniStore($storeA); $this->actingAsMiniStore($storeA);
@@ -154,24 +156,25 @@ class ExportTest extends ProcurementTestCase
if ($rows->count() !== 8) { if ($rows->count() !== 8) {
return false; return false;
} }
// 列头含参考零售价与门店列 // 列头含市场/参考零售价与门店列(市场列在供应商后,门店列从索引 12 起)
$header = $rows[3]; $header = $rows[3];
if ($header[7] !== '参考零售价' || $header[11] !== $storeA->name || $header[12] !== $storeB->name) { if ($header[4] !== '市场' || $header[8] !== '参考零售价' || $header[12] !== $storeA->name || $header[13] !== $storeB->name) {
return false; return false;
} }
// 蔬菜行:数量 2+3=5、金额 25、参考零售价 5÷10=0.50、门店列 2/3 // 蔬菜行:市场 新发地、数量 2+3=5、金额 25、参考零售价 5÷10=0.50、门店列 2/3
$vegRow = $rows->firstWhere(2, $veg->name); $vegRow = $rows->firstWhere(2, $veg->name);
if ($vegRow === null if ($vegRow === null
|| (float) $vegRow[8] !== 5.0 || (float) $vegRow[10] !== 25.0 || $vegRow[4] !== '新发地'
|| (float) $vegRow[7] !== 0.5 || (float) $vegRow[9] !== 5.0 || (float) $vegRow[11] !== 25.0
|| (float) $vegRow[11] !== 2.0 || (float) $vegRow[12] !== 3.0) { || (float) $vegRow[8] !== 0.5
|| (float) $vegRow[12] !== 2.0 || (float) $vegRow[13] !== 3.0) {
return false; return false;
} }
// 无订货商品行:数量 0、参考零售价留空、门店列 0 // 无订货商品行:数量 0、参考零售价留空、门店列 0
$extraRow = $rows->firstWhere(2, $extra->name); $extraRow = $rows->firstWhere(2, $extra->name);
if ($extraRow === null if ($extraRow === null
|| (float) $extraRow[8] !== 0.0 || $extraRow[7] !== '' || (float) $extraRow[9] !== 0.0 || $extraRow[8] !== ''
|| (float) $extraRow[11] !== 0.0) { || (float) $extraRow[12] !== 0.0) {
return false; return false;
} }
// 合计行:总数量 6、总金额 45、门店列合计 3/3 // 合计行:总数量 6、总金额 45、门店列合计 3/3
@@ -179,8 +182,8 @@ class ExportTest extends ProcurementTestCase
if ($total[2] !== '合计') { if ($total[2] !== '合计') {
return false; return false;
} }
return (float) $total[8] === 6.0 && (float) $total[10] === 45.0 return (float) $total[9] === 6.0 && (float) $total[11] === 45.0
&& (float) $total[11] === 3.0 && (float) $total[12] === 3.0; && (float) $total[12] === 3.0 && (float) $total[13] === 3.0;
} }
); );
} }
@@ -234,12 +237,12 @@ class ExportTest extends ProcurementTestCase
if ($rows->count() !== 6) { if ($rows->count() !== 6) {
return false; return false;
} }
if ($rows[2] !== ['序号', '品名', '包规', '单位', '单价', '数量', '重量(斤)', '预计金额']) { if ($rows[2] !== ['序号', '品名', '市场', '包规', '单位', '单价', '数量', '重量(斤)', '预计金额']) {
return false; return false;
} }
// 合计:数量 2+1=3,金额 10+20=30 // 合计:数量 2+1=3,金额 10+20=30
$total = $rows->last(); $total = $rows->last();
return $total[1] === '合计' && (int) $total[5] === 3 && (float) $total[7] === 30.0; return $total[1] === '合计' && (int) $total[6] === 3 && (float) $total[8] === 30.0;
} }
); );
} }
@@ -294,23 +297,23 @@ class ExportTest extends ProcurementTestCase
if ($titles !== [$supplierA->name, $supplierB->name]) { if ($titles !== [$supplierA->name, $supplierB->name]) {
return false; return false;
} }
// 供应商甲:蔬菜 2+3=5 件、金额 5×5=25 // 供应商甲:蔬菜 2+3=5 件、金额 5×5=25(市场列在品名后)
$rowsA = $sheets[0]->collection()->values(); $rowsA = $sheets[0]->collection()->values();
if ($rowsA[2] !== ['序号', '品名', '包规', '单位', '成本价', '数量', '重量(斤)', '金额']) { if ($rowsA[2] !== ['序号', '品名', '市场', '包规', '单位', '成本价', '数量', '重量(斤)', '金额']) {
return false; return false;
} }
$vegRow = $rowsA->firstWhere(1, $veg->name); $vegRow = $rowsA->firstWhere(1, $veg->name);
if ($vegRow === null || (int) $vegRow[5] !== 5 || (float) $vegRow[7] !== 25.0) { if ($vegRow === null || $vegRow[2] !== '新发地' || (int) $vegRow[6] !== 5 || (float) $vegRow[8] !== 25.0) {
return false; return false;
} }
$totalA = $rowsA->last(); $totalA = $rowsA->last();
if ($totalA[1] !== '合计' || (int) $totalA[5] !== 5 || (float) $totalA[7] !== 25.0) { if ($totalA[1] !== '合计' || (int) $totalA[6] !== 5 || (float) $totalA[8] !== 25.0) {
return false; return false;
} }
// 供应商乙:肉 1 件、金额 20 // 供应商乙:肉 1 件、金额 20
$rowsB = $sheets[1]->collection()->values(); $rowsB = $sheets[1]->collection()->values();
$meatRow = $rowsB->firstWhere(1, $meat->name); $meatRow = $rowsB->firstWhere(1, $meat->name);
return $meatRow !== null && (int) $meatRow[5] === 1 && (float) $meatRow[7] === 20.0; return $meatRow !== null && $meatRow[2] === '岳各庄' && (int) $meatRow[6] === 1 && (float) $meatRow[8] === 20.0;
} }
); );
} }
+36 -16
View File
@@ -62,6 +62,7 @@ class ProductImportExportTest extends ProcurementTestCase
ProductModel::factory()->create([ ProductModel::factory()->create([
'category_id' => $leaf->id, 'category_id' => $leaf->id,
'supplier_id' => $supplier->id, 'supplier_id' => $supplier->id,
'market' => '新发地',
'name' => '上海青', 'name' => '上海青',
'spec' => '10斤/箱', 'spec' => '10斤/箱',
'unit' => '斤', 'unit' => '斤',
@@ -93,19 +94,19 @@ class ProductImportExportTest extends ProcurementTestCase
if ($row === null) { if ($row === null) {
return false; return false;
} }
// [品名, 分类路径, 供应商, 规格, 单位, 成本价, 排序, 库存, 保质期, 状态, 备注] // [品名, 分类路径, 供应商, 市场, 规格, 单位, 成本价, 排序, 库存, 保质期, 状态, 备注]
if ($row[1] !== '蔬菜/叶菜类' || $row[2] !== '张三蔬菜批发' || $row[9] !== '上架') { if ($row[1] !== '蔬菜/叶菜类' || $row[2] !== '张三蔬菜批发' || $row[3] !== '新发地' || $row[10] !== '上架') {
return false; return false;
} }
if ((float) $row[5] !== 2.5 || (int) $row[6] !== 3 || (int) $row[7] !== 100 || (int) $row[8] !== 2) { if ((float) $row[6] !== 2.5 || (int) $row[7] !== 3 || (int) $row[8] !== 100 || (int) $row[9] !== 2) {
return false; return false;
} }
if ($row[10] !== '新鲜直达') { if ($row[11] !== '新鲜直达') {
return false; return false;
} }
$offRow = $rows->firstWhere(0, '下架商品'); $offRow = $rows->firstWhere(0, '下架商品');
return $offRow !== null && $offRow[9] === '下架' && $offRow[2] === ''; return $offRow !== null && $offRow[10] === '下架' && $offRow[2] === '' && $offRow[3] === '';
} }
); );
} }
@@ -164,6 +165,22 @@ class ProductImportExportTest extends ProcurementTestCase
$this->assertFalse($response->json('success'), '缺少导出权限点应被拦截'); $this->assertFalse($response->json('success'), '缺少导出权限点应被拦截');
} }
/** 列表按市场模糊筛选 */
public function test_list_filters_by_market(): void
{
ProductModel::factory()->create(['name' => '大白菜X', 'market' => '新发地']);
ProductModel::factory()->create(['name' => '土豆X', 'market' => '岳各庄']);
ProductModel::factory()->create(['name' => '无市场商品X', 'market' => '']);
$this->actingAsSysUser();
$response = $this->getJson('/product/goods?market=新发');
$response->assertJsonPath('success', true);
$names = collect($response->json('data.data'))->pluck('name');
$this->assertContains('大白菜X', $names);
$this->assertNotContains('土豆X', $names);
$this->assertNotContains('无市场商品X', $names);
}
// ==================== 导入 ==================== // ==================== 导入 ====================
/** 正常导入:新增商品 + 供应商自动创建 + 默认值(单位斤/状态上架) */ /** 正常导入:新增商品 + 供应商自动创建 + 默认值(单位斤/状态上架) */
@@ -174,8 +191,8 @@ class ProductImportExportTest extends ProcurementTestCase
$this->actingAsSysUser(); $this->actingAsSysUser();
$file = $this->makeUploadFile([ $file = $this->makeUploadFile([
ProductExport::HEADERS, ProductExport::HEADERS,
['上海青', '叶菜类', '新供应商A', '10斤/箱', '', 2.5, 3, 100, 2, '下架', '备注信息'], ['上海青', '叶菜类', '新供应商A', '新发地', '10斤/箱', '', 2.5, 3, 100, 2, '下架', '备注信息'],
['西红柿', '蔬菜/茄果类', '', '散装', '箱', 1.8, '', '', '', '', ''], ['西红柿', '蔬菜/茄果类', '', '', '散装', '箱', 1.8, '', '', '', '', ''],
]); ]);
// 第二行用到了不存在的「茄果类」路径 → 先补建该分类 // 第二行用到了不存在的「茄果类」路径 → 先补建该分类
ProductCategoryModel::create(['name' => '茄果类', 'parent_id' => $leaf->parent_id, 'sort' => 1, 'status' => 1]); ProductCategoryModel::create(['name' => '茄果类', 'parent_id' => $leaf->parent_id, 'sort' => 1, 'status' => 1]);
@@ -187,6 +204,7 @@ class ProductImportExportTest extends ProcurementTestCase
$productA = ProductModel::where('name', '上海青')->first(); $productA = ProductModel::where('name', '上海青')->first();
$this->assertNotNull($productA); $this->assertNotNull($productA);
$this->assertSame($leaf->id, $productA->category_id); $this->assertSame($leaf->id, $productA->category_id);
$this->assertSame('新发地', $productA->market);
$this->assertSame('斤', $productA->unit, '单位留空应默认斤'); $this->assertSame('斤', $productA->unit, '单位留空应默认斤');
$this->assertSame(ProductModel::STATUS_OFF, $productA->status); $this->assertSame(ProductModel::STATUS_OFF, $productA->status);
$this->assertSame('2.50', (string) $productA->cost_price); $this->assertSame('2.50', (string) $productA->cost_price);
@@ -197,6 +215,7 @@ class ProductImportExportTest extends ProcurementTestCase
$productB = ProductModel::where('name', '西红柿')->first(); $productB = ProductModel::where('name', '西红柿')->first();
$this->assertNotNull($productB); $this->assertNotNull($productB);
$this->assertSame('', $productB->market, '市场留空应为空字符串');
$this->assertSame(0, $productB->supplier_id, '供应商留空应为 0(不创建)'); $this->assertSame(0, $productB->supplier_id, '供应商留空应为 0(不创建)');
$this->assertSame(ProductModel::STATUS_ON, $productB->status, '状态留空应默认上架'); $this->assertSame(ProductModel::STATUS_ON, $productB->status, '状态留空应默认上架');
$this->assertSame(0, $productB->sort); $this->assertSame(0, $productB->sort);
@@ -211,7 +230,7 @@ class ProductImportExportTest extends ProcurementTestCase
$this->actingAsSysUser(); $this->actingAsSysUser();
$file = $this->makeUploadFile([ $file = $this->makeUploadFile([
ProductExport::HEADERS, ProductExport::HEADERS,
['黄瓜', '叶菜类', '老供应商', '散装', '斤', 1.2, 0, 0, 0, '上架', ''], ['黄瓜', '叶菜类', '老供应商', '', '散装', '斤', 1.2, 0, 0, 0, '上架', ''],
]); ]);
$this->post('/product/goods/import', ['file' => $file])->assertJsonPath('success', true); $this->post('/product/goods/import', ['file' => $file])->assertJsonPath('success', true);
@@ -227,10 +246,10 @@ class ProductImportExportTest extends ProcurementTestCase
$this->actingAsSysUser(); $this->actingAsSysUser();
$file = $this->makeUploadFile([ $file = $this->makeUploadFile([
ProductExport::HEADERS, ProductExport::HEADERS,
['正常商品', '叶菜类', '供应商X', '散装', '斤', 1.0, 0, 0, 0, '上架', ''], ['正常商品', '叶菜类', '供应商X', '', '散装', '斤', 1.0, 0, 0, 0, '上架', ''],
['', '叶菜类', '', '散装', '斤', 1.0, 0, 0, 0, '上架', ''], ['', '叶菜类', '', '', '散装', '斤', 1.0, 0, 0, 0, '上架', ''],
['坏商品A', '不存在的分类', '', '散装', '斤', 1.0, 0, 0, 0, '上架', ''], ['坏商品A', '不存在的分类', '', '', '散装', '斤', 1.0, 0, 0, 0, '上架', ''],
['坏商品B', '叶菜类', '', '散装', '斤', 'abc', 0, 0, 0, '上架', ''], ['坏商品B', '叶菜类', '', '', '散装', '斤', 'abc', 0, 0, 0, '上架', ''],
]); ]);
$response = $this->post('/product/goods/import', ['file' => $file]); $response = $this->post('/product/goods/import', ['file' => $file]);
@@ -260,16 +279,17 @@ class ProductImportExportTest extends ProcurementTestCase
// 路径写法命中蔬菜/叶菜 // 路径写法命中蔬菜/叶菜
$file = $this->makeUploadFile([ $file = $this->makeUploadFile([
ProductExport::HEADERS, ProductExport::HEADERS,
['菠菜', '蔬菜/叶菜', '', '散装', '斤', 1.0, 0, 0, 0, '上架', ''], ['菠菜', '蔬菜/叶菜', '', '岳各庄', '散装', '斤', 1.0, 0, 0, 0, '上架', ''],
]); ]);
$this->post('/product/goods/import', ['file' => $file])->assertJsonPath('success', true); $this->post('/product/goods/import', ['file' => $file])->assertJsonPath('success', true);
$this->assertSame($vegLeaf->id, ProductModel::where('name', '菠菜')->value('category_id')); $this->assertSame($vegLeaf->id, ProductModel::where('name', '菠菜')->value('category_id'));
$this->assertSame('岳各庄', ProductModel::where('name', '菠菜')->value('market'));
// 单名称歧义 + 非末级分类 → 报错零写入 // 单名称歧义 + 非末级分类 → 报错零写入
$file = $this->makeUploadFile([ $file = $this->makeUploadFile([
ProductExport::HEADERS, ProductExport::HEADERS,
['歧义商品', '叶菜', '', '散装', '斤', 1.0, 0, 0, 0, '上架', ''], ['歧义商品', '叶菜', '', '', '散装', '斤', 1.0, 0, 0, 0, '上架', ''],
['非末级商品', '蔬菜', '', '散装', '斤', 1.0, 0, 0, 0, '上架', ''], ['非末级商品', '蔬菜', '', '', '散装', '斤', 1.0, 0, 0, 0, '上架', ''],
]); ]);
$response = $this->post('/product/goods/import', ['file' => $file]); $response = $this->post('/product/goods/import', ['file' => $file]);
$response->assertJsonPath('success', false); $response->assertJsonPath('success', false);
@@ -299,7 +319,7 @@ class ProductImportExportTest extends ProcurementTestCase
$file = $this->makeUploadFile([ $file = $this->makeUploadFile([
ProductExport::HEADERS, ProductExport::HEADERS,
['上海青', '叶菜类', '', '散装', '斤', 1.0, 0, 0, 0, '上架', ''], ['上海青', '叶菜类', '', '', '散装', '斤', 1.0, 0, 0, 0, '上架', ''],
]); ]);
$response = $this->post('/product/goods/import', ['file' => $file]); $response = $this->post('/product/goods/import', ['file' => $file]);
$this->assertFalse($response->json('success'), '缺少导入权限点应被拦截'); $this->assertFalse($response->json('success'), '缺少导入权限点应被拦截');
+2
View File
@@ -21,6 +21,8 @@ export default interface IProduct {
category_id?: number; category_id?: number;
/** 供应商ID */ /** 供应商ID */
supplier_id?: number; supplier_id?: number;
/** 市场(如:新发地、岳各庄) */
market?: string;
/** 商品名称 */ /** 商品名称 */
name?: string; name?: string;
/** 规格/包规 */ /** 规格/包规 */
+6
View File
@@ -17,6 +17,8 @@ export interface IPurchaseDetailRow {
unit: string; unit: string;
supplier_id: number; supplier_id: number;
supplier?: { id: number; name: string } | null; supplier?: { id: number; name: string } | null;
/** 市场(取商品档案) */
market?: string;
/** 成本 */ /** 成本 */
cost_price: number; cost_price: number;
/** 合计数量 */ /** 合计数量 */
@@ -35,6 +37,8 @@ export interface IPurchaseSupplierItem {
/** 规格/包规 */ /** 规格/包规 */
product_spec: string; product_spec: string;
unit: string; unit: string;
/** 市场(取商品档案) */
market?: string;
/** 成本价 */ /** 成本价 */
cost_price: string; cost_price: string;
/** 数量(包数) */ /** 数量(包数) */
@@ -193,6 +197,8 @@ export interface IPurchaseStoreItem {
/** 规格/包规 */ /** 规格/包规 */
product_spec: string; product_spec: string;
unit: string; unit: string;
/** 市场(取商品档案) */
market?: string;
/** 单价(每包;多笔单价时为加权平均,保证 单价×数量=预计金额) */ /** 单价(每包;多笔单价时为加权平均,保证 单价×数量=预计金额) */
price: string; price: string;
/** 数量(包数) */ /** 数量(包数) */
+13 -1
View File
@@ -401,6 +401,18 @@ const ProductGoodsPage: React.FC = () => {
}, },
render: (_, record) => record.supplier?.name ?? '-', render: (_, record) => record.supplier?.name ?? '-',
}, },
{
title: '市场',
dataIndex: 'market',
valueType: 'text',
align: 'center',
fieldProps: {
placeholder: '市场名称,如:新发地',
maxLength: 50,
allowClear: true,
},
render: (_, record) => record.market || '-',
},
{ {
title: '状态', title: '状态',
dataIndex: 'status', dataIndex: 'status',
@@ -552,7 +564,7 @@ const ProductGoodsPage: React.FC = () => {
> >
<div className="mb-3"> <div className="mb-3">
<Text type="secondary"> <Text type="secondary">
**/ **/
// //
</Text> </Text>
+19 -15
View File
@@ -157,6 +157,7 @@ const PurchaseOrderPage: React.FC = () => {
product_name: row.product_name, product_name: row.product_name,
product_spec: row.product_spec, product_spec: row.product_spec,
unit: row.unit, unit: row.unit,
market: row.market,
cost_price: String(row.cost_price), cost_price: String(row.cost_price),
quantity: row.quantity, quantity: row.quantity,
weight: String(row.weight), weight: String(row.weight),
@@ -474,6 +475,7 @@ const PurchaseOrderPage: React.FC = () => {
align: 'center', align: 'center',
render: (_, row) => row.supplier?.name ?? '-', render: (_, row) => row.supplier?.name ?? '-',
}, },
{ title: '市场', dataIndex: 'market', width: 90, align: 'center', render: (v) => v || '-' },
{ {
title: '参考零售价', title: '参考零售价',
key: 'retail_price', key: 'retail_price',
@@ -558,27 +560,28 @@ const PurchaseOrderPage: React.FC = () => {
return ( return (
<Table.Summary.Row> <Table.Summary.Row>
<Table.Summary.Cell index={0} colSpan={6} align="center"> <Table.Summary.Cell index={0} colSpan={7} align="center">
<Text strong></Text> <Text strong></Text>
</Table.Summary.Cell> </Table.Summary.Cell>
{storeTotals.map((amount, index) => ( {storeTotals.map((amount, index) => (
<Table.Summary.Cell key={stores[index].id} index={6 + index} align="center"> <Table.Summary.Cell key={stores[index].id} index={7 + index} align="center">
<Text strong>¥{amount.toFixed(2)}</Text> <Text strong>¥{amount.toFixed(2)}</Text>
</Table.Summary.Cell> </Table.Summary.Cell>
))} ))}
<Table.Summary.Cell index={6 + stores.length} align="center"> <Table.Summary.Cell index={7 + stores.length} align="center">
<Text strong>{totalQuantity}</Text> <Text strong>{totalQuantity}</Text>
</Table.Summary.Cell> </Table.Summary.Cell>
<Table.Summary.Cell index={7 + stores.length} align="center"> <Table.Summary.Cell index={8 + stores.length} align="center">
<Text strong>¥{totalAmount.toFixed(2)}</Text> <Text strong>¥{totalAmount.toFixed(2)}</Text>
</Table.Summary.Cell> </Table.Summary.Cell>
</Table.Summary.Row> </Table.Summary.Row>
); );
}; };
/** 门店购买详情列:商品/购买参考零售价(单价÷包规)/包规/单位/单价/数量/预计金额 + 操作 */ /** 门店购买详情列:商品/市场/购买参考零售价(单价÷包规)/包规/单位/单价/数量/预计金额 + 操作 */
const storeColumns: TableProps<IPurchaseStoreItem>['columns'] = [ const storeColumns: TableProps<IPurchaseStoreItem>['columns'] = [
{ title: '商品', dataIndex: 'product_name', width: 160, align: 'center' }, { title: '商品', dataIndex: 'product_name', width: 160, align: 'center' },
{ title: '市场', dataIndex: 'market', width: 90, align: 'center', render: (v) => v || '-' },
{ {
title: '参考零售价', title: '参考零售价',
key: 'retail_price', key: 'retail_price',
@@ -654,28 +657,29 @@ const PurchaseOrderPage: React.FC = () => {
const totalWeight = items.reduce((sum, row) => sum + Number(row.weight), 0); const totalWeight = items.reduce((sum, row) => sum + Number(row.weight), 0);
return ( return (
<Table.Summary.Row> <Table.Summary.Row>
<Table.Summary.Cell index={0} colSpan={5} align="center"> <Table.Summary.Cell index={0} colSpan={6} align="center">
<Text strong></Text> <Text strong></Text>
</Table.Summary.Cell> </Table.Summary.Cell>
<Table.Summary.Cell index={5} align="center"> <Table.Summary.Cell index={6} align="center">
<Text strong>{totalQuantity}</Text> <Text strong>{totalQuantity}</Text>
</Table.Summary.Cell> </Table.Summary.Cell>
<Table.Summary.Cell index={6} align="center"> <Table.Summary.Cell index={7} align="center">
<Text strong>{totalWeight.toFixed(3)}</Text> <Text strong>{totalWeight.toFixed(3)}</Text>
</Table.Summary.Cell> </Table.Summary.Cell>
<Table.Summary.Cell index={7} align="center"> <Table.Summary.Cell index={8} align="center">
<Text strong type="danger">¥{totalAmount.toFixed(2)}</Text> <Text strong type="danger">¥{totalAmount.toFixed(2)}</Text>
</Table.Summary.Cell> </Table.Summary.Cell>
<Table.Summary.Cell index={8} align="center"> <Table.Summary.Cell index={9} align="center">
<Text strong>-</Text> <Text strong>-</Text>
</Table.Summary.Cell> </Table.Summary.Cell>
</Table.Summary.Row> </Table.Summary.Row>
); );
}; };
/** 供应商采购明细列:品名/包规/单位/成本价/数量/重量/金额(成本口径) */ /** 供应商采购明细列:品名/市场/包规/单位/成本价/数量/重量/金额(成本口径) */
const supplierColumns: TableProps<IPurchaseSupplierItem>['columns'] = [ const supplierColumns: TableProps<IPurchaseSupplierItem>['columns'] = [
{ title: '品名', dataIndex: 'product_name', width: 180, align: 'center' }, { title: '品名', dataIndex: 'product_name', width: 180, align: 'center' },
{ title: '市场', dataIndex: 'market', width: 100, align: 'center', render: (v) => v || '-' },
{ title: '包规', dataIndex: 'product_spec', width: 110, align: 'center', render: (v) => v || '-' }, { title: '包规', dataIndex: 'product_spec', width: 110, align: 'center', render: (v) => v || '-' },
{ title: '单位', dataIndex: 'unit', width: 90, align: 'center', render: (v) => v || '-' }, { title: '单位', dataIndex: 'unit', width: 90, align: 'center', render: (v) => v || '-' },
{ {
@@ -709,16 +713,16 @@ const PurchaseOrderPage: React.FC = () => {
const totalAmount = supplierItems.reduce((sum, row) => sum + Number(row.amount), 0); const totalAmount = supplierItems.reduce((sum, row) => sum + Number(row.amount), 0);
return ( return (
<Table.Summary.Row> <Table.Summary.Row>
<Table.Summary.Cell index={0} colSpan={4} align="center"> <Table.Summary.Cell index={0} colSpan={5} align="center">
<Text strong></Text> <Text strong></Text>
</Table.Summary.Cell> </Table.Summary.Cell>
<Table.Summary.Cell index={4} align="center"> <Table.Summary.Cell index={5} align="center">
<Text strong>{totalQuantity}</Text> <Text strong>{totalQuantity}</Text>
</Table.Summary.Cell> </Table.Summary.Cell>
<Table.Summary.Cell index={5} align="center"> <Table.Summary.Cell index={6} align="center">
<Text strong>{totalWeight.toFixed(3)}</Text> <Text strong>{totalWeight.toFixed(3)}</Text>
</Table.Summary.Cell> </Table.Summary.Cell>
<Table.Summary.Cell index={6} align="center"> <Table.Summary.Cell index={7} align="center">
<Text strong type="danger">¥{totalAmount.toFixed(2)}</Text> <Text strong type="danger">¥{totalAmount.toFixed(2)}</Text>
</Table.Summary.Cell> </Table.Summary.Cell>
</Table.Summary.Row> </Table.Summary.Row>