Dcat Admin 自定义 Form 表单实现后台系统配置内容的自定义,并可扩展配置项。
起因
- 之前在做项目的时候,后台管理扩展使用的是
Laravel-Admin
,需要在后台可以灵活添加和修改一些项目需求的配置。然后我在Github
上搜索了相关扩展,当时找到了这个 Config manager for laravel-admin。这个确实解决了我的燃眉之急,只不过这个配置比较简单,就是key => value
的配置,而且 value 的值也只是一个text
类型的input
框,不能使用时间区、下拉框、选择按钮、上传图片等控件。页面效果如下:
- 当时心想我怎么实现灵活的添加配置,并且可以任意的使用
form
表单控件,想了好久还是没有好的办法。这件事情一直搁在心里,直到最近才把它给做出来。
灵感
- 最近项目后台管理扩展使用的是
Dcat Admin
,这个是基于Laravel-Admin
的二次开发的,使用起来几乎没有障碍。UI界面比原先Laravel-Admin
的要好看,所以就用它了。 - 这次我还是需要在后台做可灵活添加的配置,然后我就看
Dcat Admin
的文档和演示案例。突然发现了一个页面是我想要的效果。页面如下:
- 然后我就获取了页面的预览代码,分析了一下。得出这个控制器里面有三个主要的方法,一个是
index()
,另外两个是form1()
和form2()
。大概意思就是通过tab栏
的选择来控制显示对应的表单页面。index 方法默认显示的是 form1 的页面。 - 虽然这个预览的代码显示的是两个
form
表单页面,但是只要你愿意,它还可以增加的新的表单,它的可扩展性是我所看重的一个点。
<?php
namespace App\Admin\Controllers;
...
class FormController extends Controller
{
// 首页
public function index(Content $content)
{
...
$content->row(function (Row $row) {
$type = request('_t', 1);
$tab = new Tab();
if ($type == 1) {
$tab->add('Form-1', $this->form1());
$tab->addLink('Form-2', request()->fullUrlWithQuery(['_t' => 2]));
} else {
$tab->addLink('Form-1', request()->fullUrlWithQuery(['_t' => 1]));
$tab->add('Form-2', $this->form2(), true);
}
$row->column(12, $tab->withCard());
});
return $content->header('Form');
}
// 表单一
protected function form1()
{
...
}
// 表单二
protected function form2()
{
...
}
}
着手
首先我们分析一下配置页面都需要做些什么。
第一: 它需要可以灵活增加Tab
栏和对应的Form
表单页面。
第二: 它需要可以将Form
表单填写的内容提交到后台,并且可入库。
第三:Form
表单需要展示入库后的各个表单项对应的值,这样方便观察表单项的配置信息。创建
SystemCofig
模型,并且增加 migration 文件。
php artisan make:model SystemConfig -m
- 迁移文件的内容为:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateSystemConfigsTable extends Migration
{
public function up()
{
Schema::create('system_configs', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->text('value');
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('system_configs');
}
}
- 创建一个新的
SystemController
控制器。
php artisan make:controller App/Admin/Controllers/SystemController
- 在路由文件中添加对应控制器的路由。
// 系统配置路由
$router->resource('system-configs', SystemConfigController::class)->only(['index', 'store']);
将预览代码复制到新增的控制器中,不过要移除里面好多不能使用的内容,比如:一些无用的
use
和一些不存在的类等等。首先处理可灵活配置
Tab
栏。
...
const CONFIG_TYPE = [
'config_one' => [
'name' => '配置表单-1',
'method' => 'config_one',
],
'config_two' => [
'name' => '配置表单-2',
'method' => 'config_two',
],
// 可以新增配置项,新增完需要添加 method 方法才行
];
// 配置首页
public function index(Content $content)
{
$content->row(function (Row $row) {
$type = request('type', 'config_one');
$tab = new Tab();
// 获取数据库存储对应配置的信息
$configArr = json_decode(SystemConfig::query()->where('name' , '=', $type)->value('value'), true);
// 循环所有配置项
foreach (self::CONFIG_TYPE as $k => $item) {
if ($k === $type) {
$tab->add($item['name'], $this->{$item['method']}($configArr), true);
} else {
$tab->addLink($item['name'], admin_route('system-configs.index', ['type' => $k]));
}
}
$row->column(12, $tab->withCard());
});
return $content->header('配置管理');
}
- 增加第一个
Form
表单。
/**
* 第一个 form 表单
* 方法名为了简单表示协程 config_one, 可替换为自己业务的名称,并在 self::CONFIG_TYPE 中的配置保持一致
* @param $configArr
* @return string
*/
protected function config_one($configArr = [])
{
$form = new Form();
$form->disableHeader(); // 隐藏 header
$form->disableViewCheck();
$form->disableCreatingCheck();
$form->disableEditingCheck();
$form->disableResetButton();
$form->action(admin_route('system-configs.store')); // 设置表单提交的方法地址
// 单个写入框的配置
$form->text('config_one.text', '标题')->required()->default(Arr::get($configArr, 'text'));
$form->password('config_one.password', '密码')->required()->default(Arr::get($configArr, 'password'));
$form->email('config_one.email', '邮箱')->default(Arr::get($configArr, 'email'));
$form->mobile('config_one.mobile', '手机')->default(Arr::get($configArr, 'mobile'));
$form->url('config_one.url', '网址')->default(Arr::get($configArr, 'url'));
$form->ip('config_one.ip', 'ip地址')->default(Arr::get($configArr, 'ip'));
$form->color('config_one.color', '颜色选择')->default(Arr::get($configArr, 'color'));
$form->divider(); // 分割线
$form->selectTable('config_one.select-table', '下拉表格')
->title('User')
->from(UsersTable::make())
->model(User::class, 'id', 'name')->default(Arr::get($configArr, 'select-table'));
$form->multipleSelectTable('config_one.select-resource-multiple', '多选下拉表格')
->title('User')
->max(4)
->from(UsersTable::make())
->model(User::class, 'id', 'name')->default(Arr::get($configArr, 'select-resource-multiple'));
$form->icon('config_one.icon', 'icon样式')->default(Arr::get($configArr, 'icon'));
$form->rate('config_one.rate', 'rate比率')->default(Arr::get($configArr, 'rate'));
$form->decimal('config_one.decimal', '金额')->default(Arr::get($configArr, 'decimal'));
$form->number('config_one.number', '数字')->default(Arr::get($configArr, 'number'));
$form->currency('config_one.currency', '美元')->default(Arr::get($configArr, 'currency'));
$form->switch('config_one.switch', '开关')->default(1)->default(Arr::get($configArr, 'switch'));
$form->divider(); // 分割线
// 单个时间选择框的配置
$form->date('config_one.date', '日期')->default(Arr::get($configArr, 'date'));
$form->time('config_one.time', '时间')->default(Arr::get($configArr, 'time'));
$form->datetime('config_one.datetime', '时分秒')->default(Arr::get($configArr, 'datetime'));
// 区间时间选择框的配置
$form->dateRange('config_one.date-start', 'config_one.date-end', '日期区间1')->default([
'start' => Arr::get($configArr, 'date-start'),
'end' => Arr::get($configArr, 'date-end'),
]);
$form->timeRange('config_one.time-start', 'config_one.time-end', '时间区间2')->default([
'start' => Arr::get($configArr, 'time-start'),
'end' => Arr::get($configArr, 'time-end'),
]);
$form->datetimeRange('config_one.datetime-start', 'config_one.datetime-end', '时分秒区间3')->default([
'start' => Arr::get($configArr, 'datetime-start'),
'end' => Arr::get($configArr, 'datetime-end'),
]);
// 文本域的配置
$form->textarea('config_one.textarea', '文本域')->default(Arr::get($configArr, 'textarea'));
$form->divider();
// 二维数组的配置
$form->table('config_one.table', function (NestedForm $table) {
$table->text('key', '名字');
$table->text('value', '分数');
$table->text('desc', '说明');
})->default(Arr::get($configArr, 'table'));
return "<div style='padding:10px 8px'>{$form->render()}</div>";
}
- 增加第二个
Form
表单。
/**
* 第二个 form 表单
* 方法名为了简单表示协程 config_two, 可替换为自己业务的名称,并在 self::CONFIG_TYPE 中的配置保持一致
* @param $configArr
* @return string
*/
protected function config_two($configArr = [])
{
$form = new Form();
$form->disableHeader(); // 隐藏 header
$form->disableViewCheck();
$form->disableCreatingCheck();
$form->disableEditingCheck();
$form->disableResetButton();
$form->action(admin_route('system-configs.store')); // 设置表单提交的方法地址
$names = $this->createNames(); // 获取用户表名称 - 可以自己更换业务内容
// 标签和单选多选下拉框的配置
$form->tags('config_two.tag', '标签')->options($names)->default(Arr::get($configArr, 'tag'));
$form->select('config_two.select', '单选下拉框')->options($names)->default(Arr::get($configArr, 'select'));
$form->multipleSelect('config_two.multiple-select', '多选下拉选框')->options($names)->options($names)->default(Arr::get($configArr, 'multiple-select'));;
// 图片和文件上传的配置
$form->image('config_two.image', '图片上传')->default(Arr::get($configArr, 'image'));
$form->multipleFile('config_two.multiple-file', '多文件上传')->limit(3)->default(Arr::get($configArr, 'multiple-file'));
// 单选多选按钮的配置
$form->checkbox('config_two.checkbox', '多选盒子')->options(['GET', 'POST', 'PUT', 'DELETE'])->canCheckAll()->default(Arr::get($configArr, 'checkbox'));
$form->radio('config_two.radio', '单选按钮')->options(['GET', 'POST', 'PUT', 'DELETE'])->default(Arr::get($configArr, 'radio'));
// 菜单树的配置
$menuModel = config('admin.database.menu_model');
$menuModel = new $menuModel;
$form->tree('config_two.tree', '选择数')
->setTitleColumn('title')
->nodes($menuModel->allNodes())->default(Arr::get($configArr, 'tree'));
// 列表盒子的配置
$form->listbox('config_two.listbox', '列表盒子')->options($names)->default(Arr::get($configArr, 'listbox'));
$form->editor('config_two.editor', '编辑器')->default(Arr::get($configArr, 'editor'));
return "<div style='padding:9px 8px'>{$form->render()}</div>";
}
- 这里面用到了一些数据,是从
User
模型中获取的,执行一下User
工厂的种子命令(千万注意,别再生产环境使用此命令,自己添加一些就行。):
php artisan db:seed
- 在使用
Form
表单的时候,碰见了一个坑,就是复制的预览代码中,Form 引用的是:use Dcat\Admin\Widgets\Form;
,但是在时间区间 $form->dateRange()、$form->timeRange()、$form->datetimeRange() 控件中没办法设置默认值。所以到最后我使用了use Dcat\Admin\Form;
可以了。为此专门在论坛提了一个问题:
Dcat Admin 有办法给 $form->dateRange(‘column’)、 $form->timeRange(‘column’) 等区间组件设置默认值么?
use Dcat\Admin\Form;
//use Dcat\Admin\Widgets\Form;
不过还有一个问题就是,use Dcat\Admin\Form;
默认有一个 header
头,把它隐藏就行。
$form = new Form();
$form->disableHeader(); // 隐藏 header
- 我这边为了存入的数据在页面上展示,统一使用了
Form
表单的default()
方法进行设置的。
$form = new Form();
$form->text('config_one.text', '标题')->required()->default(Arr::get($configArr, 'text'));
// 区间时间选择框的配置
$form->dateRange('config_one.date-start', 'config_one.date-end', '日期区间1')->default([
'start' => Arr::get($configArr, 'date-start'),
'end' => Arr::get($configArr, 'date-end'),
]);
接下来的是,表单提交的方法:
<?php namespace App\Admin\Controllers; // 引入上传文件的 Trait use Dcat\Admin\Form\Field\UploadField; //use Dcat\Admin\Http\Controllers\AdminController; // 引入后台基类控制器——>代替AdminController成为当前类继承的父类。 use Illuminate\Routing\Controller; class SystemConfigController extends Controller { use UploadField; // 引入这个上传文件的 Trait /** * 配置提交 * @return JsonResponse|mixed */ public function store() { // 获取请求过来的值 $request = request(); if (empty($allData = $request->all())) { return JsonResponse::make()->error('没有数据提交!'); } if ($request->hasFile('_file_')) { try { // 如果有图片传入 或者 如果有文件传入 return $this->upload($request->file('_file_')); } catch (\Exception $e) { return JsonResponse::make()->error('上传失败:' . $e->getMessage()); } } // 循环数据 foreach ($allData as $k => $v) { if (!in_array($k, array_keys(self::CONFIG_TYPE))) { continue; } SystemConfig::query()->updateOrCreate(['name' => $k], ['name' => $k, 'value' => json_encode($v)]); } return JsonResponse::make()->success('提交成功!')->refresh(); } }
这里面除了文件和图片的上传需要单独处理,其他的正常入库就行。
- 因为我这个
Form
表单是自定义的,里面的文件上传请求的还是控制器的store()
方法,因为我重写了store()
方法,我的方法中没有对应的文件上传的处理,然后我就根据编辑器的Ctrl + 点击
进入到源代码中,查找图片上传的逻辑代码。
// 原先 AdminController 基类里面的 store 方法
public function store()
{
return $this->form()->store();
}
protected function form()
{
// (Ctrl + 点击)这个 Form 进去找 image 的图片上传的处理方法
return Form::make(new User(), function (Form $form){
...
});
}
- 源代码的内容:
vendor\dcat\laravel-admin\src\Form.php
找到 image 和 file 的配置:
protected static $availableFields = [
...
'file' => Field\File::class,
'image' => Field\Image::class,
...
]
- 点击进
Field\Image::class
类中,里面有prepareFile()
方法:
/**
* @param UploadedFile $file
*/
protected function prepareFile(UploadedFile $file)
{
$this->callInterventionMethods($file->getRealPath(), $file->getMimeType());
$this->uploadAndDeleteOriginalThumbnail($file);
}
- 然后我点击
prepareFile()
方法,看哪里使用了,结果就找到了\vendor\dcat\laravel-admin\src\Form\Field\UploadField.php
这个文件,里面有一个upload()
方法,这个就是上传图片和文件走的方法。
public function upload(UploadedFile $file)
{
$request = request();
$id = $request->get('_id');
if (! $id) {
return $this->responseErrorMessage('Missing id');
}
if ($errors = $this->getValidationErrors($file)) {
return $this->responseValidationMessage($errors);
}
$this->name = $this->getStoreName($file);
$this->renameIfExists($file);
$this->prepareFile($file);
if (! is_null($this->storagePermission)) {
$result = $this->getStorage()->putFileAs($this->getDirectory(), $file, $this->name, $this->storagePermission);
} else {
$result = $this->getStorage()->putFileAs($this->getDirectory(), $file, $this->name);
}
if ($result) {
$path = $this->getUploadPath();
$url = $this->objectUrl($path);
// 上传成功
return $this->responseUploaded($this->saveFullUrl ? $url : $path, $url);
}
// 上传失败
throw new UploadException(trans('admin.uploader.upload_failed'));
}
use Dcat\Admin\Form\Field\UploadField;
这个引用很重要,文件上传需要这个引用。(费好大劲翻源码找到的方法)但是它有个 destroy 方法和 AdminController 控制器的冲突了。到最后我取消继承 AdminController 控制器改为 Controller 控制器了。
结束
- 到此为止,这个后台可配置的逻辑已经写完了。
- 不过还可以在
SystemConfig
模型增加配置的调用,将常用的配置缓存到Redis
中,这样可以任意的在程序里面快速的调用配置了。不过需要注意的是,后台配置修改,要清理Redis
缓存中配置才行。
本作品采用《CC 协议》,转载必须注明作者和本文链接
您好,请问是用什么版本的dcat-admin呢,我用的dcat-admin 2.2.2,图片上传会报其他错误