From 258df92e6c12c39451203c9fe28f4f5adad18331 Mon Sep 17 00:00:00 2001 From: liu <2302563948@qq.com> Date: Wed, 5 Aug 2026 19:28:53 +0800 Subject: [PATCH] =?UTF-8?q?=E5=95=86=E5=93=81=E5=88=97=E8=A1=A8=E3=80=81?= =?UTF-8?q?=E5=88=86=E7=B1=BB=E7=AE=A1=E7=90=86=E3=80=81=E5=AE=A2=E6=88=B7?= =?UTF-8?q?=E7=AD=89=E7=BA=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Product/ProductCategoryController.php | 2 +- .../Controllers/Product/ProductController.php | 17 ++ .../Customer/CustomerLevelFormRequest.php | 4 +- .../Product/ProductCategoryFormRequest.php | 4 + .../Requests/Product/ProductFormRequest.php | 15 +- app/Models/CustomerLevelModel.php | 14 +- app/Models/ProductCategoryModel.php | 22 +-- app/Models/ProductModel.php | 28 ++- .../factories/CustomerLevelModelFactory.php | 30 ---- database/factories/ProductModelFactory.php | 49 ------ .../factories/ProductPriceModelFactory.php | 29 ---- .../factories/PurchaseOrderModelFactory.php | 43 ----- database/factories/StoreModelFactory.php | 51 ------ .../factories/StoreOrderItemModelFactory.php | 42 ----- database/factories/StoreOrderModelFactory.php | 50 ------ database/factories/SupplierModelFactory.php | 43 ----- database/factories/UserModelFactory.php | 69 -------- .../2026_07_23_030608_create_store_table.php | 2 +- ...2026_07_23_030609_create_product_table.php | 6 +- ...026_07_23_030611_create_purchase_table.php | 1 + ...firmed_at_to_purchase_order_item_table.php | 32 ---- database/seeders/PermissionSeeder.php | 1 + database/seeders/SysDataSeeder.php | 6 +- .../Http/Controllers/SysFileController.php | 8 + web/api/product/category.ts | 3 +- web/api/system/sysFile.ts | 11 ++ .../XinFormField/ImageUploader/index.tsx | 39 ++++- .../XinFormField/ImageUploader/typings.ts | 2 +- .../XinFormField/RichTextEditor/index.tsx | 161 ++++++++++++++++++ .../XinFormField/RichTextEditor/typings.ts | 44 +++++ web/domain/iCustomerLevel.ts | 5 +- web/domain/iProduct.ts | 23 ++- web/domain/iProductCategory.ts | 13 +- web/pages/customer/level.tsx | 2 +- web/pages/product/category.tsx | 19 ++- web/pages/product/goods.tsx | 98 ++++++++--- 36 files changed, 472 insertions(+), 516 deletions(-) delete mode 100644 database/factories/CustomerLevelModelFactory.php delete mode 100644 database/factories/ProductModelFactory.php delete mode 100644 database/factories/ProductPriceModelFactory.php delete mode 100644 database/factories/PurchaseOrderModelFactory.php delete mode 100644 database/factories/StoreModelFactory.php delete mode 100644 database/factories/StoreOrderItemModelFactory.php delete mode 100644 database/factories/StoreOrderModelFactory.php delete mode 100644 database/factories/SupplierModelFactory.php delete mode 100644 database/factories/UserModelFactory.php delete mode 100644 database/migrations/2026_07_23_120000_add_supplier_confirmed_at_to_purchase_order_item_table.php create mode 100644 web/components/XinFormField/RichTextEditor/index.tsx create mode 100644 web/components/XinFormField/RichTextEditor/typings.ts diff --git a/app/Http/Controllers/Product/ProductCategoryController.php b/app/Http/Controllers/Product/ProductCategoryController.php index 1b194d0..506fcce 100644 --- a/app/Http/Controllers/Product/ProductCategoryController.php +++ b/app/Http/Controllers/Product/ProductCategoryController.php @@ -34,7 +34,7 @@ class ProductCategoryController extends BaseController #[GetRoute('/tree', 'query')] public function tree(): JsonResponse { - return $this->success(ProductCategoryModel::getTreeData(onlyEnabled: true)); + return $this->success(ProductCategoryModel::getTreeData(['id', 'name', 'parent_id'], true)); } /** 上传商品分类图片文件 */ diff --git a/app/Http/Controllers/Product/ProductController.php b/app/Http/Controllers/Product/ProductController.php index aa5d375..6031314 100644 --- a/app/Http/Controllers/Product/ProductController.php +++ b/app/Http/Controllers/Product/ProductController.php @@ -13,6 +13,7 @@ use App\Models\StoreModel; use App\Models\UserModel; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; use Modules\AnnoRoute\Attribute\DeleteRoute; use Modules\AnnoRoute\Attribute\GetRoute; @@ -20,6 +21,7 @@ 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; /** * 商品档案管理 @@ -53,6 +55,21 @@ class ProductController extends BaseController return $this->success($data); } + /** 上传商品分类图片文件 */ + #[PostRoute('/upload', 'create')] + public function uploadImage(Request $request, SysFileService $service): JsonResponse + { + $data = $request->validate(['file' => 'required|file']); + $result = $service->upload( + $data['file'], + 10, + 20, + Auth::id() + ); + return $this->success($result); + } + + /** 创建商品(事务内建商品 + 同步等级价格) */ #[PostRoute(authorize: 'create')] public function create(ProductFormRequest $request): JsonResponse diff --git a/app/Http/Requests/Customer/CustomerLevelFormRequest.php b/app/Http/Requests/Customer/CustomerLevelFormRequest.php index f2dd2cc..fd8efd0 100644 --- a/app/Http/Requests/Customer/CustomerLevelFormRequest.php +++ b/app/Http/Requests/Customer/CustomerLevelFormRequest.php @@ -25,7 +25,7 @@ class CustomerLevelFormRequest extends BaseFormRequest 'name' => ['required', 'string', 'max:50', $unique], 'sort' => 'nullable|integer', 'status' => 'nullable|integer|in:0,1', - 'icon' => ['nullable', 'integer', new Exists(SysFileModel::class, 'id')], + 'icon_id' => ['nullable', 'integer', new Exists(SysFileModel::class, 'id')], ]; } @@ -36,7 +36,7 @@ class CustomerLevelFormRequest extends BaseFormRequest 'name.max' => '等级名称最长 50 个字符', 'name.unique' => '等级名称已存在', 'status.in' => '状态值不正确', - 'icon.exists' => '请重新上传图片' + 'icon_id.exists' => '请重新上传图片' ]; } } diff --git a/app/Http/Requests/Product/ProductCategoryFormRequest.php b/app/Http/Requests/Product/ProductCategoryFormRequest.php index 2ee5744..ec952b5 100644 --- a/app/Http/Requests/Product/ProductCategoryFormRequest.php +++ b/app/Http/Requests/Product/ProductCategoryFormRequest.php @@ -2,7 +2,9 @@ namespace App\Http\Requests\Product; +use Illuminate\Validation\Rules\Exists; use Modules\Common\Http\Requests\BaseFormRequest; +use Modules\SystemTool\Models\SysFileModel; /** * 商品分类 创建/编辑 验证 @@ -22,6 +24,7 @@ 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', ]; } @@ -33,6 +36,7 @@ class ProductCategoryFormRequest extends BaseFormRequest 'name.max' => '分类名称最长 50 个字符', 'parent_id.min' => '父级分类ID不正确', 'status.in' => '状态值不正确', + 'icon_id.exists' => '请重新上传图片' ]; } } diff --git a/app/Http/Requests/Product/ProductFormRequest.php b/app/Http/Requests/Product/ProductFormRequest.php index d80be79..5f5cc84 100644 --- a/app/Http/Requests/Product/ProductFormRequest.php +++ b/app/Http/Requests/Product/ProductFormRequest.php @@ -2,7 +2,11 @@ namespace App\Http\Requests\Product; +use App\Models\ProductCategoryModel; +use App\Models\SupplierModel; +use Illuminate\Validation\Rules\Exists; use Modules\Common\Http\Requests\BaseFormRequest; +use Modules\SystemTool\Models\SysFileModel; /** * 商品档案 创建/编辑 验证(含多等级价格 prices 数组) @@ -14,14 +18,17 @@ class ProductFormRequest extends BaseFormRequest public function rules(): array { return [ + 'category_id' => ['required','integer', new Exists(ProductCategoryModel::class,'id')], + 'supplier_id' => ['nullable','integer', 'exclude_if:supplier_id,0', new Exists(SupplierModel::class,'id')], 'name' => 'required|string|max:100', 'spec' => 'nullable|string|max:100', - 'grade' => 'nullable|string|max:50', 'unit' => 'nullable|string|max:20', - 'category_id' => 'required|integer|exists:product_category,id', - 'supplier_id' => 'nullable|integer|exclude_if:supplier_id,0|exists:supplier,id', - 'image' => 'nullable|string|max:255', + 'image_ids' => 'nullable|array|max:255', + 'image_ids.*' => ['integer', new Exists(SysFileModel::class, 'id')], + 'content' => 'nullable|string', 'sort' => 'nullable|integer', + 'shelf_life' => 'nullable|integer|min:0', + 'stock' => 'nullable|integer|min:0', 'status' => 'nullable|integer|in:0,1', 'remark' => 'nullable|string|max:255', 'prices' => 'nullable|array', diff --git a/app/Models/CustomerLevelModel.php b/app/Models/CustomerLevelModel.php index eba2119..283ac9e 100644 --- a/app/Models/CustomerLevelModel.php +++ b/app/Models/CustomerLevelModel.php @@ -2,7 +2,6 @@ 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\HasMany; @@ -28,7 +27,7 @@ class CustomerLevelModel extends Model 'name', 'sort', 'status', - 'icon' + 'icon_id' ]; protected $casts = [ @@ -45,15 +44,16 @@ class CustomerLevelModel extends Model */ public function icon(): HasOne { - return $this->hasOne(SysFileModel::class, 'id', 'icon'); + return $this->hasOne(SysFileModel::class, 'id', 'icon_id'); } // 图标链接 - public function icon_url(): Attribute + public function getIconUrlAttribute() { - return Attribute::make( - get: fn () => $this->icon->preview_url, - ); + if($this->icon) { + return $this->icon->preview_url; + } + return null; } /** diff --git a/app/Models/ProductCategoryModel.php b/app/Models/ProductCategoryModel.php index b32aae4..df9608a 100644 --- a/app/Models/ProductCategoryModel.php +++ b/app/Models/ProductCategoryModel.php @@ -2,7 +2,6 @@ namespace App\Models; -use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; @@ -27,34 +26,37 @@ class ProductCategoryModel extends Model 'name', 'sort', 'status', - 'icon' + 'icon_id' ]; protected $casts = [ 'parent_id' => 'integer', 'sort' => 'integer', 'status' => 'integer', - 'icon' => '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'); + return $this->hasOne(SysFileModel::class, 'id', 'icon_id'); } // 图标链接 - public function icon_url(): Attribute + public function getIconUrlAttribute() { - return Attribute::make( - get: fn () => $this->icon->preview_url, - ); + if($this->icon) { + return $this->icon->preview_url; + } + return null; } /** @@ -87,14 +89,14 @@ class ProductCategoryModel extends Model * @param bool $onlyEnabled 是否仅返回启用分类 * @return array */ - public static function getTreeData(bool $onlyEnabled = false): array + public static function getTreeData($columns = ['*'], bool $onlyEnabled = false): array { $query = static::query()->orderBy('sort')->orderBy('id'); if ($onlyEnabled) { $query->where('status', self::STATUS_NORMAL); } - return static::buildTree($query->get()->toArray()); + return static::buildTree($query->get($columns)->toArray()); } /** diff --git a/app/Models/ProductModel.php b/app/Models/ProductModel.php index 68fb721..685a213 100644 --- a/app/Models/ProductModel.php +++ b/app/Models/ProductModel.php @@ -31,7 +31,7 @@ class ProductModel extends Model 'name', 'spec', 'unit', - 'images', + 'image_ids', 'content', 'sort', 'shelf_life', @@ -47,22 +47,32 @@ class ProductModel extends Model 'stock' => 'integer', 'sort' => 'integer', 'status' => 'integer', + 'created_at' => 'datetime:Y-m-d H:i:s', + 'updated_at' => 'datetime:Y-m-d H:i:s', ]; - public function images(): Attribute + protected $appends = ['images_arr']; + + /** + * 封面图片 + */ + public function imageIds(): Attribute { return Attribute::make( - get: function($value){ - if (empty($value)) { - return collect(); - } - $ids = explode(',', $value); - return SysFileModel::whereIn('id', $ids)->get(); - }, + get: fn ($value) => explode(',', $value), set: fn ($value) => is_array($value) ? implode(',', $value) : $value, ); } + // 获取封面图片关联数据 + public function getImagesArrAttribute(): array + { + if( $this->image_ids ) { + return SysFileModel::whereIn('id', $this->image_ids )->get()->toArray(); + } + return []; + } + /** * 所属分类 */ diff --git a/database/factories/CustomerLevelModelFactory.php b/database/factories/CustomerLevelModelFactory.php deleted file mode 100644 index 7ad72af..0000000 --- a/database/factories/CustomerLevelModelFactory.php +++ /dev/null @@ -1,30 +0,0 @@ - - */ -class CustomerLevelModelFactory extends Factory -{ - protected $model = CustomerLevelModel::class; - - private static int $sequence = 0; - - public function definition(): array - { - $seq = ++self::$sequence; - - return [ - 'name' => '客户等级' . $seq, - 'sort' => $seq, - 'status' => CustomerLevelModel::STATUS_NORMAL, - 'remark' => '', - ]; - } -} diff --git a/database/factories/ProductModelFactory.php b/database/factories/ProductModelFactory.php deleted file mode 100644 index 5bc9f3a..0000000 --- a/database/factories/ProductModelFactory.php +++ /dev/null @@ -1,49 +0,0 @@ - - */ -class ProductModelFactory extends Factory -{ - protected $model = ProductModel::class; - - private const NAMES = ['大白菜', '土豆', '西红柿', '黄瓜', '苹果', '香蕉']; - private const SPECS = ['500g/袋', '10斤/箱', '散装', '25斤/袋']; - private const GRADES = ['特级', '一级', '二级']; - private const UNITS = ['斤', '箱', '袋']; - - private static int $sequence = 0; - - public function definition(): array - { - $seq = ++self::$sequence; - - return [ - 'category_id' => 0, - 'supplier_id' => 0, - 'name' => self::NAMES[$seq % count(self::NAMES)] . $seq, - 'spec' => self::SPECS[$seq % count(self::SPECS)], - 'grade' => self::GRADES[$seq % count(self::GRADES)], - 'unit' => self::UNITS[$seq % count(self::UNITS)], - 'image' => '', - 'sort' => $seq, - 'status' => ProductModel::STATUS_ON, - 'remark' => '', - ]; - } - - /** - * 下架商品 - */ - public function off(): static - { - return $this->state(fn () => ['status' => ProductModel::STATUS_OFF]); - } -} diff --git a/database/factories/ProductPriceModelFactory.php b/database/factories/ProductPriceModelFactory.php deleted file mode 100644 index a6f4fcf..0000000 --- a/database/factories/ProductPriceModelFactory.php +++ /dev/null @@ -1,29 +0,0 @@ - - */ -class ProductPriceModelFactory extends Factory -{ - protected $model = ProductPriceModel::class; - - private static int $sequence = 0; - - public function definition(): array - { - $seq = ++self::$sequence; - - return [ - 'product_id' => 0, - 'level_id' => 0, - 'price' => number_format(random_int(100, 10000) / 100 + $seq * 0.01, 2, '.', ''), - ]; - } -} diff --git a/database/factories/PurchaseOrderModelFactory.php b/database/factories/PurchaseOrderModelFactory.php deleted file mode 100644 index b2a4899..0000000 --- a/database/factories/PurchaseOrderModelFactory.php +++ /dev/null @@ -1,43 +0,0 @@ - - */ -class PurchaseOrderModelFactory extends Factory -{ - protected $model = PurchaseOrderModel::class; - - private static int $sequence = 0; - - public function definition(): array - { - $seq = ++self::$sequence; - - return [ - 'purchase_no' => 'PO' . str_pad((string) $seq, 12, '0', STR_PAD_LEFT), - 'purchase_date' => now()->toDateString(), - 'status' => PurchaseOrderModel::STATUS_PENDING, - 'total_quantity' => 0, - 'total_weight' => 0, - 'estimate_amount' => 0, - 'actual_amount' => 0, - 'operator_id' => 0, - 'remark' => '', - ]; - } - - /** - * 指定采购日期 - */ - public function onDate(string $date): static - { - return $this->state(fn () => ['purchase_date' => $date]); - } -} diff --git a/database/factories/StoreModelFactory.php b/database/factories/StoreModelFactory.php deleted file mode 100644 index d579fd1..0000000 --- a/database/factories/StoreModelFactory.php +++ /dev/null @@ -1,51 +0,0 @@ - - */ -class StoreModelFactory extends Factory -{ - protected $model = StoreModel::class; - - private static int $sequence = 0; - - public function definition(): array - { - $seq = ++self::$sequence; - - return [ - 'name' => '测试门店' . $seq, - 'code' => 'S' . str_pad((string) $seq, 6, '0', STR_PAD_LEFT), - 'level_id' => 0, - 'contact' => '联系人' . $seq, - 'phone' => '138' . str_pad((string) $seq, 8, '0', STR_PAD_LEFT), - 'address' => '测试地址' . $seq . '号', - 'payment_cycle_days' => $seq % 8, - 'status' => StoreModel::STATUS_NORMAL, - 'remark' => '', - ]; - } - - /** - * 停用门店 - */ - public function disabled(): static - { - return $this->state(fn () => ['status' => StoreModel::STATUS_DISABLED]); - } - - /** - * 指定回款周期(天) - */ - public function paymentCycle(int $days): static - { - return $this->state(fn () => ['payment_cycle_days' => $days]); - } -} diff --git a/database/factories/StoreOrderItemModelFactory.php b/database/factories/StoreOrderItemModelFactory.php deleted file mode 100644 index 75bc2f2..0000000 --- a/database/factories/StoreOrderItemModelFactory.php +++ /dev/null @@ -1,42 +0,0 @@ - - */ -class StoreOrderItemModelFactory extends Factory -{ - protected $model = StoreOrderItemModel::class; - - public function definition(): array - { - return [ - 'order_id' => 0, - 'store_id' => 0, - 'product_id' => 0, - 'product_name' => '测试商品', - 'product_spec' => '500g/袋', - 'price' => number_format(random_int(100, 5000) / 100, 2, '.', ''), - 'quantity' => number_format(random_int(100, 10000) / 100, 2, '.', ''), - 'weight' => 0, - 'amount' => 0, - 'remark' => '', - ]; - } - - public function configure(): static - { - return $this->afterMaking(function (StoreOrderItemModel $item): void { - if ((float) $item->amount === 0.0 && (float) $item->price > 0 && (float) $item->quantity > 0) { - $item->amount = bcmul((string) $item->price, (string) $item->quantity, 2); - } - }); - } -} diff --git a/database/factories/StoreOrderModelFactory.php b/database/factories/StoreOrderModelFactory.php deleted file mode 100644 index 68a32e5..0000000 --- a/database/factories/StoreOrderModelFactory.php +++ /dev/null @@ -1,50 +0,0 @@ - - */ -class StoreOrderModelFactory extends Factory -{ - protected $model = StoreOrderModel::class; - - private static int $sequence = 0; - - public function definition(): array - { - $seq = ++self::$sequence; - - return [ - 'order_no' => 'SO' . str_pad((string) $seq, 12, '0', STR_PAD_LEFT), - 'store_id' => 0, - 'order_date' => now()->toDateString(), - 'total_quantity' => 0, - 'total_weight' => 0, - 'total_amount' => 0, - 'status' => StoreOrderModel::STATUS_PENDING, - 'remark' => '', - ]; - } - - /** - * 指定订货日期 - */ - public function onDate(string $date): static - { - return $this->state(fn () => ['order_date' => $date]); - } - - /** - * 已汇总(已被采购单归集) - */ - public function summarized(): static - { - return $this->state(fn () => ['status' => StoreOrderModel::STATUS_SUMMARIZED]); - } -} diff --git a/database/factories/SupplierModelFactory.php b/database/factories/SupplierModelFactory.php deleted file mode 100644 index 07a6309..0000000 --- a/database/factories/SupplierModelFactory.php +++ /dev/null @@ -1,43 +0,0 @@ - - */ -class SupplierModelFactory extends Factory -{ - protected $model = SupplierModel::class; - - private const MAIN_PRODUCTS = ['蔬菜', '水果', '蔬菜/水果', '肉禽蛋']; - - private static int $sequence = 0; - - public function definition(): array - { - $seq = ++self::$sequence; - - return [ - 'name' => '测试供应商' . $seq, - 'contact' => '联系人' . $seq, - 'phone' => '139' . str_pad((string) $seq, 8, '0', STR_PAD_LEFT), - 'address' => '供应商地址' . $seq . '号', - 'main_products' => self::MAIN_PRODUCTS[$seq % count(self::MAIN_PRODUCTS)], - 'status' => SupplierModel::STATUS_NORMAL, - 'remark' => '', - ]; - } - - /** - * 停用供应商 - */ - public function disabled(): static - { - return $this->state(fn () => ['status' => SupplierModel::STATUS_DISABLED]); - } -} diff --git a/database/factories/UserModelFactory.php b/database/factories/UserModelFactory.php deleted file mode 100644 index b3d5115..0000000 --- a/database/factories/UserModelFactory.php +++ /dev/null @@ -1,69 +0,0 @@ - - */ -class UserModelFactory extends Factory -{ - protected $model = UserModel::class; - - private static int $sequence = 0; - - public function definition(): array - { - $seq = ++self::$sequence; - - return [ - 'username' => null, - 'password' => null, - 'nickname' => '微信用户' . $seq, - 'email' => '', - 'openid' => 'openid_' . str_pad((string) $seq, 16, '0', STR_PAD_LEFT), - 'unionid' => '', - 'phone' => '', - 'avatar' => '', - 'type' => UserModel::TYPE_PENDING, - 'store_id' => 0, - 'supplier_id' => 0, - 'status' => UserModel::STATUS_NORMAL, - 'last_login_at' => null, - ]; - } - - /** - * 已绑定门店的门店用户 - */ - public function forStore(int $storeId): static - { - return $this->state(fn () => [ - 'type' => UserModel::TYPE_STORE, - 'store_id' => $storeId, - ]); - } - - /** - * 已绑定供应商的供应商用户 - */ - public function forSupplier(int $supplierId): static - { - return $this->state(fn () => [ - 'type' => UserModel::TYPE_SUPPLIER, - 'supplier_id' => $supplierId, - ]); - } - - /** - * 停用账号 - */ - public function disabled(): static - { - return $this->state(fn () => ['status' => UserModel::STATUS_DISABLED]); - } -} diff --git a/database/migrations/2026_07_23_030608_create_store_table.php b/database/migrations/2026_07_23_030608_create_store_table.php index c1ff494..042d140 100644 --- a/database/migrations/2026_07_23_030608_create_store_table.php +++ b/database/migrations/2026_07_23_030608_create_store_table.php @@ -16,7 +16,7 @@ return new class extends Migration if (! Schema::hasTable('customer_level')) { Schema::create('customer_level', function (Blueprint $table) { $table->increments('id')->comment('等级ID'); - $table->integer('icon')->nullable()->comment('等级图标ID'); + $table->integer('icon_id')->nullable()->comment('等级图标ID'); $table->string('name', 50)->comment('等级名称(如:一级客户、二级客户)'); $table->integer('sort')->default(0)->comment('排序'); $table->integer('status')->default(1)->comment('状态(1正常 0停用)'); diff --git a/database/migrations/2026_07_23_030609_create_product_table.php b/database/migrations/2026_07_23_030609_create_product_table.php index b346b8c..2732f3f 100644 --- a/database/migrations/2026_07_23_030609_create_product_table.php +++ b/database/migrations/2026_07_23_030609_create_product_table.php @@ -17,7 +17,7 @@ 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')->nullable()->comment('商品图标'); + $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停用)'); @@ -36,8 +36,8 @@ return new class extends Migration $table->string('name', 100)->comment('品名'); $table->string('spec', 100)->default('')->comment('规格/包规'); $table->string('unit', 20)->default('斤')->comment('计价单位(斤/件/箱等)'); - $table->string('images', 255)->default('')->comment('商品图片'); - $table->text('content')->default('')->comment('商品图文详情'); + $table->string('image_ids', 255)->default('')->comment('商品图片'); + $table->text('content')->comment('商品图文详情'); $table->integer('sort')->default(0)->comment('排序'); $table->integer('shelf_life')->default(0)->comment('保质期'); $table->integer('stock')->default(0)->comment('库存'); diff --git a/database/migrations/2026_07_23_030611_create_purchase_table.php b/database/migrations/2026_07_23_030611_create_purchase_table.php index 9f6a1b2..f274925 100644 --- a/database/migrations/2026_07_23_030611_create_purchase_table.php +++ b/database/migrations/2026_07_23_030611_create_purchase_table.php @@ -47,6 +47,7 @@ return new class extends Migration $table->integer('sort')->default(0)->comment('排序(导出用)'); $table->integer('is_sent')->default(0)->comment('是否已发送供应商(1已发送 0未发送)'); $table->timestamp('sent_at')->nullable()->comment('发送时间'); + $table->timestamp('supplier_confirmed_at')->nullable()->comment('供应商确认接单时间(NULL未确认)'); $table->string('remark', 255)->default('')->comment('备注'); $table->timestamps(); $table->index(['purchase_id'], 'purchase_order_item_purchase_index'); diff --git a/database/migrations/2026_07_23_120000_add_supplier_confirmed_at_to_purchase_order_item_table.php b/database/migrations/2026_07_23_120000_add_supplier_confirmed_at_to_purchase_order_item_table.php deleted file mode 100644 index 7ab2b34..0000000 --- a/database/migrations/2026_07_23_120000_add_supplier_confirmed_at_to_purchase_order_item_table.php +++ /dev/null @@ -1,32 +0,0 @@ -timestamp('supplier_confirmed_at')->nullable()->after('sent_at')->comment('供应商确认接单时间(NULL未确认)'); - }); - } - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - if (Schema::hasColumn('purchase_order_item', 'supplier_confirmed_at')) { - Schema::table('purchase_order_item', function (Blueprint $table) { - $table->dropColumn('supplier_confirmed_at'); - }); - } - } -}; diff --git a/database/seeders/PermissionSeeder.php b/database/seeders/PermissionSeeder.php index f999068..f357dff 100644 --- a/database/seeders/PermissionSeeder.php +++ b/database/seeders/PermissionSeeder.php @@ -368,6 +368,7 @@ class PermissionSeeder extends Seeder ['type' => 'rule', 'name' => '编辑文件夹', 'key' => 'system.file.group.update'], ['type' => 'rule', 'name' => '删除文件夹', 'key' => 'system.file.group.delete'], ['type' => 'rule', 'name' => '查询文件列表', 'key' => 'system.file.list.query'], + ['type' => 'rule', 'name' => '查看文件详情', 'key' => 'system.file.list.show'], ['type' => 'rule', 'name' => '上传文件', 'key' => 'system.file.list.upload'], ['type' => 'rule', 'name' => '下载文件', 'key' => 'system.file.list.download'], ['type' => 'rule', 'name' => '删除文件', 'key' => 'system.file.list.delete'], diff --git a/database/seeders/SysDataSeeder.php b/database/seeders/SysDataSeeder.php index 261a68f..0c13f07 100644 --- a/database/seeders/SysDataSeeder.php +++ b/database/seeders/SysDataSeeder.php @@ -56,8 +56,10 @@ class SysDataSeeder extends Seeder ['id' => 5, 'name' => '系统附件', 'sort' => 4, 'describe' => '系统附件分组', 'created_at' => $date, 'updated_at' => $date], ['id' => 6, 'name' => '其他文件', 'sort' => 5, 'describe' => '其他文件分组', 'created_at' => $date, 'updated_at' => $date], ['id' => 7, 'name' => '临时文件', 'sort' => 6, 'describe' => '临时文件分组,用于存放临时上传的文件', 'created_at' => $date, 'updated_at' => $date], - ['id' => 8, 'name' => '等级图片', 'sort' => 7, 'describe' => '存放等级图片', 'created_at' => $date, 'updated_at' => $date], - ['id' => 9, 'name' => '分类图片', 'sort' => 8, 'describe' => '存放商品分类图片', 'created_at' => $date, 'updated_at' => $date], + ['id' => 8, 'name' => '等级图片', 'sort' => 7, 'describe' => '存放等级图标', 'created_at' => $date, 'updated_at' => $date], + ['id' => 9, 'name' => '分类图片', 'sort' => 8, 'describe' => '存放商品分类图标', 'created_at' => $date, 'updated_at' => $date], + ['id' => 10, 'name' => '商品封面图片', 'sort' => 9, 'describe' => '存放商品封面图片', 'created_at' => $date, 'updated_at' => $date], + ['id' => 11, 'name' => '商品详情图片', 'sort' => 10, 'describe' => '存放商品详情图片', 'created_at' => $date, 'updated_at' => $date], ]); } } diff --git a/modules/SystemTool/Http/Controllers/SysFileController.php b/modules/SystemTool/Http/Controllers/SysFileController.php index cb58cc5..12973bb 100644 --- a/modules/SystemTool/Http/Controllers/SysFileController.php +++ b/modules/SystemTool/Http/Controllers/SysFileController.php @@ -45,6 +45,14 @@ class SysFileController extends BaseController return $this->success($data); } + /** 获取文件详情 */ + #[GetRoute('/show/{id}', 'show', where: ['id' => '[0-9]+'])] + public function show(int $id): JsonResponse + { + $file = SysFileModel::findOrFail($id)->toArray(); + return $this->success($file); + } + /** 上传文件 */ #[PostRoute('/upload', 'upload')] public function uploadImage(Request $request): JsonResponse diff --git a/web/api/product/category.ts b/web/api/product/category.ts index 1046b5c..18c734f 100644 --- a/web/api/product/category.ts +++ b/web/api/product/category.ts @@ -1,9 +1,10 @@ import createAxios from '@/utils/request'; import type IProductCategory from '@/domain/iProductCategory.ts'; +import type { IProductCategoryTree } from '@/domain/iProductCategory.ts'; /** 分类级联树(商品表单分类下拉、对账筛选用,仅启用分类) */ export async function getCategoryTree() { - return createAxios({ + return createAxios({ url: '/product/category/tree', method: 'get', }); diff --git a/web/api/system/sysFile.ts b/web/api/system/sysFile.ts index 490f00b..56f0d45 100644 --- a/web/api/system/sysFile.ts +++ b/web/api/system/sysFile.ts @@ -19,6 +19,17 @@ export function getFileList(params: FileListParams) { }); } +/** + * 获取文件详情 + * @param id 文件ID + */ +export function getFileInfo(id: number) { + return createAxios({ + url: `/system/file/list/show/${id}`, + method: 'get', + }); +} + /** * 获取回收站文件列表 * @param params 查询参数 diff --git a/web/components/XinFormField/ImageUploader/index.tsx b/web/components/XinFormField/ImageUploader/index.tsx index 45695b5..18edb6e 100644 --- a/web/components/XinFormField/ImageUploader/index.tsx +++ b/web/components/XinFormField/ImageUploader/index.tsx @@ -6,6 +6,7 @@ import type { UploadFile, UploadProps } from 'antd'; import type { RcFile } from 'antd/es/upload'; import type { ImageUploaderProps } from './typings'; import type { ISysFileInfo } from '@/domain/iSysFile'; +import { getFileInfo } from '@/api/system/sysFile'; import { useTranslation } from 'react-i18next'; /** @@ -51,8 +52,29 @@ const ImageUploader: React.FC = ({ return; } const valueArray = Array.isArray(value) ? value : [value]; - const newFileList: UploadFile[] = valueArray.map(fileToList); - setFileList(newFileList); + // changeType='id' 时表单值为文件 id:拉取文件信息回显; + if (changeType === 'id') { + const ids = valueArray as number[]; + + const idSet = new Set(fileList.map(obj => Number(obj.uid))); + + // 过滤出不在 Set 中的数字 + const missing = ids.filter(num => !idSet.has(Number(num))); + + if (missing.length > 0) { + const fetchers = missing.map((id) => getFileInfo(id)); + Promise.all(fetchers).then((resList) => { + const files = resList + .map((res) => res.data.data) + .filter(i => !!i) + .map(fileToList); + setFileList([...files, ...fileList]); + }) + } + } else { + const newFileList: UploadFile[] = (valueArray as ISysFileInfo[]).map(fileToList); + setFileList(newFileList); + } }, [value]); // 上传前校验 @@ -106,8 +128,16 @@ const ImageUploader: React.FC = ({ // 处理文件列表变化 const handleChange: UploadProps['onChange'] = ({ fileList: newFileList }) => { - // 如果所有文件都被删除 - if (newFileList.length === 0) return; + // 所有文件都被删除时,同步清空表单值,避免残留旧 id 被提交 + if (newFileList.length === 0) { + setFileList([]); + if (mode === 'single') { + onChange?.(null); + } else { + onChange?.([]); + } + return; + } setFileList(newFileList); // 全部上传完成格式化图片列表 if (newFileList.every((file) => file.status === 'done' || file.status === 'error')) { @@ -130,6 +160,7 @@ const ImageUploader: React.FC = ({ } // 上传失败的文件 const errorFiles = newFileList.filter((file) => file.status === 'error'); + setFileList([...uploadedFiles.map(fileToList), ...errorFiles]); } }; diff --git a/web/components/XinFormField/ImageUploader/typings.ts b/web/components/XinFormField/ImageUploader/typings.ts index 968c6e5..66c4301 100644 --- a/web/components/XinFormField/ImageUploader/typings.ts +++ b/web/components/XinFormField/ImageUploader/typings.ts @@ -15,7 +15,7 @@ export interface ImageUploaderProps { * - 单选模式:ISysFileInfo | null * - 多选模式:ISysFileInfo[] */ - value?: ISysFileInfo | ISysFileInfo[] | null; + value?: ISysFileInfo | ISysFileInfo[] | number | number[] | null; /** * 赋值类型 diff --git a/web/components/XinFormField/RichTextEditor/index.tsx b/web/components/XinFormField/RichTextEditor/index.tsx new file mode 100644 index 0000000..09f49d9 --- /dev/null +++ b/web/components/XinFormField/RichTextEditor/index.tsx @@ -0,0 +1,161 @@ +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { message, theme } from 'antd'; +import { Editor, Toolbar } from '@wangeditor/editor-for-react'; +import type { IDomEditor, IEditorConfig, IToolbarConfig } from '@wangeditor/editor'; +import '@wangeditor/editor/dist/css/style.css'; +import { uploadFile } from '@/api/system/sysFile'; +import type { ISysFileInfo } from '@/domain/iSysFile'; +import { useTranslation } from 'react-i18next'; +import type { RichTextEditorProps } from './typings'; + +/** + * 富文本编辑器(wangEditor v5) + * - 自定义图片上传:复用系统文件上传接口,校验参照 ImageUploader + * - 隐藏视频上传、表情、附件等复杂功能,仅保留基础排版与图片 + */ +const RichTextEditor: React.FC = ({ + value, + onChange, + disabled = false, + height = 400, + placeholder, + groupId = 0, + maxSize = 5, +}) => { + const { t } = useTranslation(); + const { token } = theme.useToken(); + const [editor, setEditor] = useState(null); + + // 自定义图片上传(校验逻辑参照 ImageUploader 组件) + const customUpload = useCallback( + async (file: File, insertFn: (src: string, alt: string, href: string) => void) => { + // 1. 文件类型校验 + if (!file.type.startsWith('image/')) { + message.error(t('xin.form.richText.error.notImage')); + return; + } + + // 2. 文件大小校验 + if (file.size / 1024 / 1024 > maxSize) { + message.error(t('xin.form.richText.error.sizeExceeded', { maxSize })); + return; + } + + // 3. 调用系统文件上传接口 + try { + const res = await uploadFile(file, groupId); + const info = res.data.data as ISysFileInfo; + insertFn(info.preview_url || info.file_url || '', info.file_name || '', ''); + } catch { + message.error(t('xin.form.richText.error.uploadFailed')); + } + }, + [t, groupId, maxSize] + ); + + // 工具栏配置:只保留基础排版 + 图片,隐藏视频/表情/附件等复杂功能 + const toolbarConfig: Partial = useMemo( + () => ({ + toolbarKeys: [ + 'headerSelect', + 'blockquote', + '|', + 'bold', + 'underline', + 'italic', + 'through', + '|', + 'color', + 'bgColor', + '|', + 'bulletedList', + 'numberedList', + 'todo', + '|', + 'justifyLeft', + 'justifyCenter', + 'justifyRight', + '|', + 'insertLink', + 'uploadImage', + 'insertTable', + '|', + 'undo', + 'redo', + 'fullScreen', + ], + }), + [] + ); + + // 编辑器配置 + const editorConfig: Partial = useMemo( + () => ({ + placeholder, + MENU_CONF: { + uploadImage: { + // 自定义上传:覆盖默认的服务端上传方式 + customUpload, + allowedFileTypes: ['image/*'], + maxFileSize: maxSize * 1024 * 1024, + // 禁用 base64 插入,粘贴/拖拽图片一律走自定义上传 + base64LimitSize: 0, + }, + }, + }), + [placeholder, customUpload, maxSize] + ); + + // 禁用/启用编辑器 + useEffect(() => { + if (editor == null) return; + if (disabled) { + editor.disable(); + } else { + editor.enable(); + } + }, [editor, disabled]); + + // 组件卸载时销毁编辑器实例 + useEffect( + () => () => { + if (editor == null) return; + editor.destroy(); + setEditor(null); + }, + [editor] + ); + + return ( +
+ {!disabled && ( + + )} + onChange?.(ed.getHtml())} + mode="default" + style={{ + height, + overflowY: 'hidden', + background: disabled ? token.colorBgLayout : undefined, + }} + /> +
+ ); +}; + +export default RichTextEditor; diff --git a/web/components/XinFormField/RichTextEditor/typings.ts b/web/components/XinFormField/RichTextEditor/typings.ts new file mode 100644 index 0000000..d2c60a6 --- /dev/null +++ b/web/components/XinFormField/RichTextEditor/typings.ts @@ -0,0 +1,44 @@ +/** + * 富文本编辑器组件属性 + */ +export interface RichTextEditorProps { + /** + * 当前值(HTML 字符串) + */ + value?: string; + + /** + * 值变化回调 + * @param value HTML 字符串 + */ + onChange?: (value?: string) => void; + + /** + * 是否禁用 + * @default false + */ + disabled?: boolean; + + /** + * 编辑器高度 (px) + * @default 400 + */ + height?: number; + + /** + * 占位符 + */ + placeholder?: string; + + /** + * 上传文件分组 ID + * @default 0 + */ + groupId?: number; + + /** + * 图片大小限制 (MB) + * @default 5 + */ + maxSize?: number; +} diff --git a/web/domain/iCustomerLevel.ts b/web/domain/iCustomerLevel.ts index 01c5aae..df7460d 100644 --- a/web/domain/iCustomerLevel.ts +++ b/web/domain/iCustomerLevel.ts @@ -1,3 +1,5 @@ +import type {ISysFileInfo} from "@/domain/iSysFile.ts"; + /** 客户等级 */ export default interface ICustomerLevel { id?: number; @@ -5,7 +7,8 @@ export default interface ICustomerLevel { name?: string; sort?: number; status?: number; - icon?: number; + icon_id?: number; + icon?: ISysFileInfo; icon_url?: string; created_at?: string; updated_at?: string; diff --git a/web/domain/iProduct.ts b/web/domain/iProduct.ts index 58f4ad0..632bac1 100644 --- a/web/domain/iProduct.ts +++ b/web/domain/iProduct.ts @@ -1,5 +1,6 @@ import type IProductCategory from '@/domain/iProductCategory.ts'; import type ISupplier from '@/domain/iSupplier.ts'; +import type {ISysFileInfo} from "@/domain/iSysFile.ts"; /** 商品等级价格行 */ export interface IProductPrice { @@ -12,24 +13,40 @@ export interface IProductPrice { /** 商品档案 */ export default interface IProduct { + /** 商品ID */ id?: number; + /** 分类ID */ category_id?: number; + /** 供应商ID */ supplier_id?: number; + /** 商品名称 */ name?: string; /** 规格/包规 */ spec?: string; - /** 商品等级 */ - grade?: string; /** 计价单位 */ unit?: string; - image?: string; + /** 封面 */ + image_ids?: string; + images_arr?: ISysFileInfo[]; + /** 商品图文详情(富文本 HTML) */ + content?: string; + /** 排序 */ sort?: number; + /** 保质期 */ + shelf_life?: number; + /** 库存 */ + stock?: number; + /** 状态 */ status?: number; + /** 描述 */ remark?: string; + /** 分类关联数据 */ category?: IProductCategory; + /** 供应商关联数据 */ supplier?: ISupplier; /** 多等级价格 */ prices?: IProductPrice[]; + /** 创建时间 */ created_at?: string; } diff --git a/web/domain/iProductCategory.ts b/web/domain/iProductCategory.ts index b204f0a..469f958 100644 --- a/web/domain/iProductCategory.ts +++ b/web/domain/iProductCategory.ts @@ -1,3 +1,5 @@ +import type {ISysFileInfo} from "@/domain/iSysFile.ts"; + /** 商品分类(多级,children 由后端组装) */ export default interface IProductCategory { id?: number; @@ -5,12 +7,21 @@ export default interface IProductCategory { name?: string; sort?: number; status?: number; - icon?: number; + icon_id?: number; + icon?: ISysFileInfo; icon_url?: string; children?: IProductCategory[]; created_at?: string; } +/** 商品分类(多级,children 由后端组装) */ +export interface IProductCategoryTree { + id?: number; + parent_id?: number; + name?: string; + children?: IProductCategoryTree[]; +} + export const CATEGORY_STATUS_MAP: Record = { 0: { text: '停用', color: 'error' }, 1: { text: '正常', color: 'success' }, diff --git a/web/pages/customer/level.tsx b/web/pages/customer/level.tsx index c747ffa..b48f382 100644 --- a/web/pages/customer/level.tsx +++ b/web/pages/customer/level.tsx @@ -38,7 +38,7 @@ const CustomerLevelPage: React.FC = () => { }, { title: '图片等级', - dataIndex: 'icon', + dataIndex: 'icon_id', valueType: 'image', fieldProps: { action: '/customer/level/upload', diff --git a/web/pages/product/category.tsx b/web/pages/product/category.tsx index a28ad71..d80bba6 100644 --- a/web/pages/product/category.tsx +++ b/web/pages/product/category.tsx @@ -1,11 +1,12 @@ -import React, { useState } from 'react'; +import React, {useEffect, useState} from 'react'; import {Button, Image, Tag, Typography} from 'antd'; import {NodeExpandOutlined} from '@ant-design/icons'; import XinTable from '@/components/XinTable'; import type { XinTableColumn, XinTableProps } from '@/components/XinTable/typings.ts'; import type IProductCategory from '@/domain/iProductCategory.ts'; +import type { IProductCategoryTree } from '@/domain/iProductCategory.ts'; import { CATEGORY_STATUS_MAP } from '@/domain/iProductCategory.ts'; -import { getCategoryTable } from '@/api/product/category.ts'; +import {getCategoryTable, getCategoryTree} from '@/api/product/category.ts'; const { Title, Text } = Typography; @@ -34,7 +35,11 @@ function collectAllIds(nodes: IProductCategory[]): number[] { const ProductCategoryPage: React.FC = () => { const [expandedKeys, setExpandedKeys] = useState([]); const [allIds, setAllIds] = useState([]); - const [categoryTree, setCategoryTree] = useState([]); + const [categoryTree, setCategoryTree] = useState([]); + + useEffect(() => { + getCategoryTree().then((res) => setCategoryTree(res.data.data ?? [])); + }, []); const columns: XinTableColumn[] = [ { @@ -63,6 +68,7 @@ const ProductCategoryPage: React.FC = () => { dataIndex: 'sort', valueType: 'digit', hideInSearch: true, + initialValue: 0, fieldProps: { min: 0 }, align: 'center', }, @@ -86,7 +92,7 @@ const ProductCategoryPage: React.FC = () => { }, { title: '分类图标', - dataIndex: 'icon', + dataIndex: 'icon_id', valueType: 'image', fieldProps: { action: '/product/category/upload', @@ -100,8 +106,8 @@ const ProductCategoryPage: React.FC = () => { return ( ); @@ -134,7 +140,6 @@ const ProductCategoryPage: React.FC = () => { handleRequest: async () => { const res = await getCategoryTable(); const tree = res.data.data ?? []; - setCategoryTree(tree); setAllIds(collectAllIds(tree)); return { data: tree, total: tree.length }; }, diff --git a/web/pages/product/goods.tsx b/web/pages/product/goods.tsx index 30fccb7..f5715f5 100644 --- a/web/pages/product/goods.tsx +++ b/web/pages/product/goods.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react'; import { Button, Drawer, - Form, + Form, Image, Input, InputNumber, message, @@ -21,7 +21,7 @@ import type IProduct from '@/domain/iProduct.ts'; import type { IBatchPriceUpdate, IPriceMatrixRow } from '@/domain/iProduct.ts'; import { PRODUCT_STATUS_MAP } from '@/domain/iProduct.ts'; import type ICustomerLevel from '@/domain/iCustomerLevel.ts'; -import type IProductCategory from '@/domain/iProductCategory.ts'; +import type {IProductCategoryTree} from '@/domain/iProductCategory.ts'; import { getLevelOptions } from '@/api/customer/level.ts'; import { getSupplierOptions } from '@/api/customer/supplier.ts'; import type ISupplier from '@/domain/iSupplier.ts'; @@ -36,7 +36,7 @@ const { Title, Text } = Typography; const ProductGoodsPage: React.FC = () => { const [levels, setLevels] = useState([]); const [suppliers, setSuppliers] = useState([]); - const [categoryTree, setCategoryTree] = useState([]); + const [categoryTree, setCategoryTree] = useState([]); // ===== 价格矩阵抽屉 ===== const [matrixOpen, setMatrixOpen] = useState(false); @@ -161,22 +161,61 @@ const ProductGoodsPage: React.FC = () => { width: 70, align: 'center', }, + { + title: '商品图片', + dataIndex: 'image_ids', + valueType: 'image', + fieldProps: { + action: '/product/goods/upload', + maxWidth: 3000, + maxHeight: 3000, + maxSize: 10, + mode: 'multiple', + maxCount: 5, + changeType: "id" + }, + render: (_, record) => { + if (!record.images_arr || record.images_arr?.length <= 0) return '-'; + return ( + + {record.images_arr.map(i => ( + + ))} + + ); + }, + align: 'center', + hideInSearch: true, + colProps: { span: 24 }, + }, { title: '商品名称', dataIndex: 'name', valueType: 'text', colProps: { span: 24 }, - fieldProps: { - styles: { input: { height: 52, fontSize: 22 } } - }, required: true, rules: [{ required: true, message: '请输入商品名称' }], }, + { + title: '商品描述', + dataIndex: 'remark', + valueType: 'text', + hideInSearch: true, + hideInTable: true, + colProps: { span: 24 }, + }, { title: '规格/包规', dataIndex: 'spec', valueType: 'text', hideInSearch: true, + align: "center", }, { title: '单位', @@ -184,12 +223,14 @@ const ProductGoodsPage: React.FC = () => { valueType: 'text', hideInSearch: true, initialValue: '斤', + align: "center", }, { title: '分类', dataIndex: 'category_id', valueType: 'treeSelect', required: true, + align: "center", rules: [{ required: true, message: '请选择分类' }], fieldProps: { treeData: categoryTree, @@ -207,6 +248,7 @@ const ProductGoodsPage: React.FC = () => { dataIndex: 'supplier_id', valueType: 'select', hideInSearch: true, + align: "center", fieldProps: { options: suppliers.map((s) => ({ label: s.name, value: s.id })), showSearch: true, @@ -216,17 +258,33 @@ const ProductGoodsPage: React.FC = () => { }, render: (_, record) => record.supplier?.name ?? '-', }, + { + title: '图文详情', + dataIndex: 'content', + valueType: 'richText', + hideInSearch: true, + hideInTable: true, + colProps: { span: 24 }, + fieldProps: { + height: 400, + groupId: 11, + placeholder: '输入商品图文详情,支持插入图片', + }, + }, { title: '等级价格', dataIndex: 'prices', hideInForm: true, hideInSearch: true, + width: 370, + align: 'center', render: (_, record) => ( - + {record.prices?.length ? record.prices.map((p) => ( - - {p.level?.name ?? `等级${p.level_id}`} ¥{p.price} + + {p.level?.name ?? `等级${p.level_id}`} + ¥{p.price ?? '未设定'} )) : '-'} @@ -258,19 +316,12 @@ const ProductGoodsPage: React.FC = () => { }, align: 'center', }, - { - title: '备注', - dataIndex: 'remark', - valueType: 'textarea', - hideInSearch: true, - hideInTable: true, - fieldProps: { rows: 2 }, - }, { title: '等级价格设置', dataIndex: 'prices', hideInTable: true, hideInSearch: true, + colProps: { span: 24 }, fieldRender: () => ( {(fields, { add, remove }) => ( @@ -281,7 +332,7 @@ const ProductGoodsPage: React.FC = () => { {...restField} name={[name, 'level_id']} rules={[{ required: true, message: '请选择等级' }]} - className="!mb-0" + className="mb-0!" >