移除分类图片
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -17,39 +17,11 @@ use Modules\AnnoRoute\Attribute\RequestAttribute;
|
||||
#[RequestAttribute('/mini', 'mini', authGuard: 'users')]
|
||||
class ProductController extends BaseMiniController
|
||||
{
|
||||
/** 分类树(仅含上架商品的分类及其祖先,保证树结构完整) */
|
||||
/** 分类树(全部启用分类,含暂无商品的分类) */
|
||||
#[GetRoute('/product/categories', authorize: true)]
|
||||
public function categories(): JsonResponse
|
||||
{
|
||||
$activeCategoryIds = ProductModel::query()
|
||||
->where('status', ProductModel::STATUS_ON)
|
||||
->distinct()
|
||||
->pluck('category_id')
|
||||
->map(static fn ($id) => (int) $id)
|
||||
->filter(static fn (int $id) => $id > 0);
|
||||
|
||||
$categories = ProductCategoryModel::query()
|
||||
->where('status', ProductCategoryModel::STATUS_NORMAL)
|
||||
->get()
|
||||
->keyBy('id');
|
||||
|
||||
// 保留有上架商品的分类 + 其全部祖先
|
||||
$keep = [];
|
||||
foreach ($activeCategoryIds as $categoryId) {
|
||||
$cursor = $categoryId;
|
||||
$guard = 0;
|
||||
while ($cursor > 0 && $guard++ < 20 && $categories->has($cursor)) {
|
||||
$keep[$cursor] = true;
|
||||
$cursor = (int) $categories[$cursor]->parent_id;
|
||||
}
|
||||
}
|
||||
|
||||
$filtered = array_values(array_filter(
|
||||
$categories->toArray(),
|
||||
static fn (array $item) => isset($keep[$item['id']])
|
||||
));
|
||||
|
||||
return $this->success(ProductCategoryModel::buildTree($filtered));
|
||||
return $this->success(ProductCategoryModel::getTreeData(['*'], true));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -88,7 +60,7 @@ class ProductController extends BaseMiniController
|
||||
if (!$user) return $this->success($data);
|
||||
// 门店
|
||||
$store = $this->boundStore($user);
|
||||
if(!$store) return $this->success('12313');
|
||||
if(!$store) return $this->success($data);
|
||||
|
||||
if ($store->level_id > 0) {
|
||||
foreach ($data['data'] as &$row) {
|
||||
|
||||
@@ -7,15 +7,12 @@ use App\Http\Requests\Product\ProductCategoryFormRequest;
|
||||
use App\Models\ProductCategoryModel;
|
||||
use App\Models\ProductModel;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Modules\AnnoRoute\Attribute\DeleteRoute;
|
||||
use Modules\AnnoRoute\Attribute\GetRoute;
|
||||
use Modules\AnnoRoute\Attribute\PostRoute;
|
||||
use Modules\AnnoRoute\Attribute\PutRoute;
|
||||
use Modules\AnnoRoute\Attribute\RequestAttribute;
|
||||
use Modules\Common\Http\Controllers\BaseController;
|
||||
use Modules\SystemTool\Services\SysFileService;
|
||||
|
||||
/**
|
||||
* 商品分类管理(多级分类:蔬菜/水果/其他)
|
||||
@@ -37,34 +34,17 @@ class ProductCategoryController extends BaseController
|
||||
return $this->success(ProductCategoryModel::getTreeData(['id', 'name', 'parent_id'], true));
|
||||
}
|
||||
|
||||
/** 上传商品分类图片文件 */
|
||||
#[PostRoute('/upload', 'create')]
|
||||
public function uploadImage(Request $request, SysFileService $service): JsonResponse
|
||||
{
|
||||
$data = $request->validate(['file' => 'required|file']);
|
||||
$result = $service->upload(
|
||||
$data['file'],
|
||||
9,
|
||||
20,
|
||||
Auth::id()
|
||||
);
|
||||
return $this->success($result);
|
||||
}
|
||||
|
||||
/** 创建分类 */
|
||||
#[PostRoute(authorize: 'create')]
|
||||
public function create(ProductCategoryFormRequest $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validated();
|
||||
$parentId = (int) $validated['parent_id'];
|
||||
if ($parentId > 0 && ! ProductCategoryModel::whereKey($parentId)->exists()) {
|
||||
throw new RepositoryException('父级分类不存在');
|
||||
}
|
||||
$this->assertParentIsTopLevel((int) $validated['parent_id']);
|
||||
ProductCategoryModel::create($validated);
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
/** 编辑分类(防自引用成环) */
|
||||
/** 编辑分类(防自引用成环;分类最多二级) */
|
||||
#[PutRoute(route: '/{id}', authorize: 'update', where: ['id' => '[0-9]+'])]
|
||||
public function update(int $id, ProductCategoryFormRequest $request): JsonResponse
|
||||
{
|
||||
@@ -73,7 +53,12 @@ class ProductCategoryController extends BaseController
|
||||
throw new RepositoryException('分类不存在');
|
||||
}
|
||||
$validated = $request->validated();
|
||||
$this->assertNoCycle($id, (int) $validated['parent_id']);
|
||||
$parentId = (int) $validated['parent_id'];
|
||||
$this->assertNoCycle($id, $parentId);
|
||||
$this->assertParentIsTopLevel($parentId);
|
||||
if ($parentId > 0 && ProductCategoryModel::where('parent_id', $id)->exists()) {
|
||||
throw new RepositoryException('该分类下存在子分类,不能调整为子分类');
|
||||
}
|
||||
$model->update($validated);
|
||||
return $this->success();
|
||||
}
|
||||
@@ -96,6 +81,23 @@ class ProductCategoryController extends BaseController
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验父级分类:存在且必须为顶级分类(分类最多二级)
|
||||
*/
|
||||
private function assertParentIsTopLevel(int $parentId): void
|
||||
{
|
||||
if ($parentId === 0) {
|
||||
return;
|
||||
}
|
||||
$parent = ProductCategoryModel::whereKey($parentId)->first(['id', 'parent_id']);
|
||||
if ($parent === null) {
|
||||
throw new RepositoryException('父级分类不存在');
|
||||
}
|
||||
if ((int) $parent->parent_id !== 0) {
|
||||
throw new RepositoryException('最多支持二级分类,不能选择子分类作为上级');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 沿父链向上检查,防止 parent_id 指向自身或子孙分类形成环
|
||||
*/
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
|
||||
namespace App\Http\Requests\Product;
|
||||
|
||||
use Illuminate\Validation\Rules\Exists;
|
||||
use Modules\Common\Http\Requests\BaseFormRequest;
|
||||
use Modules\SystemTool\Models\SysFileModel;
|
||||
|
||||
/**
|
||||
* 商品分类 创建/编辑 验证
|
||||
@@ -24,7 +22,6 @@ class ProductCategoryFormRequest extends BaseFormRequest
|
||||
'name' => 'required|string|max:50',
|
||||
'parent_id' => 'required|integer|min:0',
|
||||
'sort' => 'nullable|integer',
|
||||
'icon_id' => ['nullable', 'integer', new Exists(SysFileModel::class, 'id')],
|
||||
'status' => 'nullable|integer|in:0,1',
|
||||
];
|
||||
}
|
||||
@@ -36,7 +33,6 @@ class ProductCategoryFormRequest extends BaseFormRequest
|
||||
'name.max' => '分类名称最长 50 个字符',
|
||||
'parent_id.min' => '父级分类ID不正确',
|
||||
'status.in' => '状态值不正确',
|
||||
'icon_id.exists' => '请重新上传图片'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,13 +44,17 @@ class ProductFormRequest extends BaseFormRequest
|
||||
}
|
||||
|
||||
/**
|
||||
* 交叉校验:按成本百分比计价(price_type=1)时上浮百分点必填
|
||||
* 交叉校验:商品只能挂在末级分类;按成本百分比计价(price_type=1)时上浮百分点必填
|
||||
* (Laravel 12 FormRequest 的 after() 需返回单个 Closure,由容器 call 后注册到 Validator)
|
||||
*/
|
||||
public function after(): Closure
|
||||
{
|
||||
return function (Validator $validator): void {
|
||||
$data = (array) $validator->getData();
|
||||
$categoryId = (int) ($data['category_id'] ?? 0);
|
||||
if ($categoryId > 0 && ProductCategoryModel::where('parent_id', $categoryId)->exists()) {
|
||||
$validator->errors()->add('category_id', '该分类下存在子分类,请选择末级分类');
|
||||
}
|
||||
foreach ((array) ($data['prices'] ?? []) as $index => $row) {
|
||||
$priceType = (int) ($row['price_type'] ?? ProductPriceModel::PRICE_TYPE_FIXED);
|
||||
if ($priceType === ProductPriceModel::PRICE_TYPE_PERCENT
|
||||
|
||||
@@ -5,8 +5,6 @@ namespace App\Models;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Modules\SystemTool\Models\SysFileModel;
|
||||
|
||||
/**
|
||||
* 商品分类模型(蔬菜/水果/其他,多级分类自关联)
|
||||
@@ -25,40 +23,17 @@ class ProductCategoryModel extends Model
|
||||
'parent_id',
|
||||
'name',
|
||||
'sort',
|
||||
'status',
|
||||
'icon_id'
|
||||
'status'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'parent_id' => 'integer',
|
||||
'sort' => 'integer',
|
||||
'status' => 'integer',
|
||||
'icon_id' => 'integer',
|
||||
'created_at' => 'datetime:Y-m-d H:i:s',
|
||||
'updated_at' => 'datetime:Y-m-d H:i:s',
|
||||
];
|
||||
|
||||
protected $appends = ['icon_url'];
|
||||
|
||||
protected $with = ['icon'];
|
||||
|
||||
/**
|
||||
* 关联图标
|
||||
*/
|
||||
public function icon(): HasOne
|
||||
{
|
||||
return $this->hasOne(SysFileModel::class, 'id', 'icon_id');
|
||||
}
|
||||
|
||||
// 图标链接
|
||||
public function getIconUrlAttribute()
|
||||
{
|
||||
if($this->icon) {
|
||||
return $this->icon->preview_url;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 父分类
|
||||
*/
|
||||
|
||||
@@ -17,7 +17,6 @@ return new class extends Migration
|
||||
Schema::create('product_category', function (Blueprint $table) {
|
||||
$table->increments('id')->comment('分类ID');
|
||||
$table->integer('parent_id')->default(0)->comment('父级分类ID(0为顶级)');
|
||||
$table->integer('icon_id')->nullable()->comment('商品图标');
|
||||
$table->string('name', 50)->comment('分类名称');
|
||||
$table->integer('sort')->default(0)->comment('排序(采购单导出按此排序)');
|
||||
$table->integer('status')->default(1)->comment('状态(1正常 0停用)');
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\ProductCategoryModel;
|
||||
use App\Models\UserModel;
|
||||
|
||||
/**
|
||||
* 商品分类两级限制 + 商品只能挂末级分类:
|
||||
* 创建/编辑分类的父级必须为顶级、含子分类的分类不可移动为子级、商品分类必须为末级
|
||||
*/
|
||||
class ProductCategoryTest extends ProcurementTestCase
|
||||
{
|
||||
/** 造一个顶级分类 */
|
||||
private function makeTopCategory(string $name = '蔬菜'): ProductCategoryModel
|
||||
{
|
||||
return ProductCategoryModel::create([
|
||||
'parent_id' => 0,
|
||||
'name' => $name,
|
||||
'sort' => 1,
|
||||
'status' => ProductCategoryModel::STATUS_NORMAL,
|
||||
]);
|
||||
}
|
||||
|
||||
/** 顶级分类下可创建子分类(二级) */
|
||||
public function test_create_child_category_under_top_level(): void
|
||||
{
|
||||
$this->actingAsSysUser();
|
||||
$top = $this->makeTopCategory();
|
||||
|
||||
$this->postJson('/product/category', [
|
||||
'parent_id' => $top->id,
|
||||
'name' => '叶菜类',
|
||||
])->assertOk()->assertJsonPath('success', true);
|
||||
|
||||
$this->assertDatabaseHas('product_category', ['name' => '叶菜类', 'parent_id' => $top->id]);
|
||||
}
|
||||
|
||||
/** 二级分类不可作为父级(最多二级) */
|
||||
public function test_create_category_under_second_level_rejected(): void
|
||||
{
|
||||
$this->actingAsSysUser();
|
||||
$top = $this->makeTopCategory();
|
||||
$child = ProductCategoryModel::create([
|
||||
'parent_id' => $top->id,
|
||||
'name' => '叶菜类',
|
||||
'sort' => 1,
|
||||
'status' => ProductCategoryModel::STATUS_NORMAL,
|
||||
]);
|
||||
|
||||
$this->postJson('/product/category', [
|
||||
'parent_id' => $child->id,
|
||||
'name' => '菠菜',
|
||||
])->assertOk()
|
||||
->assertJsonPath('success', false)
|
||||
->assertJsonPath('msg', '最多支持二级分类,不能选择子分类作为上级');
|
||||
}
|
||||
|
||||
/** 编辑:二级分类可平级移动到另一个顶级分类下 */
|
||||
public function test_update_second_level_move_to_another_top(): void
|
||||
{
|
||||
$this->actingAsSysUser();
|
||||
$topA = $this->makeTopCategory('蔬菜');
|
||||
$topB = $this->makeTopCategory('水果');
|
||||
$child = ProductCategoryModel::create([
|
||||
'parent_id' => $topA->id,
|
||||
'name' => '叶菜类',
|
||||
'sort' => 1,
|
||||
'status' => ProductCategoryModel::STATUS_NORMAL,
|
||||
]);
|
||||
|
||||
$this->putJson('/product/category/' . $child->id, [
|
||||
'parent_id' => $topB->id,
|
||||
'name' => '叶菜类',
|
||||
])->assertOk()->assertJsonPath('success', true);
|
||||
|
||||
$this->assertSame($topB->id, (int) $child->fresh()->parent_id);
|
||||
}
|
||||
|
||||
/** 编辑:含子分类的分类不能调整为子分类(否则子分类变三级) */
|
||||
public function test_update_parent_with_children_cannot_move_under_category(): void
|
||||
{
|
||||
$this->actingAsSysUser();
|
||||
$top = $this->makeTopCategory('蔬菜');
|
||||
$other = $this->makeTopCategory('水果');
|
||||
ProductCategoryModel::create([
|
||||
'parent_id' => $top->id,
|
||||
'name' => '叶菜类',
|
||||
'sort' => 1,
|
||||
'status' => ProductCategoryModel::STATUS_NORMAL,
|
||||
]);
|
||||
|
||||
$this->putJson('/product/category/' . $top->id, [
|
||||
'parent_id' => $other->id,
|
||||
'name' => '蔬菜',
|
||||
])->assertOk()
|
||||
->assertJsonPath('success', false)
|
||||
->assertJsonPath('msg', '该分类下存在子分类,不能调整为子分类');
|
||||
}
|
||||
|
||||
/** 编辑:父级不存在时提示 */
|
||||
public function test_update_with_missing_parent_rejected(): void
|
||||
{
|
||||
$this->actingAsSysUser();
|
||||
$top = $this->makeTopCategory();
|
||||
|
||||
$this->putJson('/product/category/' . $top->id, [
|
||||
'parent_id' => 99999,
|
||||
'name' => '蔬菜',
|
||||
])->assertOk()
|
||||
->assertJsonPath('success', false)
|
||||
->assertJsonPath('msg', '父级分类不存在');
|
||||
}
|
||||
|
||||
/** 商品只能挂在末级分类:选择有子分类的分类创建商品 → 验证失败 */
|
||||
public function test_create_product_with_parent_category_rejected(): void
|
||||
{
|
||||
$this->actingAsSysUser();
|
||||
$top = $this->makeTopCategory('蔬菜');
|
||||
ProductCategoryModel::create([
|
||||
'parent_id' => $top->id,
|
||||
'name' => '叶菜类',
|
||||
'sort' => 1,
|
||||
'status' => ProductCategoryModel::STATUS_NORMAL,
|
||||
]);
|
||||
|
||||
$this->postJson('/product/goods', [
|
||||
'category_id' => $top->id,
|
||||
'name' => '大白菜',
|
||||
])->assertOk()
|
||||
->assertJsonPath('success', false)
|
||||
->assertJsonPath('msg', '该分类下存在子分类,请选择末级分类');
|
||||
}
|
||||
|
||||
/** 商品挂在末级分类(无子分类)可正常创建 */
|
||||
public function test_create_product_with_leaf_category_ok(): void
|
||||
{
|
||||
$this->actingAsSysUser();
|
||||
$top = $this->makeTopCategory('蔬菜');
|
||||
$leaf = ProductCategoryModel::create([
|
||||
'parent_id' => $top->id,
|
||||
'name' => '叶菜类',
|
||||
'sort' => 1,
|
||||
'status' => ProductCategoryModel::STATUS_NORMAL,
|
||||
]);
|
||||
|
||||
$this->postJson('/product/goods', [
|
||||
'category_id' => $leaf->id,
|
||||
'name' => '大白菜',
|
||||
'content' => '图文详情',
|
||||
])->assertOk()->assertJsonPath('success', true);
|
||||
}
|
||||
|
||||
/** 小程序分类树:返回全部启用分类(含无商品的分类),停用分类不返回 */
|
||||
public function test_mini_categories_returns_all_enabled_categories(): void
|
||||
{
|
||||
$top = $this->makeTopCategory('蔬菜');
|
||||
$empty = $this->makeTopCategory('水产'); // 无任何商品
|
||||
ProductCategoryModel::create([
|
||||
'parent_id' => $top->id,
|
||||
'name' => '叶菜类',
|
||||
'sort' => 1,
|
||||
'status' => ProductCategoryModel::STATUS_NORMAL,
|
||||
]);
|
||||
$disabled = ProductCategoryModel::create([
|
||||
'parent_id' => 0,
|
||||
'name' => '停用分类',
|
||||
'sort' => 1,
|
||||
'status' => ProductCategoryModel::STATUS_DISABLED,
|
||||
]);
|
||||
|
||||
$this->actingAsMiniUser(UserModel::factory()->create());
|
||||
|
||||
$response = $this->getJson('/mini/product/categories');
|
||||
$response->assertOk()->assertJsonPath('success', true);
|
||||
|
||||
$ids = collect($response->json('data'))->pluck('id');
|
||||
$this->assertContains($top->id, $ids);
|
||||
$this->assertContains($empty->id, $ids, '无商品的分类也应返回');
|
||||
$this->assertNotContains($disabled->id, $ids, '停用分类不应返回');
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
import type {ISysFileInfo} from "@/domain/iSysFile.ts";
|
||||
|
||||
/** 商品分类(多级,children 由后端组装) */
|
||||
export default interface IProductCategory {
|
||||
id?: number;
|
||||
@@ -7,9 +5,6 @@ export default interface IProductCategory {
|
||||
name?: string;
|
||||
sort?: number;
|
||||
status?: number;
|
||||
icon_id?: number;
|
||||
icon?: ISysFileInfo;
|
||||
icon_url?: string;
|
||||
children?: IProductCategory[];
|
||||
created_at?: string;
|
||||
}
|
||||
@@ -19,6 +14,10 @@ export interface IProductCategoryTree {
|
||||
id?: number;
|
||||
parent_id?: number;
|
||||
name?: string;
|
||||
/** TreeSelect 节点禁选标记(前端组装:父分类/超层级节点不可选) */
|
||||
disabled?: boolean;
|
||||
/** Tree 节点不可选中标记(前端组装:侧栏父分类仅供展开) */
|
||||
selectable?: boolean;
|
||||
children?: IProductCategoryTree[];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {Button, Image, Tag, Typography} from 'antd';
|
||||
import React, {useEffect, useMemo, useState} from 'react';
|
||||
import {Button, Form, Tag, TreeSelect, Typography} from 'antd';
|
||||
import type {FormInstance} from 'antd';
|
||||
import {NodeExpandOutlined} from '@ant-design/icons';
|
||||
import XinTable from '@/components/XinTable';
|
||||
import type { XinTableColumn, XinTableProps } from '@/components/XinTable/typings.ts';
|
||||
@@ -10,6 +11,45 @@ import {getCategoryTable, getCategoryTree} from '@/api/product/category.ts';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
/**
|
||||
* 上级分类选择(分类最多二级):
|
||||
* - 二级分类禁选(不能作为上级,否则出现三级)
|
||||
* - 编辑时禁选自身
|
||||
* - 编辑的分类含子分类时禁选所有一级分类(只能保持顶级,否则其子分类会变成三级)
|
||||
*/
|
||||
const ParentCategorySelect: React.FC<{
|
||||
form: FormInstance;
|
||||
tree: IProductCategoryTree[];
|
||||
value?: number;
|
||||
onChange?: (value: number) => void;
|
||||
}> = ({ form, tree, value, onChange }) => {
|
||||
// id/children 由 XinTable 编辑时 setFieldsValue(record) 写入(非表单项,需传 form 监听)
|
||||
const editingId = Form.useWatch('id', form);
|
||||
const children = Form.useWatch('children', form);
|
||||
const hasChildren = Array.isArray(children) && children.length > 0;
|
||||
|
||||
const treeData = useMemo(() => {
|
||||
const walk = (nodes: IProductCategoryTree[], depth: number): IProductCategoryTree[] =>
|
||||
nodes.map((node) => ({
|
||||
...node,
|
||||
disabled: depth >= 2 || node.id === editingId || (hasChildren && depth >= 1),
|
||||
children: node.children?.length ? walk(node.children, depth + 1) : node.children,
|
||||
}));
|
||||
return [{ id: 0, name: '顶级分类', children: walk(tree, 1) }];
|
||||
}, [tree, editingId, hasChildren]);
|
||||
|
||||
return (
|
||||
<TreeSelect
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
treeData={treeData}
|
||||
fieldNames={{ label: 'name', value: 'id', children: 'children' }}
|
||||
placeholder="默认顶级分类"
|
||||
treeDefaultExpandAll
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 递归收集全部节点 id(用于展开整棵树)
|
||||
*/
|
||||
@@ -52,16 +92,10 @@ const ProductCategoryPage: React.FC = () => {
|
||||
{
|
||||
title: '上级分类',
|
||||
dataIndex: 'parent_id',
|
||||
valueType: 'treeSelect',
|
||||
hideInTable: true,
|
||||
hideInSearch: true,
|
||||
initialValue: 0,
|
||||
fieldProps: {
|
||||
treeData: [{ id: 0, name: '顶级分类', children: categoryTree }],
|
||||
fieldNames: { label: 'name', value: 'id', children: 'children' },
|
||||
placeholder: '默认顶级分类',
|
||||
treeDefaultExpandAll: true,
|
||||
},
|
||||
fieldRender: (form) => <ParentCategorySelect form={form} tree={categoryTree} />,
|
||||
},
|
||||
{
|
||||
title: '排序',
|
||||
@@ -90,31 +124,6 @@ const ProductCategoryPage: React.FC = () => {
|
||||
},
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '分类图标',
|
||||
dataIndex: 'icon_id',
|
||||
valueType: 'image',
|
||||
fieldProps: {
|
||||
action: '/product/category/upload',
|
||||
mode: 'single',
|
||||
maxCount: 1,
|
||||
changeType: "id"
|
||||
},
|
||||
render: (_, record: IProductCategory) => {
|
||||
const url = record.icon_url
|
||||
if (!url) return '-';
|
||||
return (
|
||||
<Image
|
||||
src={url}
|
||||
width={32}
|
||||
height={32}
|
||||
style={{ objectFit: 'cover', borderRadius: 4 }}
|
||||
/>
|
||||
);
|
||||
},
|
||||
align: 'center',
|
||||
hideInSearch: true,
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'updated_at',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
Button, Card,
|
||||
Drawer,
|
||||
@@ -41,6 +41,22 @@ const PRICE_TYPE_OPTIONS = [
|
||||
/** 四舍五入保留两位 */
|
||||
const round2 = (v: number) => Math.round(v * 100) / 100;
|
||||
|
||||
/** 商品表单用分类树:有子分类的节点禁选(商品只能挂在末级分类上) */
|
||||
const markParentDisabled = (nodes: IProductCategoryTree[]): IProductCategoryTree[] =>
|
||||
nodes.map((node) => ({
|
||||
...node,
|
||||
disabled: !!node.children?.length,
|
||||
children: node.children?.length ? markParentDisabled(node.children) : node.children,
|
||||
}));
|
||||
|
||||
/** 侧栏用分类树:有子分类的节点不可选中(仅供展开,筛选按末级分类) */
|
||||
const markParentUnselectable = (nodes: IProductCategoryTree[]): IProductCategoryTree[] =>
|
||||
nodes.map((node) => ({
|
||||
...node,
|
||||
selectable: !node.children?.length,
|
||||
children: node.children?.length ? markParentUnselectable(node.children) : node.children,
|
||||
}));
|
||||
|
||||
/**
|
||||
* 等级价格表单:每个等级 = 计价类型(固定价/成本百分比)+ 对应输入框;
|
||||
* 切换计价类型时按成本价自动换算(固定→百分比:percent=(price/cost-1)*100;百分比→固定:price=cost*(1+percent/100))
|
||||
@@ -159,6 +175,10 @@ const ProductGoodsPage: React.FC = () => {
|
||||
const [levels, setLevels] = useState<ICustomerLevel[]>([]);
|
||||
const [suppliers, setSuppliers] = useState<ISupplier[]>([]);
|
||||
const [categoryTree, setCategoryTree] = useState<IProductCategoryTree[]>([]);
|
||||
// 商品表单专用分类树:父分类禁选,仅末级可选(侧栏筛选/价格矩阵仍用原始树)
|
||||
const formCategoryTree = useMemo(() => markParentDisabled(categoryTree), [categoryTree]);
|
||||
// 侧栏分类树:父分类不可选中,仅作展开归组
|
||||
const sidebarCategoryTree = useMemo(() => markParentUnselectable(categoryTree), [categoryTree]);
|
||||
|
||||
// ===== 分类侧栏 =====
|
||||
const [activeCategory, setActiveCategory] = useState<number | undefined>(undefined);
|
||||
@@ -476,12 +496,12 @@ const ProductGoodsPage: React.FC = () => {
|
||||
align: "center",
|
||||
rules: [{ required: true, message: '请选择分类' }],
|
||||
fieldProps: {
|
||||
treeData: categoryTree,
|
||||
treeData: formCategoryTree,
|
||||
fieldNames: { label: 'name', value: 'id', children: 'children' },
|
||||
treeDefaultExpandAll: true,
|
||||
showSearch: true,
|
||||
treeNodeFilterProp: 'name',
|
||||
placeholder: '选择分类',
|
||||
placeholder: '选择末级分类',
|
||||
},
|
||||
hideInSearch: true,
|
||||
render: (_, record) =>
|
||||
@@ -647,7 +667,7 @@ const ProductGoodsPage: React.FC = () => {
|
||||
showLine
|
||||
blockNode
|
||||
onSelect={(selectedKeys) => setActiveCategory(Number(selectedKeys[0]))}
|
||||
treeData={categoryTree}
|
||||
treeData={sidebarCategoryTree}
|
||||
selectedKeys={activeCategory ? [activeCategory] : undefined}
|
||||
fieldNames={{title: 'name', key: 'id', children: 'children'}}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user