分类图片上传和用户等级图片上传

This commit is contained in:
liu
2026-08-05 13:32:50 +08:00
parent b296ea9bb5
commit 38338a8833
16 changed files with 218 additions and 52 deletions
@@ -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
@@ -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
@@ -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' => '请重新上传图片'
];
}
}
+26 -3
View File
@@ -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,
);
}
/**
* 该等级下的门店
*/
+27 -2
View File
@@ -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,
);
}
/**
* 父分类
*/
@@ -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('客户等级表');
});
@@ -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('父级分类ID0为顶级)');
$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');
+2
View File
@@ -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],
]);
}
}
+1
View File
@@ -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",
@@ -14,6 +14,7 @@ import { useTranslation } from 'react-i18next';
const ImageUploader: React.FC<ImageUploaderProps> = ({
action,
value,
changeType,
onChange,
mode = 'single',
disabled = false,
@@ -115,9 +116,17 @@ const ImageUploader: React.FC<ImageUploaderProps> = ({
.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');
@@ -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;
/**
* 上传模式
+2 -1
View File
@@ -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;
}
+2
View File
@@ -5,6 +5,8 @@ export default interface IProductCategory {
name?: string;
sort?: number;
status?: number;
icon?: number;
icon_url?: string;
children?: IProductCategory[];
created_at?: string;
}
+33 -5
View File
@@ -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 (
<Image
src={url}
width={60}
height={60}
style={{ objectFit: 'cover', borderRadius: 4 }}
/>
);
},
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 },
+60 -31
View File
@@ -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<number[]>([]);
const [categoryTree, setCategoryTree] = useState<IProductCategory[]>([]);
const loadTree = async () => {
const res = await getCategoryTable();
const tree = res.data.data ?? [];
setCategoryTree(tree);
setAllIds(collectAllIds(tree));
};
useEffect(() => {
loadTree();
}, []);
const columns: XinTableColumn<IProductCategory>[] = [
{
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 (
<Image
src={url}
width={60}
height={60}
style={{ objectFit: 'cover', borderRadius: 4 }}
/>
);
},
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<IProductCategory> = {
@@ -115,9 +143,22 @@ const ProductCategoryPage: React.FC = () => {
expandedRowKeys: expandedKeys,
onExpandedRowsChange: (keys) => setExpandedKeys([...keys] as number[]),
},
actionBarRender: (dom) => [
dom.add,
<Button
icon={<NodeExpandOutlined />}
onClick={() =>
setExpandedKeys(expandedKeys.length ? [] : allIds)
}
>
{expandedKeys.length ? '全部收起' : '全部展开'}
</Button>,
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 (
<>
<div className="mb-5 flex items-start justify-between">
<div>
<Title level={3}></Title>
<Text type="secondary">
//
</Text>
</div>
<Space>
<Button
icon={<NodeExpandOutlined />}
onClick={() =>
setExpandedKeys(expandedKeys.length ? [] : allIds)
}
>
{expandedKeys.length ? '全部收起' : '全部展开'}
</Button>
</Space>
<div className={"mb-5"}>
<Title level={3}></Title>
<Text type="secondary">
//
</Text>
</div>
<XinTable<IProductCategory> {...tableProps} />
</>
+2 -1
View File
@@ -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={
<Space>
<Button onClick={() => loadMatrix()}></Button>