diff --git a/app/Http/Controllers/Customer/CustomerLevelController.php b/app/Http/Controllers/Customer/CustomerLevelController.php index 71fe9ca..a72562c 100644 --- a/app/Http/Controllers/Customer/CustomerLevelController.php +++ b/app/Http/Controllers/Customer/CustomerLevelController.php @@ -7,12 +7,14 @@ use App\Http\Requests\Customer\CustomerLevelFormRequest; use App\Models\CustomerLevelModel; 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; /** * 客户等级管理(同一商品按客户等级定价) @@ -39,6 +41,20 @@ class CustomerLevelController 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'], + 8, + 20, + Auth::id() + ); + return $this->success($result); + } + /** 创建等级 */ #[PostRoute(authorize: 'create')] public function create(CustomerLevelFormRequest $request): JsonResponse diff --git a/app/Http/Controllers/Product/ProductCategoryController.php b/app/Http/Controllers/Product/ProductCategoryController.php index f79ffe2..1b194d0 100644 --- a/app/Http/Controllers/Product/ProductCategoryController.php +++ b/app/Http/Controllers/Product/ProductCategoryController.php @@ -7,12 +7,15 @@ 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; /** * 商品分类管理(多级分类:蔬菜/水果/其他) @@ -34,6 +37,20 @@ class ProductCategoryController extends BaseController return $this->success(ProductCategoryModel::getTreeData(onlyEnabled: 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 diff --git a/app/Http/Requests/Customer/CustomerLevelFormRequest.php b/app/Http/Requests/Customer/CustomerLevelFormRequest.php index 72a8aca..f2dd2cc 100644 --- a/app/Http/Requests/Customer/CustomerLevelFormRequest.php +++ b/app/Http/Requests/Customer/CustomerLevelFormRequest.php @@ -3,7 +3,9 @@ namespace App\Http\Requests\Customer; use Illuminate\Validation\Rule; +use Illuminate\Validation\Rules\Exists; use Modules\Common\Http\Requests\BaseFormRequest; +use Modules\SystemTool\Models\SysFileModel; /** * 客户等级 创建/编辑 验证 @@ -23,7 +25,7 @@ class CustomerLevelFormRequest extends BaseFormRequest 'name' => ['required', 'string', 'max:50', $unique], 'sort' => 'nullable|integer', 'status' => 'nullable|integer|in:0,1', - 'remark' => 'nullable|string|max:255', + 'icon' => ['nullable', 'integer', new Exists(SysFileModel::class, 'id')], ]; } @@ -34,6 +36,7 @@ class CustomerLevelFormRequest extends BaseFormRequest 'name.max' => '等级名称最长 50 个字符', 'name.unique' => '等级名称已存在', 'status.in' => '状态值不正确', + 'icon.exists' => '请重新上传图片' ]; } } diff --git a/app/Models/CustomerLevelModel.php b/app/Models/CustomerLevelModel.php index 60d7de2..eba2119 100644 --- a/app/Models/CustomerLevelModel.php +++ b/app/Models/CustomerLevelModel.php @@ -2,9 +2,12 @@ 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; +use Illuminate\Database\Eloquent\Relations\HasOne; +use Modules\SystemTool\Models\SysFileModel; /** * 客户等级模型(同一商品按客户等级定价) @@ -14,9 +17,9 @@ class CustomerLevelModel extends Model use HasFactory; /** 状态:停用 */ - public const STATUS_DISABLED = 0; + public const int STATUS_DISABLED = 0; /** 状态:正常 */ - public const STATUS_NORMAL = 1; + public const int STATUS_NORMAL = 1; protected $table = 'customer_level'; protected $primaryKey = 'id'; @@ -25,14 +28,34 @@ class CustomerLevelModel extends Model 'name', 'sort', 'status', - 'remark', + 'icon' ]; protected $casts = [ 'sort' => 'integer', 'status' => 'integer', + 'created_at' => 'datetime:Y-m-d H:i:s', + 'updated_at' => 'datetime:Y-m-d H:i:s', ]; + protected $appends = ['icon_url']; + + /** + * 关联图标 + */ + public function icon(): HasOne + { + return $this->hasOne(SysFileModel::class, 'id', 'icon'); + } + + // 图标链接 + public function icon_url(): Attribute + { + return Attribute::make( + get: fn () => $this->icon->preview_url, + ); + } + /** * 该等级下的门店 */ diff --git a/app/Models/ProductCategoryModel.php b/app/Models/ProductCategoryModel.php index 8b61895..b32aae4 100644 --- a/app/Models/ProductCategoryModel.php +++ b/app/Models/ProductCategoryModel.php @@ -2,9 +2,12 @@ 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; +use Illuminate\Database\Eloquent\Relations\HasOne; +use Modules\SystemTool\Models\SysFileModel; /** * 商品分类模型(蔬菜/水果/其他,多级分类自关联) @@ -12,9 +15,9 @@ use Illuminate\Database\Eloquent\Relations\HasMany; class ProductCategoryModel extends Model { /** 状态:停用 */ - public const STATUS_DISABLED = 0; + public const int STATUS_DISABLED = 0; /** 状态:正常 */ - public const STATUS_NORMAL = 1; + public const int STATUS_NORMAL = 1; protected $table = 'product_category'; protected $primaryKey = 'id'; @@ -24,14 +27,36 @@ class ProductCategoryModel extends Model 'name', 'sort', 'status', + 'icon' ]; protected $casts = [ 'parent_id' => 'integer', 'sort' => 'integer', 'status' => 'integer', + 'icon' => 'integer', + 'created_at' => 'datetime:Y-m-d H:i:s', + 'updated_at' => 'datetime:Y-m-d H:i:s', ]; + protected $appends = ['icon_url']; + + /** + * 关联图标 + */ + public function icon(): HasOne + { + return $this->hasOne(SysFileModel::class, 'id', 'icon'); + } + + // 图标链接 + public function icon_url(): Attribute + { + return Attribute::make( + get: fn () => $this->icon->preview_url, + ); + } + /** * 父分类 */ 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 a6194e2..c1ff494 100644 --- a/database/migrations/2026_07_23_030608_create_store_table.php +++ b/database/migrations/2026_07_23_030608_create_store_table.php @@ -16,10 +16,10 @@ 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->string('name', 50)->comment('等级名称(如:一级客户、二级客户)'); $table->integer('sort')->default(0)->comment('排序'); $table->integer('status')->default(1)->comment('状态(1正常 0停用)'); - $table->string('remark', 255)->default('')->comment('备注'); $table->timestamps(); $table->comment('客户等级表'); }); 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 097e4cd..2ebecf1 100644 --- a/database/migrations/2026_07_23_030609_create_product_table.php +++ b/database/migrations/2026_07_23_030609_create_product_table.php @@ -17,6 +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->string('name', 50)->comment('分类名称'); $table->integer('sort')->default(0)->comment('排序(采购单导出按此排序)'); $table->integer('status')->default(1)->comment('状态(1正常 0停用)'); @@ -34,10 +35,12 @@ return new class extends Migration $table->integer('supplier_id')->default(0)->comment('默认供应商ID'); $table->string('name', 100)->comment('品名'); $table->string('spec', 100)->default('')->comment('规格/包规'); - $table->string('grade', 50)->default('')->comment('商品等级'); $table->string('unit', 20)->default('斤')->comment('计价单位(斤/件/箱等)'); $table->string('image', 255)->default('')->comment('商品图片'); + $table->text('content')->default('')->comment('商品图文详情'); $table->integer('sort')->default(0)->comment('排序'); + $table->integer('shelf_life')->default(0)->comment('保质期'); + $table->integer('stock')->default(0)->comment('库存'); $table->integer('status')->default(1)->comment('状态(1上架 0下架)'); $table->string('remark', 255)->default('')->comment('备注'); $table->timestamps(); @@ -48,7 +51,7 @@ return new class extends Migration }); } - // 商品等级价格表(A2 价格策略:同一商品对不同客户等级显示不同单价) + // 商品等级价格表 if (! Schema::hasTable('product_price')) { Schema::create('product_price', function (Blueprint $table) { $table->increments('id')->comment('价格ID'); diff --git a/database/seeders/SysDataSeeder.php b/database/seeders/SysDataSeeder.php index a549cbd..261a68f 100644 --- a/database/seeders/SysDataSeeder.php +++ b/database/seeders/SysDataSeeder.php @@ -56,6 +56,8 @@ 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], ]); } } diff --git a/package.json b/package.json index 88ead0b..ad0ad81 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "dayjs": "^1.11.18", "echarts-for-react": "^3.0.2", "i18next": "^25.4.2", + "ifanrx-react-ueditor": "^2.3.2", "lodash": "^4.17.21", "react": "^19.1.0", "react-dom": "^19.1.0", diff --git a/web/components/XinFormField/ImageUploader/index.tsx b/web/components/XinFormField/ImageUploader/index.tsx index 0358a50..45695b5 100644 --- a/web/components/XinFormField/ImageUploader/index.tsx +++ b/web/components/XinFormField/ImageUploader/index.tsx @@ -14,6 +14,7 @@ import { useTranslation } from 'react-i18next'; const ImageUploader: React.FC = ({ action, value, + changeType, onChange, mode = 'single', disabled = false, @@ -115,9 +116,17 @@ const ImageUploader: React.FC = ({ .filter((file) => file.status === 'done' && file.response) .map((file) => file.response.data as ISysFileInfo); if( mode === 'single' ) { - onChange?.(uploadedFiles[0]); + if (changeType !== 'id') { + onChange?.(uploadedFiles[0]); + } else { + onChange?.(uploadedFiles[0].id); + } } else { - onChange?.(uploadedFiles); + if (changeType !== 'id') { + onChange?.(uploadedFiles); + } else { + onChange?.(uploadedFiles.map(item => item.id!)); + } } // 上传失败的文件 const errorFiles = newFileList.filter((file) => file.status === 'error'); diff --git a/web/components/XinFormField/ImageUploader/typings.ts b/web/components/XinFormField/ImageUploader/typings.ts index 1f6c1aa..968c6e5 100644 --- a/web/components/XinFormField/ImageUploader/typings.ts +++ b/web/components/XinFormField/ImageUploader/typings.ts @@ -17,11 +17,17 @@ export interface ImageUploaderProps { */ value?: ISysFileInfo | ISysFileInfo[] | null; + /** + * 赋值类型 + * 上传完成后赋值的类型。 + */ + changeType?: 'id' | 'object'; + /** * 上传完成回调 * @param value ISysFileInfo | ISysFileInfo[] | null */ - onChange?: (value: ISysFileInfo | ISysFileInfo[] | null) => void; + onChange?: (value?: ISysFileInfo | ISysFileInfo[] | number | number[] | null) => void; /** * 上传模式 @@ -65,9 +71,9 @@ export interface ImageUploaderProps { */ croppable?: boolean; - /** + /** * 剪裁参数 */ cropperOptions?: Omit; -} \ No newline at end of file +} diff --git a/web/domain/iCustomerLevel.ts b/web/domain/iCustomerLevel.ts index 2b249bd..01c5aae 100644 --- a/web/domain/iCustomerLevel.ts +++ b/web/domain/iCustomerLevel.ts @@ -5,7 +5,8 @@ export default interface ICustomerLevel { name?: string; sort?: number; status?: number; - remark?: string; + icon?: number; + icon_url?: string; created_at?: string; updated_at?: string; } diff --git a/web/domain/iProductCategory.ts b/web/domain/iProductCategory.ts index ec8e94b..b204f0a 100644 --- a/web/domain/iProductCategory.ts +++ b/web/domain/iProductCategory.ts @@ -5,6 +5,8 @@ export default interface IProductCategory { name?: string; sort?: number; status?: number; + icon?: number; + icon_url?: string; children?: IProductCategory[]; created_at?: string; } diff --git a/web/pages/customer/level.tsx b/web/pages/customer/level.tsx index feb5f68..c747ffa 100644 --- a/web/pages/customer/level.tsx +++ b/web/pages/customer/level.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Tag, Typography } from 'antd'; +import { Tag, Typography, Image } from 'antd'; import XinTable from '@/components/XinTable'; import type { XinTableColumn, XinTableProps } from '@/components/XinTable/typings.ts'; import type ICustomerLevel from '@/domain/iCustomerLevel.ts'; @@ -25,6 +25,7 @@ const CustomerLevelPage: React.FC = () => { dataIndex: 'name', valueType: 'text', required: true, + align: 'center', rules: [{ required: true, message: '请输入等级名称' }], }, { @@ -32,8 +33,34 @@ const CustomerLevelPage: React.FC = () => { dataIndex: 'sort', valueType: 'digit', hideInSearch: true, + initialValue: 0, fieldProps: { min: 0 }, }, + { + title: '图片等级', + dataIndex: 'icon', + valueType: 'image', + fieldProps: { + action: '/customer/level/upload', + mode: 'single', + maxCount: 1, + changeType: "id" + }, + render: (_, record: ICustomerLevel) => { + const url = record.icon_url + if (!url) return '-'; + return ( + + ); + }, + align: 'center', + hideInSearch: true, + }, { title: '状态', dataIndex: 'status', @@ -51,11 +78,11 @@ const CustomerLevelPage: React.FC = () => { }, }, { - title: '备注', - dataIndex: 'remark', - valueType: 'textarea', + title: '创建时间', + dataIndex: 'updated_at', + hideInForm: true, hideInSearch: true, - fieldProps: { rows: 2 }, + align: 'center', }, { title: '创建时间', @@ -74,6 +101,7 @@ const CustomerLevelPage: React.FC = () => { formProps: { grid: true, colProps: { span: 12 }, + rowProps: { gutter: 20 }, layout: 'vertical', }, modalProps: { width: 640 }, diff --git a/web/pages/product/category.tsx b/web/pages/product/category.tsx index 16ac337..a28ad71 100644 --- a/web/pages/product/category.tsx +++ b/web/pages/product/category.tsx @@ -1,6 +1,6 @@ -import React, { useEffect, useState } from 'react'; -import { Button, Space, Tag, Typography } from 'antd'; -import { NodeExpandOutlined } from '@ant-design/icons'; +import React, { 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'; @@ -36,17 +36,6 @@ const ProductCategoryPage: React.FC = () => { const [allIds, setAllIds] = useState([]); const [categoryTree, setCategoryTree] = useState([]); - const loadTree = async () => { - const res = await getCategoryTable(); - const tree = res.data.data ?? []; - setCategoryTree(tree); - setAllIds(collectAllIds(tree)); - }; - - useEffect(() => { - loadTree(); - }, []); - const columns: XinTableColumn[] = [ { title: '分类名称', @@ -95,6 +84,45 @@ const ProductCategoryPage: React.FC = () => { }, align: 'center', }, + { + title: '分类图标', + dataIndex: 'icon', + 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 ( + + ); + }, + align: 'center', + hideInSearch: true, + }, + { + title: '创建时间', + dataIndex: 'updated_at', + hideInForm: true, + hideInSearch: true, + align: 'center', + }, + { + title: '创建时间', + dataIndex: 'created_at', + hideInForm: true, + hideInSearch: true, + align: 'center', + }, ]; const tableProps: XinTableProps = { @@ -115,9 +143,22 @@ const ProductCategoryPage: React.FC = () => { expandedRowKeys: expandedKeys, onExpandedRowsChange: (keys) => setExpandedKeys([...keys] as number[]), }, + actionBarRender: (dom) => [ + dom.add, + , + dom.keywordSearch, + ], formProps: { grid: true, colProps: { span: 12 }, + rowProps: { gutter: 20 }, layout: 'vertical', }, modalProps: { width: 640 }, @@ -125,23 +166,11 @@ const ProductCategoryPage: React.FC = () => { return ( <> -
-
- 商品分类 - - 多级分类(如蔬菜/水果/其他),采购单导出与对账筛选按分类归组;有子分类或挂载商品时不可删除。 - -
- - - +
+ 商品分类 + + 多级分类(如蔬菜/水果/其他),采购单导出与对账筛选按分类归组;有子分类或挂载商品时不可删除。 +
{...tableProps} /> diff --git a/web/pages/product/goods.tsx b/web/pages/product/goods.tsx index 827cdf7..3af01d9 100644 --- a/web/pages/product/goods.tsx +++ b/web/pages/product/goods.tsx @@ -344,6 +344,7 @@ const ProductGoodsPage: React.FC = () => { formProps: { grid: true, colProps: { span: 12 }, + rowProps: { gutter: 20 }, layout: 'vertical', }, modalProps: { width: 800 }, @@ -363,7 +364,7 @@ const ProductGoodsPage: React.FC = () => { title="价格矩阵 · 批量调价" open={matrixOpen} onClose={() => setMatrixOpen(false)} - width={matrixLevels.length * 150 + 260} + size={800} extra={