前端打包

This commit is contained in:
liu
2026-08-09 11:15:38 +08:00
parent f67567bdc9
commit 9153c4eead
32 changed files with 1235 additions and 27 deletions
+26
View File
@@ -12,6 +12,8 @@ use Modules\AnnoRoute\Attribute\PostRoute;
use Modules\AnnoRoute\Attribute\RequestAttribute; use Modules\AnnoRoute\Attribute\RequestAttribute;
use Modules\Common\Trait\RequestJson; use Modules\Common\Trait\RequestJson;
use Modules\SystemTool\Models\SysCarouselModel; use Modules\SystemTool\Models\SysCarouselModel;
use Modules\SystemTool\Models\SysCategoryItemModel;
use Modules\SystemTool\Models\SysCategoryModel;
use Modules\SystemTool\Models\SysGridNavModel; use Modules\SystemTool\Models\SysGridNavModel;
#[RequestAttribute('/api', authGuard: 'users')] #[RequestAttribute('/api', authGuard: 'users')]
@@ -81,4 +83,28 @@ class IndexController
return $this->success(compact('carousel', 'gridNav', 'site_title')); return $this->success(compact('carousel', 'gridNav', 'site_title'));
} }
/**
* 获取分类及分类下所有链接(公开接口)
* 前端点击轮播图/导航时,若 link_type 为分类则跳转分类项页,调用此接口
*/
#[GetRoute('/category/{id}', false, where: ['id' => '[0-9]+'])]
public function category(int $id): JsonResponse
{
$category = SysCategoryModel::query()
->where('status', 0)
->find($id);
if (empty($category)) {
return $this->error('分类不存在');
}
$items = SysCategoryItemModel::query()
->where('category_id', $id)
->where('status', 0)
->orderBy('sort', 'desc')
->orderBy('id', 'desc')
->get();
return $this->success($items->toArray());
}
} }
@@ -16,7 +16,7 @@ return new class extends Migration
Schema::create('sys_carousel', function (Blueprint $table) { Schema::create('sys_carousel', function (Blueprint $table) {
$table->increments('id'); $table->increments('id');
$table->string('title', 100)->comment('轮播图标题'); $table->string('title', 100)->comment('轮播图标题');
$table->integer('image_id')->comment('轮播图图片信息JSON'); $table->integer('image_id')->comment('轮播图图片信息');
$table->string('link', 500)->nullable()->default('')->comment('跳转链接'); $table->string('link', 500)->nullable()->default('')->comment('跳转链接');
$table->unsignedTinyInteger('status')->default(0)->comment('状态:0=启用,1=禁用'); $table->unsignedTinyInteger('status')->default(0)->comment('状态:0=启用,1=禁用');
$table->integer('sort')->default(0)->comment('排序(数字越大越靠前)'); $table->integer('sort')->default(0)->comment('排序(数字越大越靠前)');
@@ -30,7 +30,7 @@ return new class extends Migration
Schema::create('sys_grid_nav', function (Blueprint $table) { Schema::create('sys_grid_nav', function (Blueprint $table) {
$table->increments('id'); $table->increments('id');
$table->string('title', 100)->comment('导航标题'); $table->string('title', 100)->comment('导航标题');
$table->integer('image_id')->comment('导航图标信息JSON'); $table->integer('image_id')->comment('导航图标信息');
$table->string('link', 500)->nullable()->default('')->comment('跳转链接'); $table->string('link', 500)->nullable()->default('')->comment('跳转链接');
$table->unsignedTinyInteger('status')->default(0)->comment('状态:0=启用,1=禁用'); $table->unsignedTinyInteger('status')->default(0)->comment('状态:0=启用,1=禁用');
$table->integer('sort')->default(0)->comment('排序(数字越大越靠前)'); $table->integer('sort')->default(0)->comment('排序(数字越大越靠前)');
@@ -0,0 +1,162 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
// 首页分类表
if (! Schema::hasTable('sys_category')) {
Schema::create('sys_category', function (Blueprint $table) {
$table->increments('id');
$table->string('name', 100)->comment('分类名称');
$table->unsignedTinyInteger('status')->default(0)->comment('状态:0=启用,1=禁用');
$table->integer('sort')->default(0)->comment('排序(数字越大越靠前)');
$table->timestamps();
$table->comment('首页分类管理表');
});
}
// 首页分类项表(字段与宫格导航表一致)
if (! Schema::hasTable('sys_category_item')) {
Schema::create('sys_category_item', function (Blueprint $table) {
$table->increments('id');
$table->integer('category_id')->default(0)->comment('所属分类ID');
$table->string('title', 100)->comment('导航标题');
$table->integer('image_id')->comment('导航图标信息');
$table->string('link', 500)->nullable()->default('')->comment('跳转链接');
$table->unsignedTinyInteger('status')->default(0)->comment('状态:0=启用,1=禁用');
$table->integer('sort')->default(0)->comment('排序(数字越大越靠前)');
$table->timestamps();
$table->index('category_id', 'idx_category_item_category_id');
$table->comment('首页分类项管理表');
});
}
// 轮播图表:新增跳转类型与分类ID
if (Schema::hasTable('sys_carousel') && ! Schema::hasColumn('sys_carousel', 'link_type')) {
Schema::table('sys_carousel', function (Blueprint $table) {
$table->unsignedTinyInteger('link_type')->default(0)->comment('跳转类型:0=链接,1=分类');
$table->integer('category_id')->nullable()->default(0)->comment('分类IDlink_type=1 时生效)');
});
}
// 宫格导航表:新增跳转类型与分类ID
if (Schema::hasTable('sys_grid_nav') && ! Schema::hasColumn('sys_grid_nav', 'link_type')) {
Schema::table('sys_grid_nav', function (Blueprint $table) {
$table->unsignedTinyInteger('link_type')->default(0)->comment('跳转类型:0=链接,1=分类');
$table->integer('category_id')->nullable()->default(0)->comment('分类IDlink_type=1 时生效)');
});
}
$this->insertMenuRules();
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('sys_carousel', function (Blueprint $table) {
if (Schema::hasColumn('sys_carousel', 'link_type')) {
$table->dropColumn(['link_type', 'category_id']);
}
});
Schema::table('sys_grid_nav', function (Blueprint $table) {
if (Schema::hasColumn('sys_grid_nav', 'link_type')) {
$table->dropColumn(['link_type', 'category_id']);
}
});
Schema::dropIfExists('sys_category_item');
Schema::dropIfExists('sys_category');
$this->deleteMenuRules();
}
/**
* 向权限菜单表插入「分类管理」菜单(幂等,已存在则跳过)
*/
protected function insertMenuRules(): void
{
if (! Schema::hasTable('sys_rule')) {
return;
}
$exists = DB::table('sys_rule')->where('key', 'system.category')->exists();
if ($exists) {
return;
}
$date = date('Y-m-d H:i:s');
$menuId = DB::table('sys_rule')->insertGetId([
'parent_id' => 0,
'type' => 'route',
'key' => 'system.category',
'name' => '分类管理',
'path' => '/system/category',
'icon' => '',
'order' => 0,
'local' => 'menu.system.category',
'status' => 1,
'hidden' => 1,
'link' => 0,
'created_at' => $date,
'updated_at' => $date,
]);
$children = [
['name' => '查询分类列表', 'key' => 'system.category.query'],
['name' => '新增分类', 'key' => 'system.category.create'],
['name' => '编辑分类', 'key' => 'system.category.update'],
['name' => '删除分类', 'key' => 'system.category.delete'],
['name' => '分类项列表', 'key' => 'system.category.item.query'],
['name' => '分类项新增', 'key' => 'system.category.item.create'],
['name' => '分类项编辑', 'key' => 'system.category.item.update'],
['name' => '分类项删除', 'key' => 'system.category.item.delete'],
];
foreach ($children as $i => $child) {
DB::table('sys_rule')->insert([
'parent_id' => $menuId,
'type' => 'rule',
'key' => $child['key'],
'name' => $child['name'],
'path' => '',
'icon' => '',
'order' => $i,
'local' => '',
'status' => 1,
'hidden' => 1,
'link' => 0,
'created_at' => $date,
'updated_at' => $date,
]);
}
}
/**
* 删除「分类管理」菜单及其子规则
*/
protected function deleteMenuRules(): void
{
if (! Schema::hasTable('sys_rule')) {
return;
}
$menuId = DB::table('sys_rule')->where('key', 'system.category')->value('id');
if ($menuId) {
DB::table('sys_rule')->where('key', 'system.category')->delete();
DB::table('sys_rule')->where('parent_id', $menuId)->delete();
}
}
};
@@ -0,0 +1,87 @@
<?php
namespace Modules\SystemTool\Http\Controllers;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
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\Http\Requests\SysCategoryFormRequest;
use Modules\SystemTool\Models\SysCategoryModel;
/**
* 首页分类管理
*/
#[RequestAttribute('/system/category', 'system.category')]
class SysCategoryController extends BaseController
{
protected array $searchField = [
'status' => '=',
];
protected array $quickSearchField = ['name'];
/** 查询分类列表 */
#[GetRoute(authorize: 'query')]
public function query(Request $request): JsonResponse
{
$query = SysCategoryModel::query();
$data = $this->buildSearch($request->all(), $query)
->orderBy('sort', 'desc')
->orderBy('id', 'desc')
->paginate($request->input('pageSize', 10))
->toArray();
return $this->success($data);
}
/** 创建分类 */
#[PostRoute(authorize: 'create')]
public function create(SysCategoryFormRequest $request): JsonResponse
{
$validated = $request->validated();
$model = SysCategoryModel::create($validated);
if (empty($model)) {
return $this->error();
}
return $this->success();
}
/** 编辑分类 */
#[PutRoute(
route: '/{id}',
authorize: 'update',
where: ['id' => '[0-9]+']
)]
public function update(int $id, SysCategoryFormRequest $request): JsonResponse
{
$validated = $request->validated();
$model = SysCategoryModel::find($id);
if (empty($model)) {
return $this->error('分类不存在');
}
$model->update($validated);
return $this->success();
}
/** 删除分类 */
#[DeleteRoute(
route: '/{id}',
authorize: 'delete',
where: ['id' => '[0-9]+']
)]
public function delete(int $id): JsonResponse
{
$model = SysCategoryModel::find($id);
if (empty($model)) {
return $this->error('分类不存在');
}
// 级联删除该分类下的分类项
$model->categoryItems()->delete();
$model->delete();
return $this->success();
}
}
@@ -0,0 +1,86 @@
<?php
namespace Modules\SystemTool\Http\Controllers;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
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\Http\Requests\SysCategoryItemFormRequest;
use Modules\SystemTool\Models\SysCategoryItemModel;
/**
* 首页分类项管理
*/
#[RequestAttribute('/system/category/item', 'system.category.item')]
class SysCategoryItemController extends BaseController
{
protected array $searchField = [
'category_id' => '=',
'status' => '=',
];
protected array $quickSearchField = ['title'];
/** 查询分类项列表 */
#[GetRoute(authorize: 'query')]
public function query(Request $request): JsonResponse
{
$query = SysCategoryItemModel::query();
$data = $this->buildSearch($request->all(), $query)
->orderBy('sort', 'desc')
->orderBy('id', 'desc')
->paginate($request->input('pageSize', 10))
->toArray();
return $this->success($data);
}
/** 创建分类项 */
#[PostRoute(authorize: 'create')]
public function create(SysCategoryItemFormRequest $request): JsonResponse
{
$validated = $request->validated();
$model = SysCategoryItemModel::create($validated);
if (empty($model)) {
return $this->error();
}
return $this->success();
}
/** 编辑分类项 */
#[PutRoute(
route: '/{id}',
authorize: 'update',
where: ['id' => '[0-9]+']
)]
public function update(int $id, SysCategoryItemFormRequest $request): JsonResponse
{
$validated = $request->validated();
$model = SysCategoryItemModel::find($id);
if (empty($model)) {
return $this->error('分类项不存在');
}
$model->update($validated);
return $this->success();
}
/** 删除分类项 */
#[DeleteRoute(
route: '/{id}',
authorize: 'delete',
where: ['id' => '[0-9]+']
)]
public function delete(int $id): JsonResponse
{
$model = SysCategoryItemModel::find($id);
if (empty($model)) {
return $this->error('分类项不存在');
}
$model->delete();
return $this->success();
}
}
@@ -2,8 +2,10 @@
namespace Modules\SystemTool\Http\Requests; namespace Modules\SystemTool\Http\Requests;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\Exists; use Illuminate\Validation\Rules\Exists;
use Modules\Common\Http\Requests\BaseFormRequest; use Modules\Common\Http\Requests\BaseFormRequest;
use Modules\SystemTool\Models\SysCategoryModel;
use Modules\SystemTool\Models\SysFileModel; use Modules\SystemTool\Models\SysFileModel;
class SysCarouselFormRequest extends BaseFormRequest class SysCarouselFormRequest extends BaseFormRequest
@@ -15,7 +17,19 @@ class SysCarouselFormRequest extends BaseFormRequest
return [ return [
'title' => 'required|string|max:100', 'title' => 'required|string|max:100',
'image_id' => ['required', 'integer', new Exists(SysFileModel::class, 'id')], 'image_id' => ['required', 'integer', new Exists(SysFileModel::class, 'id')],
'link' => 'nullable|string|max:500', 'link_type' => 'nullable|integer|in:0,1',
'link' => [
'nullable',
'string',
'max:500',
Rule::requiredIf(fn () => $this->input('link_type', 0) == 0),
],
'category_id' => [
'nullable',
'integer',
Rule::requiredIf(fn () => $this->input('link_type', 0) == 1),
new Exists(SysCategoryModel::class, 'id'),
],
'status' => 'nullable|integer|in:0,1', 'status' => 'nullable|integer|in:0,1',
'sort' => 'nullable|integer', 'sort' => 'nullable|integer',
]; ];
@@ -27,7 +41,10 @@ class SysCarouselFormRequest extends BaseFormRequest
'title.required' => '轮播图标题是必填的', 'title.required' => '轮播图标题是必填的',
'title.max' => '轮播图标题不能超过 :max 个字符', 'title.max' => '轮播图标题不能超过 :max 个字符',
'image_id.required' => '轮播图图片是必填的', 'image_id.required' => '轮播图图片是必填的',
'link_type.in' => '跳转类型只能是 0(链接)或 1(分类)',
'link.required' => '链接模式下跳转链接是必填的',
'link.max' => '跳转链接不能超过 :max 个字符', 'link.max' => '跳转链接不能超过 :max 个字符',
'category_id.required' => '分类模式下请选择分类',
'status.in' => '状态值只能是 0(启用)或 1(禁用)', 'status.in' => '状态值只能是 0(启用)或 1(禁用)',
'sort.integer' => '排序必须是整数', 'sort.integer' => '排序必须是整数',
]; ];
@@ -0,0 +1,29 @@
<?php
namespace Modules\SystemTool\Http\Requests;
use Modules\Common\Http\Requests\BaseFormRequest;
class SysCategoryFormRequest extends BaseFormRequest
{
protected $stopOnFirstFailure = true;
public function rules(): array
{
return [
'name' => 'required|string|max:100',
'status' => 'nullable|integer|in:0,1',
'sort' => 'nullable|integer',
];
}
public function messages(): array
{
return [
'name.required' => '分类名称是必填的',
'name.max' => '分类名称不能超过 :max 个字符',
'status.in' => '状态值只能是 0(启用)或 1(禁用)',
'sort.integer' => '排序必须是整数',
];
}
}
@@ -0,0 +1,38 @@
<?php
namespace Modules\SystemTool\Http\Requests;
use Illuminate\Validation\Rules\Exists;
use Modules\Common\Http\Requests\BaseFormRequest;
use Modules\SystemTool\Models\SysCategoryModel;
use Modules\SystemTool\Models\SysFileModel;
class SysCategoryItemFormRequest extends BaseFormRequest
{
protected $stopOnFirstFailure = true;
public function rules(): array
{
return [
'category_id' => ['required', 'integer', new Exists(SysCategoryModel::class, 'id')],
'title' => 'required|string|max:100',
'image_id' => ['required', 'integer', new Exists(SysFileModel::class, 'id')],
'link' => 'nullable|string|max:500',
'status' => 'nullable|integer|in:0,1',
'sort' => 'nullable|integer',
];
}
public function messages(): array
{
return [
'category_id.required' => '所属分类是必填的',
'title.required' => '分类项标题是必填的',
'title.max' => '分类项标题不能超过 :max 个字符',
'image_id.required' => '分类项图标是必填的',
'link.max' => '跳转链接不能超过 :max 个字符',
'status.in' => '状态值只能是 0(启用)或 1(禁用)',
'sort.integer' => '排序必须是整数',
];
}
}
@@ -2,8 +2,10 @@
namespace Modules\SystemTool\Http\Requests; namespace Modules\SystemTool\Http\Requests;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\Exists; use Illuminate\Validation\Rules\Exists;
use Modules\Common\Http\Requests\BaseFormRequest; use Modules\Common\Http\Requests\BaseFormRequest;
use Modules\SystemTool\Models\SysCategoryModel;
use Modules\SystemTool\Models\SysFileModel; use Modules\SystemTool\Models\SysFileModel;
class SysGridNavFormRequest extends BaseFormRequest class SysGridNavFormRequest extends BaseFormRequest
@@ -15,7 +17,19 @@ class SysGridNavFormRequest extends BaseFormRequest
return [ return [
'title' => 'required|string|max:100', 'title' => 'required|string|max:100',
'image_id' => ['required', 'integer', new Exists(SysFileModel::class, 'id')], 'image_id' => ['required', 'integer', new Exists(SysFileModel::class, 'id')],
'link' => 'nullable|string|max:500', 'link_type' => 'nullable|integer|in:0,1',
'link' => [
'nullable',
'string',
'max:500',
Rule::requiredIf(fn () => $this->input('link_type', 0) == 0),
],
'category_id' => [
'nullable',
'integer',
Rule::requiredIf(fn () => $this->input('link_type', 0) == 1),
new Exists(SysCategoryModel::class, 'id'),
],
'status' => 'nullable|integer|in:0,1', 'status' => 'nullable|integer|in:0,1',
'sort' => 'nullable|integer', 'sort' => 'nullable|integer',
]; ];
@@ -27,7 +41,10 @@ class SysGridNavFormRequest extends BaseFormRequest
'title.required' => '导航标题是必填的', 'title.required' => '导航标题是必填的',
'title.max' => '导航标题不能超过 :max 个字符', 'title.max' => '导航标题不能超过 :max 个字符',
'image_id.required' => '导航图标是必填的', 'image_id.required' => '导航图标是必填的',
'link_type.in' => '跳转类型只能是 0(链接)或 1(分类)',
'link.required' => '链接模式下跳转链接是必填的',
'link.max' => '跳转链接不能超过 :max 个字符', 'link.max' => '跳转链接不能超过 :max 个字符',
'category_id.required' => '分类模式下请选择分类',
'status.in' => '状态值只能是 0(启用)或 1(禁用)', 'status.in' => '状态值只能是 0(启用)或 1(禁用)',
'sort.integer' => '排序必须是整数', 'sort.integer' => '排序必须是整数',
]; ];
+15 -1
View File
@@ -3,6 +3,7 @@
namespace Modules\SystemTool\Models; namespace Modules\SystemTool\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\Relations\HasOne;
/** /**
@@ -16,17 +17,21 @@ class SysCarouselModel extends Model
'status' => 'int', 'status' => 'int',
'sort' => 'int', 'sort' => 'int',
'image_id' => 'array', 'image_id' => 'array',
'link_type' => 'int',
'category_id' => 'int',
]; ];
protected $fillable = [ protected $fillable = [
'title', 'title',
'image_id', 'image_id',
'link', 'link',
'link_type',
'category_id',
'status', 'status',
'sort', 'sort',
]; ];
protected $with = ['image']; protected $with = ['image', 'category'];
/** /**
* 关联图片 * 关联图片
@@ -37,4 +42,13 @@ class SysCarouselModel extends Model
return $this->hasOne(SysFileModel::class, 'id', 'image_id'); return $this->hasOne(SysFileModel::class, 'id', 'image_id');
} }
/**
* 关联所属分类(link_type=1 时生效)
* @return BelongsTo
*/
public function category(): BelongsTo
{
return $this->belongsTo(SysCategoryModel::class, 'category_id', 'id');
}
} }
@@ -0,0 +1,52 @@
<?php
namespace Modules\SystemTool\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasOne;
/**
* 首页分类项模型(字段与宫格导航表一致)
*/
class SysCategoryItemModel extends Model
{
protected $table = 'sys_category_item';
protected $casts = [
'category_id' => 'int',
'status' => 'int',
'sort' => 'int',
'image_id' => 'int',
];
protected $fillable = [
'category_id',
'title',
'image_id',
'link',
'status',
'sort',
];
protected $with = ['image'];
/**
* 关联图片
* @return HasOne
*/
public function image(): HasOne
{
return $this->hasOne(SysFileModel::class, 'id', 'image_id');
}
/**
* 关联所属分类
* @return BelongsTo
*/
public function category(): BelongsTo
{
return $this->belongsTo(SysCategoryModel::class, 'category_id', 'id');
}
}
@@ -0,0 +1,37 @@
<?php
namespace Modules\SystemTool\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* 首页分类模型
*/
class SysCategoryModel extends Model
{
protected $table = 'sys_category';
protected $casts = [
'status' => 'int',
'sort' => 'int',
];
protected $fillable = [
'name',
'status',
'sort',
];
/**
* 关联分类项
* @return HasMany
*/
public function categoryItems(): HasMany
{
return $this->hasMany(SysCategoryItemModel::class, 'category_id', 'id')
->orderBy('sort', 'desc')
->orderBy('id', 'desc');
}
}
+15 -1
View File
@@ -3,6 +3,7 @@
namespace Modules\SystemTool\Models; namespace Modules\SystemTool\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\Relations\HasOne;
/** /**
@@ -16,17 +17,21 @@ class SysGridNavModel extends Model
'status' => 'int', 'status' => 'int',
'sort' => 'int', 'sort' => 'int',
'image_id' => 'int', 'image_id' => 'int',
'link_type' => 'int',
'category_id' => 'int',
]; ];
protected $fillable = [ protected $fillable = [
'title', 'title',
'image_id', 'image_id',
'link', 'link',
'link_type',
'category_id',
'status', 'status',
'sort', 'sort',
]; ];
protected $with = ['image']; protected $with = ['image', 'category'];
/** /**
* 关联图片 * 关联图片
@@ -37,4 +42,13 @@ class SysGridNavModel extends Model
return $this->hasOne(SysFileModel::class, 'id', 'image_id'); return $this->hasOne(SysFileModel::class, 'id', 'image_id');
} }
/**
* 关联所属分类(link_type=1 时生效)
* @return BelongsTo
*/
public function category(): BelongsTo
{
return $this->belongsTo(SysCategoryModel::class, 'category_id', 'id');
}
} }
+33 -17
View File
@@ -11,8 +11,8 @@ Route::controller(App\Http\Controllers\IndexController::class)->prefix('api')->g
Route::get('/index', 'index')->middleware(['auth:sanctum', 'authGuard:users']); Route::get('/index', 'index')->middleware(['auth:sanctum', 'authGuard:users']);
Route::post('/login', 'login')->middleware(['auth:sanctum', 'authGuard:users']); Route::post('/login', 'login')->middleware(['auth:sanctum', 'authGuard:users']);
Route::post('/register', 'register')->middleware(['auth:sanctum', 'authGuard:users']); Route::post('/register', 'register')->middleware(['auth:sanctum', 'authGuard:users']);
// 首页公开接口
Route::get('/home', 'home'); Route::get('/home', 'home');
Route::get('/category/{id}', 'category');
}); });
// UserController // UserController
@@ -54,6 +54,30 @@ Route::controller(Modules\SystemTool\Http\Controllers\SysAiController::class)->p
Route::post('/test', 'testConnection')->middleware(['auth:sanctum', 'authGuard', 'abilities:system.ai.test']); Route::post('/test', 'testConnection')->middleware(['auth:sanctum', 'authGuard', 'abilities:system.ai.test']);
}); });
// SysCarouselController
Route::controller(Modules\SystemTool\Http\Controllers\SysCarouselController::class)->prefix('system/carousel')->group(function () {
Route::get('/', 'query')->middleware(['auth:sanctum', 'authGuard', 'abilities:system.carousel.query']);
Route::post('/', 'create')->middleware(['auth:sanctum', 'authGuard', 'abilities:system.carousel.create']);
Route::put('/{id}', 'update')->middleware(['auth:sanctum', 'authGuard', 'abilities:system.carousel.update']);
Route::delete('/{id}', 'delete')->middleware(['auth:sanctum', 'authGuard', 'abilities:system.carousel.delete']);
});
// SysCategoryController
Route::controller(Modules\SystemTool\Http\Controllers\SysCategoryController::class)->prefix('system/category')->group(function () {
Route::get('/', 'query')->middleware(['auth:sanctum', 'authGuard', 'abilities:system.category.query']);
Route::post('/', 'create')->middleware(['auth:sanctum', 'authGuard', 'abilities:system.category.create']);
Route::put('/{id}', 'update')->middleware(['auth:sanctum', 'authGuard', 'abilities:system.category.update']);
Route::delete('/{id}', 'delete')->middleware(['auth:sanctum', 'authGuard', 'abilities:system.category.delete']);
});
// SysCategoryItemController
Route::controller(Modules\SystemTool\Http\Controllers\SysCategoryItemController::class)->prefix('system/category/item')->group(function () {
Route::get('/', 'query')->middleware(['auth:sanctum', 'authGuard', 'abilities:system.category.item.query']);
Route::post('/', 'create')->middleware(['auth:sanctum', 'authGuard', 'abilities:system.category.item.create']);
Route::put('/{id}', 'update')->middleware(['auth:sanctum', 'authGuard', 'abilities:system.category.item.update']);
Route::delete('/{id}', 'delete')->middleware(['auth:sanctum', 'authGuard', 'abilities:system.category.item.delete']);
});
// SysDictController // SysDictController
Route::controller(Modules\SystemTool\Http\Controllers\SysDictController::class)->prefix('system/dict/list')->group(function () { Route::controller(Modules\SystemTool\Http\Controllers\SysDictController::class)->prefix('system/dict/list')->group(function () {
Route::get('/', 'query')->middleware(['auth:sanctum', 'authGuard', 'abilities:system.dict.list.query']); Route::get('/', 'query')->middleware(['auth:sanctum', 'authGuard', 'abilities:system.dict.list.query']);
@@ -97,6 +121,14 @@ Route::controller(Modules\SystemTool\Http\Controllers\SysFileGroupController::cl
Route::delete('/{id}', 'delete')->middleware(['auth:sanctum', 'authGuard', 'abilities:system.file.group.delete']); Route::delete('/{id}', 'delete')->middleware(['auth:sanctum', 'authGuard', 'abilities:system.file.group.delete']);
}); });
// SysGridNavController
Route::controller(Modules\SystemTool\Http\Controllers\SysGridNavController::class)->prefix('system/grid-nav')->group(function () {
Route::get('/', 'query')->middleware(['auth:sanctum', 'authGuard', 'abilities:system.grid-nav.query']);
Route::post('/', 'create')->middleware(['auth:sanctum', 'authGuard', 'abilities:system.grid-nav.create']);
Route::put('/{id}', 'update')->middleware(['auth:sanctum', 'authGuard', 'abilities:system.grid-nav.update']);
Route::delete('/{id}', 'delete')->middleware(['auth:sanctum', 'authGuard', 'abilities:system.grid-nav.delete']);
});
// SysIndexController // SysIndexController
Route::controller(Modules\SystemTool\Http\Controllers\SysIndexController::class)->group(function () { Route::controller(Modules\SystemTool\Http\Controllers\SysIndexController::class)->group(function () {
Route::get('/index', 'index'); Route::get('/index', 'index');
@@ -178,22 +210,6 @@ Route::controller(Modules\SystemUser\Http\Controllers\SysRuleController::class)-
Route::put('/status/{id}', 'status')->middleware(['auth:sanctum', 'authGuard', 'abilities:system.rule.status']); Route::put('/status/{id}', 'status')->middleware(['auth:sanctum', 'authGuard', 'abilities:system.rule.status']);
}); });
// SysCarouselController
Route::controller(Modules\SystemTool\Http\Controllers\SysCarouselController::class)->prefix('system/carousel')->group(function () {
Route::get('/', 'query')->middleware(['auth:sanctum', 'authGuard', 'abilities:system.carousel.query']);
Route::post('/', 'create')->middleware(['auth:sanctum', 'authGuard', 'abilities:system.carousel.create']);
Route::put('/{id}', 'update')->middleware(['auth:sanctum', 'authGuard', 'abilities:system.carousel.update']);
Route::delete('/{id}', 'delete')->middleware(['auth:sanctum', 'authGuard', 'abilities:system.carousel.delete']);
});
// SysGridNavController
Route::controller(Modules\SystemTool\Http\Controllers\SysGridNavController::class)->prefix('system/grid-nav')->group(function () {
Route::get('/', 'query')->middleware(['auth:sanctum', 'authGuard', 'abilities:system.grid-nav.query']);
Route::post('/', 'create')->middleware(['auth:sanctum', 'authGuard', 'abilities:system.grid-nav.create']);
Route::put('/{id}', 'update')->middleware(['auth:sanctum', 'authGuard', 'abilities:system.grid-nav.update']);
Route::delete('/{id}', 'delete')->middleware(['auth:sanctum', 'authGuard', 'abilities:system.grid-nav.delete']);
});
// SysUserController // SysUserController
Route::controller(Modules\SystemUser\Http\Controllers\SysUserController::class)->prefix('system/user')->group(function () { Route::controller(Modules\SystemUser\Http\Controllers\SysUserController::class)->prefix('system/user')->group(function () {
Route::get('/', 'query')->middleware(['auth:sanctum', 'authGuard', 'abilities:system.user.query']); Route::get('/', 'query')->middleware(['auth:sanctum', 'authGuard', 'abilities:system.user.query']);
+8 -1
View File
@@ -1,3 +1,4 @@
import type { ICategory } from '@/domain/iCategory';
import type { ISysFileInfo } from '@/domain/iSysFile'; import type { ISysFileInfo } from '@/domain/iSysFile';
/** 首页轮播图 */ /** 首页轮播图 */
@@ -7,8 +8,14 @@ export interface ICarousel {
title: string; title: string;
/** 轮播图图片信息(ISysFileInfo 对象) */ /** 轮播图图片信息(ISysFileInfo 对象) */
image: ISysFileInfo | string; image: ISysFileInfo | string;
/** 跳转链接 */ /** 跳转类型:0=链接,1=分类 */
link_type?: number;
/** 跳转链接(link_type=0 时生效) */
link?: string; link?: string;
/** 分类IDlink_type=1 时生效) */
category_id?: number;
/** 所属分类(后端关联返回) */
category?: ICategory;
/** 状态:0=启用,1=禁用 */ /** 状态:0=启用,1=禁用 */
status: number; status: number;
/** 排序 */ /** 排序 */
+12
View File
@@ -0,0 +1,12 @@
/** 首页分类 */
export interface ICategory {
id?: number;
/** 分类名称 */
name: string;
/** 状态:0=启用,1=禁用 */
status: number;
/** 排序 */
sort: number;
created_at?: string;
updated_at?: string;
}
+20
View File
@@ -0,0 +1,20 @@
import type { ISysFileInfo } from '@/domain/iSysFile';
/** 首页分类项(字段与宫格导航一致) */
export interface ICategoryItem {
id?: number;
/** 所属分类ID */
category_id?: number;
/** 导航标题 */
title: string;
/** 导航图标信息(ISysFileInfo 对象) */
image: ISysFileInfo | string;
/** 跳转链接 */
link?: string;
/** 状态:0=启用,1=禁用 */
status: number;
/** 排序 */
sort: number;
created_at?: string;
updated_at?: string;
}
+8 -1
View File
@@ -1,3 +1,4 @@
import type { ICategory } from '@/domain/iCategory';
import type { ISysFileInfo } from '@/domain/iSysFile'; import type { ISysFileInfo } from '@/domain/iSysFile';
/** 首页宫格导航 */ /** 首页宫格导航 */
@@ -7,8 +8,14 @@ export interface IGridNav {
title: string; title: string;
/** 导航图标信息(ISysFileInfo 对象) */ /** 导航图标信息(ISysFileInfo 对象) */
image: ISysFileInfo | string; image: ISysFileInfo | string;
/** 跳转链接 */ /** 跳转类型:0=链接,1=分类 */
link_type?: number;
/** 跳转链接(link_type=0 时生效) */
link?: string; link?: string;
/** 分类IDlink_type=1 时生效) */
category_id?: number;
/** 所属分类(后端关联返回) */
category?: ICategory;
/** 状态:0=启用,1=禁用 */ /** 状态:0=启用,1=禁用 */
status: number; status: number;
/** 排序 */ /** 排序 */
+2
View File
@@ -18,6 +18,7 @@ import systemRule from "./system/rule";
import systemAi from "./system/ai"; import systemAi from "./system/ai";
import systemCarousel from "./system/carousel"; import systemCarousel from "./system/carousel";
import systemGridNav from "./system/gridNav"; import systemGridNav from "./system/gridNav";
import systemCategory from "./system/category";
import aiChat from "./ai/chat"; import aiChat from "./ai/chat";
import aiConversation from "./ai/conversation"; import aiConversation from "./ai/conversation";
@@ -50,6 +51,7 @@ export default {
...systemAi, ...systemAi,
...systemCarousel, ...systemCarousel,
...systemGridNav, ...systemGridNav,
...systemCategory,
...aiChat, ...aiChat,
...aiConversation, ...aiConversation,
...aiAgent, ...aiAgent,
+1
View File
@@ -46,5 +46,6 @@ export default {
"menu.system.ai": "AI", "menu.system.ai": "AI",
"menu.system.carousel": "Carousel", "menu.system.carousel": "Carousel",
"menu.system.grid-nav": "Grid Navigation", "menu.system.grid-nav": "Grid Navigation",
"menu.system.category": "Categories",
"menu.xin-admin": "XinAdmin", "menu.xin-admin": "XinAdmin",
} }
+6
View File
@@ -9,8 +9,14 @@ export default {
"system.carousel.title.required": "Carousel title is required", "system.carousel.title.required": "Carousel title is required",
"system.carousel.image": "Carousel Image", "system.carousel.image": "Carousel Image",
"system.carousel.image.required": "Carousel image is required", "system.carousel.image.required": "Carousel image is required",
"system.carousel.linkType": "Link Type",
"system.carousel.linkType.required": "Link type is required",
"system.carousel.linkType.link": "Link",
"system.carousel.linkType.category": "Category",
"system.carousel.link": "Redirect Link", "system.carousel.link": "Redirect Link",
"system.carousel.link.placeholder": "Enter redirect link", "system.carousel.link.placeholder": "Enter redirect link",
"system.carousel.category": "Target Category",
"system.carousel.category.placeholder": "Select a category",
"system.carousel.status": "Status", "system.carousel.status": "Status",
"system.carousel.status.required": "Status is required", "system.carousel.status.required": "Status is required",
"system.carousel.status.normal": "Active", "system.carousel.status.normal": "Active",
+40
View File
@@ -0,0 +1,40 @@
export default {
// Page Title
"system.category.page.title": "Category Management",
"system.category.page.description": "Manage homepage categories and category item links",
// Fields
"system.category.id": "ID",
"system.category.name": "Category Name",
"system.category.name.required": "Category name is required",
"system.category.status": "Status",
"system.category.status.required": "Status is required",
"system.category.status.normal": "Active",
"system.category.status.disabled": "Disabled",
"system.category.sort": "Sort",
"system.category.createdAt": "Created At",
"system.category.updatedAt": "Updated At",
// Category Items
"system.category.manageItems": "Manage Items",
"system.category.itemManagement": "Category Items",
"system.category.backToList": "Back to Categories",
"system.category.selectCategoryFirst": "Please select a category first",
"system.category.item.id": "ID",
"system.category.item.title": "Item Title",
"system.category.item.title.required": "Item title is required",
"system.category.item.image": "Item Icon",
"system.category.item.image.required": "Item icon is required",
"system.category.item.link": "Redirect Link",
"system.category.item.link.placeholder": "Enter redirect link",
"system.category.item.status": "Status",
"system.category.item.status.required": "Status is required",
"system.category.item.status.normal": "Active",
"system.category.item.status.disabled": "Disabled",
"system.category.item.sort": "Sort",
"system.category.item.createdAt": "Created At",
"system.category.item.updatedAt": "Updated At",
"system.category.item.createSuccess": "Category item created successfully",
"system.category.item.updateSuccess": "Category item updated successfully",
};
+6
View File
@@ -9,8 +9,14 @@ export default {
"system.gridNav.title.required": "Navigation title is required", "system.gridNav.title.required": "Navigation title is required",
"system.gridNav.image": "Navigation Icon", "system.gridNav.image": "Navigation Icon",
"system.gridNav.image.required": "Navigation icon is required", "system.gridNav.image.required": "Navigation icon is required",
"system.gridNav.linkType": "Link Type",
"system.gridNav.linkType.required": "Link type is required",
"system.gridNav.linkType.link": "Link",
"system.gridNav.linkType.category": "Category",
"system.gridNav.link": "Redirect Link", "system.gridNav.link": "Redirect Link",
"system.gridNav.link.placeholder": "Enter redirect link", "system.gridNav.link.placeholder": "Enter redirect link",
"system.gridNav.category": "Target Category",
"system.gridNav.category.placeholder": "Select a category",
"system.gridNav.status": "Status", "system.gridNav.status": "Status",
"system.gridNav.status.required": "Status is required", "system.gridNav.status.required": "Status is required",
"system.gridNav.status.normal": "Active", "system.gridNav.status.normal": "Active",
+2
View File
@@ -18,6 +18,7 @@ import systemRule from "./system/rule";
import systemAi from "./system/ai"; import systemAi from "./system/ai";
import systemCarousel from "./system/carousel"; import systemCarousel from "./system/carousel";
import systemGridNav from "./system/gridNav"; import systemGridNav from "./system/gridNav";
import systemCategory from "./system/category";
import aiChat from "./ai/chat"; import aiChat from "./ai/chat";
import aiConversation from "./ai/conversation"; import aiConversation from "./ai/conversation";
@@ -50,6 +51,7 @@ export default {
...systemAi, ...systemAi,
...systemCarousel, ...systemCarousel,
...systemGridNav, ...systemGridNav,
...systemCategory,
...aiChat, ...aiChat,
...aiConversation, ...aiConversation,
...aiAgent, ...aiAgent,
+1
View File
@@ -46,5 +46,6 @@ export default {
"menu.system.ai": "AI 配置", "menu.system.ai": "AI 配置",
"menu.system.carousel": "轮播图管理", "menu.system.carousel": "轮播图管理",
"menu.system.grid-nav": "宫格导航管理", "menu.system.grid-nav": "宫格导航管理",
"menu.system.category": "分类管理",
"menu.xin-admin": "XinAdmin", "menu.xin-admin": "XinAdmin",
}; };
+6
View File
@@ -9,8 +9,14 @@ export default {
"system.carousel.title.required": "轮播图标题不能为空", "system.carousel.title.required": "轮播图标题不能为空",
"system.carousel.image": "轮播图片", "system.carousel.image": "轮播图片",
"system.carousel.image.required": "轮播图片不能为空", "system.carousel.image.required": "轮播图片不能为空",
"system.carousel.linkType": "跳转类型",
"system.carousel.linkType.required": "跳转类型不能为空",
"system.carousel.linkType.link": "链接",
"system.carousel.linkType.category": "分类",
"system.carousel.link": "跳转链接", "system.carousel.link": "跳转链接",
"system.carousel.link.placeholder": "请输入跳转链接", "system.carousel.link.placeholder": "请输入跳转链接",
"system.carousel.category": "跳转分类",
"system.carousel.category.placeholder": "请选择分类",
"system.carousel.status": "状态", "system.carousel.status": "状态",
"system.carousel.status.required": "状态不能为空", "system.carousel.status.required": "状态不能为空",
"system.carousel.status.normal": "启用", "system.carousel.status.normal": "启用",
+40
View File
@@ -0,0 +1,40 @@
export default {
// 页面标题
"system.category.page.title": "首页分类管理",
"system.category.page.description": "管理首页分类及分类项跳转链接",
// 字段
"system.category.id": "ID",
"system.category.name": "分类名称",
"system.category.name.required": "分类名称不能为空",
"system.category.status": "状态",
"system.category.status.required": "状态不能为空",
"system.category.status.normal": "启用",
"system.category.status.disabled": "禁用",
"system.category.sort": "排序",
"system.category.createdAt": "创建时间",
"system.category.updatedAt": "更新时间",
// 分类项
"system.category.manageItems": "分类项管理",
"system.category.itemManagement": "分类项管理",
"system.category.backToList": "返回分类列表",
"system.category.selectCategoryFirst": "请先选择分类",
"system.category.item.id": "ID",
"system.category.item.title": "导航标题",
"system.category.item.title.required": "导航标题不能为空",
"system.category.item.image": "导航图标",
"system.category.item.image.required": "导航图标不能为空",
"system.category.item.link": "跳转链接",
"system.category.item.link.placeholder": "请输入跳转链接",
"system.category.item.status": "状态",
"system.category.item.status.required": "状态不能为空",
"system.category.item.status.normal": "启用",
"system.category.item.status.disabled": "禁用",
"system.category.item.sort": "排序",
"system.category.item.createdAt": "创建时间",
"system.category.item.updatedAt": "更新时间",
"system.category.item.createSuccess": "分类项创建成功",
"system.category.item.updateSuccess": "分类项更新成功",
};
+6
View File
@@ -9,8 +9,14 @@ export default {
"system.gridNav.title.required": "导航标题不能为空", "system.gridNav.title.required": "导航标题不能为空",
"system.gridNav.image": "导航图标", "system.gridNav.image": "导航图标",
"system.gridNav.image.required": "导航图标不能为空", "system.gridNav.image.required": "导航图标不能为空",
"system.gridNav.linkType": "跳转类型",
"system.gridNav.linkType.required": "跳转类型不能为空",
"system.gridNav.linkType.link": "链接",
"system.gridNav.linkType.category": "分类",
"system.gridNav.link": "跳转链接", "system.gridNav.link": "跳转链接",
"system.gridNav.link.placeholder": "请输入跳转链接", "system.gridNav.link.placeholder": "请输入跳转链接",
"system.gridNav.category": "跳转分类",
"system.gridNav.category.placeholder": "请选择分类",
"system.gridNav.status": "状态", "system.gridNav.status": "状态",
"system.gridNav.status.required": "状态不能为空", "system.gridNav.status.required": "状态不能为空",
"system.gridNav.status.normal": "启用", "system.gridNav.status.normal": "启用",
+66 -1
View File
@@ -1,8 +1,11 @@
import XinTable from '@/components/XinTable'; import XinTable from '@/components/XinTable';
import { Badge, Image, Typography } from 'antd'; import { Badge, Image, Tag, Typography } from 'antd';
import { useEffect, useState } from 'react';
import type { ICarousel } from '@/domain/iCarousel'; import type { ICarousel } from '@/domain/iCarousel';
import type { ICategory } from '@/domain/iCategory';
import type { XinTableColumn } from '@/components/XinTable/typings'; import type { XinTableColumn } from '@/components/XinTable/typings';
import type { ISysFileInfo } from '@/domain/iSysFile'; import type { ISysFileInfo } from '@/domain/iSysFile';
import { List } from '@/api/common/table';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
@@ -22,6 +25,20 @@ function getImageUrl(image: ISysFileInfo | string | null | undefined): string {
export default function CarouselPage() { export default function CarouselPage() {
const { t } = useTranslation(); const { t } = useTranslation();
// 分类下拉选项
const [categoryOptions, setCategoryOptions] = useState<{ label: string; value: number }[]>([]);
useEffect(() => {
List<ICategory>('/system/category', { pageSize: 1000 }).then((res) => {
const list = res.data?.data?.data ?? [];
setCategoryOptions(list.map((item) => ({
label: item.name,
value: item.id as number,
})));
}).catch(() => {
// 分类加载失败不影响页面使用
});
}, []);
const columns: XinTableColumn<ICarousel>[] = [ const columns: XinTableColumn<ICarousel>[] = [
{ {
title: t('system.carousel.id'), title: t('system.carousel.id'),
@@ -64,6 +81,25 @@ export default function CarouselPage() {
); );
}, },
}, },
{
title: t('system.carousel.linkType'),
dataIndex: 'link_type',
valueType: 'select',
colProps: { span: 12 },
initialValue: 0,
rules: [{ required: true, message: t('system.carousel.linkType.required') }],
fieldProps: {
options: [
{ label: t('system.carousel.linkType.link'), value: 0 },
{ label: t('system.carousel.linkType.category'), value: 1 },
],
},
render: (value: number) => {
return value === 1
? <Tag color="blue">{t('system.carousel.linkType.category')}</Tag>
: <Tag>{t('system.carousel.linkType.link')}</Tag>;
},
},
{ {
title: t('system.carousel.link'), title: t('system.carousel.link'),
dataIndex: 'link', dataIndex: 'link',
@@ -73,6 +109,35 @@ export default function CarouselPage() {
fieldProps: { fieldProps: {
placeholder: t('system.carousel.link.placeholder'), placeholder: t('system.carousel.link.placeholder'),
}, },
render: (_, record) => {
if(record.link_type === 1) {
const name = record.category?.name;
return name ? <Tag>{name}</Tag> : '-';
}
return record.link || '-'
},
dependency: {
dependencies: ['link_type'],
visible: (values) => values.link_type !== 1,
},
},
{
title: t('system.carousel.category'),
dataIndex: 'category_id',
valueType: 'select',
colProps: { span: 12 },
hideInSearch: true,
hideInTable: true,
fieldProps: {
options: categoryOptions,
showSearch: true,
optionFilterProp: 'label',
placeholder: t('system.carousel.category.placeholder'),
},
dependency: {
dependencies: ['link_type'],
visible: (values) => values.link_type === 1,
},
}, },
{ {
title: t('system.carousel.status'), title: t('system.carousel.status'),
+121
View File
@@ -0,0 +1,121 @@
import XinTable from '@/components/XinTable';
import { Badge, Button, Tooltip, Typography } from 'antd';
import { UnorderedListOutlined } from '@ant-design/icons';
import type { ICategory } from '@/domain/iCategory';
import type { XinTableColumn } from '@/components/XinTable/typings';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router';
import dayjs from 'dayjs';
const { Title, Text } = Typography;
/** 首页分类管理 */
export default function CategoryPage() {
const { t } = useTranslation();
const navigate = useNavigate();
const columns: XinTableColumn<ICategory>[] = [
{
title: t('system.category.id'),
dataIndex: 'id',
hideInForm: true,
width: 80,
sorter: true,
align: 'center',
},
{
title: t('system.category.name'),
dataIndex: 'name',
valueType: 'text',
colProps: { span: 12 },
rules: [{ required: true, message: t('system.category.name.required') }],
},
{
title: t('system.category.status'),
dataIndex: 'status',
valueType: 'select',
filters: [
{ text: t('system.category.status.normal'), value: 0 },
{ text: t('system.category.status.disabled'), value: 1 },
],
colProps: { span: 12 },
rules: [{ required: true, message: t('system.category.status.required') }],
fieldProps: {
options: [
{ label: t('system.category.status.normal'), value: 0 },
{ label: t('system.category.status.disabled'), value: 1 },
],
},
render: (value: number) => {
return value === 0
? <Badge status="success" text={t('system.category.status.normal')} />
: <Badge status="error" text={t('system.category.status.disabled')} />;
},
},
{
title: t('system.category.sort'),
dataIndex: 'sort',
valueType: 'digit',
colProps: { span: 12 },
hideInSearch: true,
initialValue: 0,
fieldProps: {
min: 0,
style: { width: '100%' },
},
},
{
title: t('system.category.createdAt'),
dataIndex: 'created_at',
render: (value: string) => (value ? dayjs(value).format('YYYY-MM-DD HH:mm') : '-'),
hideInForm: true,
hideInSearch: true,
width: 160,
},
];
// 跳转到分类项管理页面
const handleGoToItems = (record: ICategory) => {
navigate(`/system/category/item?categoryId=${record.id}&categoryName=${encodeURIComponent(record.name || '')}`);
};
return (
<>
<div className="mb-5">
<Title level={3}>{t('system.category.page.title')}</Title>
<Text type="secondary">{t('system.category.page.description')}</Text>
</div>
<XinTable<ICategory>
api="/system/category"
columns={columns}
rowKey="id"
accessName="system.category"
formProps={{
grid: true,
colProps: { span: 12 },
rowProps: { gutter: [30, 0] },
layout: 'vertical',
}}
modalProps={{ width: 600 }}
searchProps={false}
operateProps={{
fixed: 'right',
width: 180,
}}
scroll={{ x: 1000 }}
operateRender={(record, dom) => [
<Tooltip title={t('system.category.manageItems')} key="items">
<Button
type="default"
icon={<UnorderedListOutlined />}
size="small"
onClick={() => handleGoToItems(record)}
/>
</Tooltip>,
dom.edit,
dom.del,
]}
/>
</>
);
}
+206
View File
@@ -0,0 +1,206 @@
import XinTable from '@/components/XinTable';
import { Badge, Button, Image, Space, Typography } from 'antd';
import { LeftOutlined } from '@ant-design/icons';
import type { ICategoryItem } from '@/domain/iCategoryItem';
import type { XinTableColumn } from '@/components/XinTable/typings';
import type { ISysFileInfo } from '@/domain/iSysFile';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate, useSearchParams } from 'react-router';
import { Create, Update } from '@/api/common/table.ts';
import dayjs from 'dayjs';
const { Title } = Typography;
/**
* 从 image 字段中提取预览 URL
* image 字段可能是 ISysFileInfo 对象或 URL 字符串
*/
function getImageUrl(image: ISysFileInfo | string | null | undefined): string {
if (!image) return '';
if (typeof image === 'string') return image;
return image.preview_url || image.file_url || '';
}
/** 首页分类项管理 */
export default function CategoryItemPage() {
const { t } = useTranslation();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
// 从 URL 参数获取分类信息
const categoryId = searchParams.get('categoryId');
const categoryName = searchParams.get('categoryName') || '';
const [currentCategory, setCurrentCategory] = useState({
id: categoryId ? parseInt(categoryId) : 0,
name: decodeURIComponent(categoryName),
});
// 监听 URL 参数变化
useEffect(() => {
if (categoryId) {
setCurrentCategory({
id: parseInt(categoryId),
name: decodeURIComponent(categoryName),
});
}
}, [categoryId, categoryName]);
const columns: XinTableColumn<ICategoryItem>[] = [
{
title: t('system.category.item.id'),
dataIndex: 'id',
hideInForm: true,
width: 80,
align: 'center',
},
{
title: t('system.category.item.title'),
dataIndex: 'title',
valueType: 'text',
colProps: { span: 12 },
rules: [{ required: true, message: t('system.category.item.title.required') }],
},
{
title: t('system.category.item.image'),
dataIndex: 'image_id',
valueType: 'image',
colProps: { span: 24 },
rules: [{ required: true, message: t('system.category.item.image.required') }],
hideInSearch: true,
fieldProps: {
action: '/system/file/list/upload',
mode: 'single',
maxCount: 1,
changeType: 'id',
},
render: (_value: ISysFileInfo | string, record: ICategoryItem) => {
const url = getImageUrl(record.image);
if (!url) return '-';
return (
<Image
src={url}
width={60}
height={60}
style={{ objectFit: 'cover', borderRadius: 4 }}
/>
);
},
},
{
title: t('system.category.item.link'),
dataIndex: 'link',
valueType: 'text',
colProps: { span: 12 },
hideInSearch: true,
fieldProps: {
placeholder: t('system.category.item.link.placeholder'),
},
},
{
title: t('system.category.item.status'),
dataIndex: 'status',
valueType: 'select',
colProps: { span: 12 },
initialValue: 0,
rules: [{ required: true, message: t('system.category.item.status.required') }],
fieldProps: {
options: [
{ label: t('system.category.item.status.normal'), value: 0 },
{ label: t('system.category.item.status.disabled'), value: 1 },
],
},
render: (value: number) => {
return value === 0
? <Badge status="success" text={t('system.category.item.status.normal')} />
: <Badge status="error" text={t('system.category.item.status.disabled')} />;
},
},
{
title: t('system.category.item.sort'),
dataIndex: 'sort',
valueType: 'digit',
colProps: { span: 12 },
hideInSearch: true,
initialValue: 0,
fieldProps: {
min: 0,
style: { width: '100%' },
},
},
{
title: t('system.category.item.createdAt'),
dataIndex: 'created_at',
render: (value: string) => (value ? dayjs(value).format('YYYY-MM-DD HH:mm') : '-'),
hideInForm: true,
hideInSearch: true,
width: 160,
},
];
// 返回分类列表
const handleGoBack = () => {
navigate('/system/category');
};
// 如果没有分类ID,显示提示
if (!currentCategory.id) {
return (
<div style={{ padding: 50, textAlign: 'center' }}>
<div style={{ marginTop: 50, color: '#999' }}>
{t('system.category.selectCategoryFirst')}
</div>
</div>
);
}
return (
<Space orientation={'vertical'} style={{ width: '100%' }}>
<div>
<Button type={'link'} onClick={handleGoBack} icon={<LeftOutlined />} classNames={{ root: 'p-0 mb-2' }}>
{t('system.category.backToList')}
</Button>
<Title level={3}>
<span className={'mr-2'}>{t('system.category.itemManagement')} - {currentCategory.name}</span>
</Title>
</div>
<XinTable<ICategoryItem>
api="/system/category/item"
columns={columns}
rowKey="id"
accessName="system.category.item"
formProps={{
grid: true,
colProps: { span: 12 },
rowProps: { gutter: [30, 0] },
layout: 'vertical',
}}
modalProps={{ width: 600 }}
requestParams={(params) => {
return {
...params,
category_id: currentCategory.id,
};
}}
searchShow={false}
handleFinish={async (values, mode, _form, defaultValue) => {
if (mode === 'create') {
await Create('/system/category/item', {
...values,
category_id: currentCategory.id,
});
window.$message?.success(t('system.category.item.createSuccess'));
} else {
await Update('/system/category/item/' + defaultValue?.id, {
...values,
category_id: currentCategory.id,
});
window.$message?.success(t('system.category.item.updateSuccess'));
}
return true;
}}
/>
</Space>
);
}
+66 -1
View File
@@ -1,8 +1,11 @@
import XinTable from '@/components/XinTable'; import XinTable from '@/components/XinTable';
import { Badge, Image, Typography } from 'antd'; import { Badge, Image, Tag, Typography } from 'antd';
import { useEffect, useState } from 'react';
import type { IGridNav } from '@/domain/iGridNav'; import type { IGridNav } from '@/domain/iGridNav';
import type { ICategory } from '@/domain/iCategory';
import type { XinTableColumn } from '@/components/XinTable/typings'; import type { XinTableColumn } from '@/components/XinTable/typings';
import type { ISysFileInfo } from '@/domain/iSysFile'; import type { ISysFileInfo } from '@/domain/iSysFile';
import { List } from '@/api/common/table';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
@@ -22,6 +25,20 @@ function getImageUrl(image: ISysFileInfo | string | null | undefined): string {
export default function GridNavPage() { export default function GridNavPage() {
const { t } = useTranslation(); const { t } = useTranslation();
// 分类下拉选项
const [categoryOptions, setCategoryOptions] = useState<{ label: string; value: number }[]>([]);
useEffect(() => {
List<ICategory>('/system/category', { pageSize: 1000 }).then((res) => {
const list = res.data?.data?.data ?? [];
setCategoryOptions(list.map((item) => ({
label: item.name,
value: item.id as number,
})));
}).catch(() => {
// 分类加载失败不影响页面使用
});
}, []);
const columns: XinTableColumn<IGridNav>[] = [ const columns: XinTableColumn<IGridNav>[] = [
{ {
title: t('system.gridNav.id'), title: t('system.gridNav.id'),
@@ -64,6 +81,25 @@ export default function GridNavPage() {
); );
}, },
}, },
{
title: t('system.gridNav.linkType'),
dataIndex: 'link_type',
valueType: 'select',
colProps: { span: 12 },
initialValue: 0,
rules: [{ required: true, message: t('system.gridNav.linkType.required') }],
fieldProps: {
options: [
{ label: t('system.gridNav.linkType.link'), value: 0 },
{ label: t('system.gridNav.linkType.category'), value: 1 },
],
},
render: (value: number) => {
return value === 1
? <Tag color="blue">{t('system.gridNav.linkType.category')}</Tag>
: <Tag>{t('system.gridNav.linkType.link')}</Tag>;
},
},
{ {
title: t('system.gridNav.link'), title: t('system.gridNav.link'),
dataIndex: 'link', dataIndex: 'link',
@@ -73,6 +109,35 @@ export default function GridNavPage() {
fieldProps: { fieldProps: {
placeholder: t('system.gridNav.link.placeholder'), placeholder: t('system.gridNav.link.placeholder'),
}, },
render: (_, record) => {
if(record.link_type === 1) {
const name = record.category?.name;
return name ? <Tag>{name}</Tag> : '-';
}
return record.link || '-'
},
dependency: {
dependencies: ['link_type'],
visible: (values) => values.link_type !== 1,
},
},
{
title: t('system.carousel.category'),
dataIndex: 'category_id',
valueType: 'select',
colProps: { span: 12 },
hideInSearch: true,
hideInTable: true,
fieldProps: {
options: categoryOptions,
showSearch: true,
optionFilterProp: 'label',
placeholder: t('system.carousel.category.placeholder'),
},
dependency: {
dependencies: ['link_type'],
visible: (values) => values.link_type === 1,
},
}, },
{ {
title: t('system.gridNav.status'), title: t('system.gridNav.status'),