Dcat Admin 自定義 Form 表單實現後臺系統配置內容的自定義,並可擴充套件配置項。

laravel_peng發表於2022-03-27

配置演示

可配置專案程式碼Gitee地址

起因

  • 之前在做專案的時候,後臺管理擴充套件使用的是 Laravel-Admin,需要在後臺可以靈活新增和修改一些專案需求的配置。然後我在 Github 上搜尋了相關擴充套件,當時找到了這個 Config manager for laravel-admin。這個確實解決了我的燃眉之急,只不過這個配置比較簡單,就是 key => value 的配置,而且 value 的值也只是一個 text 型別的 input 框,不能使用時間區、下拉框、選擇按鈕、上傳圖片等控制元件。頁面效果如下:

Dcat Admin 自定義 Form 表單實現後臺系統配置內容的自定義

  • 當時心想我怎麼實現靈活的新增配置,並且可以任意的使用 form 表單控制元件,想了好久還是沒有好的辦法。這件事情一直擱在心裡,直到最近才把它給做出來。

靈感

  • 最近專案後臺管理擴充套件使用的是 Dcat Admin,這個是基於 Laravel-Admin 的二次開發的,使用起來幾乎沒有障礙。UI介面比原先 Laravel-Admin 的要好看,所以就用它了。
  • 這次我還是需要在後臺做可靈活新增的配置,然後我就看 Dcat Admin 的文件和演示案例。突然發現了一個頁面是我想要的效果。頁面如下

Dcat Admin 自定義 Form 表單實現後臺系統配置內容的自定義

  • 然後我就獲取了頁面的預覽程式碼,分析了一下。得出這個控制器裡面有三個主要的方法,一個是 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 表單實現後臺系統配置內容的自定義

Dcat Admin 有辦法給 $form->dateRange(‘column’)、 $form->timeRange(‘column’) 等區間元件設定預設值麼?

use Dcat\Admin\Form;
//use Dcat\Admin\Widgets\Form;

不過還有一個問題就是,use Dcat\Admin\Form; 預設有一個 header 頭,把它隱藏就行。

Dcat Admin 自定義 Form 表單實現後臺系統配置內容的自定義

$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();
      }
    }
  • 這裡面除了檔案和圖片的上傳需要單獨處理,其他的正常入庫就行。

Dcat Admin 自定義 Form 表單實現後臺系統配置內容的自定義

Dcat Admin 自定義 Form 表單實現後臺系統配置內容的自定義

  • 因為我這個 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 協議》,轉載必須註明作者和本文連結
Xiao Peng

相關文章