Open robinhoo1973 opened 6 years ago
由于master-detail表单形式在一对多和多对多的数据结构中常常使用.所以在不改动目前架构代码的情况下加了个外挂,希望z-song可以加入下个版本,如果能直接修改Form和Grid模块通过对Form加上属性Modal属性和相应方法,对Grid加上属性Detail等属性和相应方法,应该比外挂更容易实现,而且bug会少很多.
下面先说我的外挂组成:分别是ModalForm.php和DetailGrid.php两个外挂类型用来实现modal form和内嵌表格的主要功能和设置,其中ModalForm.php会引用客制化view:admin.extensions.modal_form 三个文件分别的存储位置是,其中{$project_dir}是你项目的目录地址 {$project_dir}/vendor/encore/laravel-admin/src/ModalForm.php
<?php namespace Encore\Admin; use Closure; use Encore\Admin\Exception\Handler; use Illuminate\Database\Eloquent\Model as EloquentModel; use Encore\Admin\Form; use Encore\Admin\Form\Tools; use Encore\Admin\Form\Builder; use Encore\Admin\Form\Field; use Encore\Admin\Form\Field\File; use Encore\Admin\Form\Row; use Encore\Admin\Form\Tab; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Http\Request; use Illuminate\Support\Arr; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Input; use Illuminate\Support\MessageBag; use Illuminate\Support\Str; use Illuminate\Validation\Validator; use Spatie\EloquentSortable\Sortable; use Symfony\Component\HttpFoundation\Response; class ModalForm { public $form_name; public $modal_form; /** * Create a new form instance. * * @param $model * @param \Closure $callback */ protected function getModel($model) { if ($model instanceof EloquentModel) { return $model; } if (is_string($model) && class_exists($model)) { return $this->getModel(new $model()); } throw new InvalidArgumentException("$model is not a valid model"); } protected function formatFormName($form_name) { return 'Modal_Form_'.str_replace(' ','_',trim(ucwords(preg_replace('/([^(A-Za-z0-9)])+/',' ',$form_name)))); } public function __construct($model, $callback,$form_name='default') { $this->form_name = $this->formatFormName($form_name); $this->modal_form = new Form($this->getModel($model),$callback); $this->modal_form->disableSubmit(); $this->modal_form->disableReset(); $this->modal_form->setView('admin.extensions.modal_form'); } public function render() { $this->modal_form->builder()->getTools()->disableBackButton(); $this->modal_form->builder()->getTools()->disableListButton(); $render=preg_replace('/<form /i',"<form id='{$this->form_name}' ",$this->modal_form->render()); $render=str_replace('__MODAL_FORM_NAME__',"{$this->form_name}",$render); $render=str_replace('__SUBMIT__',trans('admin.submit'),$render); $render=str_replace('__RESET__',trans('admin.reset'),$render); $admin_new=trans('admin.new'); $admin_create=trans('admin.create'); $admin_edit=trans('admin.edit'); $admin_delete=trans('admin.delete'); $admin_delete_confirm=trans('admin.delete_confirm'); $admin_cancel=trans('admin.cancel'); $admin_confirm=trans('admin.confirm'); $script=<<<SCRIPT window.modal_form_mode=''; window.modal_form_id=''; window.modal_form=''; function registerLoadOptions() { var regEx=/(?:\\$\\(document\\)\\.on\\(\\'change\\',\\s*\\"\\.\\w+\\",\\s*function\\s*\\(\\)\\s*{\\s+)([^]*?)(?=\\.trigger)/g; var html=$('body').html(); var match; window.modal_load_options=[]; // console.log(window.modal_load_options); while ((match = regEx.exec(html)) !== null) { var loadRegEx=/var target = \\$\\(this\\)\\.closest\\(\\'\\.fields-group\\'\\)\\.find\\(\\"\\.(\w+)\\"\\)/; if (loadRegEx.exec(match[1])!== null) { var name=loadRegEx.exec(match[1])[1]; var loadUrlRegEx=/(?:\\$\\.get\\(\\")([^]*?)(?=\\"\\+this\\.value)/; var loadUrl=match[1].match(loadUrlRegEx); // console.log(loadUrlRegEx); // console.log(loadUrl); window.modal_load_options[name] = loadUrl[1]; } } // console.log(window.modal_load_options); } function selectOptions(key,value) { if (!Array.isArray(window.modal_load_options)) { registerLoadOptions(); } if (window.modal_load_options.hasOwnProperty(key)) { var target = window.modal_form.find('select[name=' + key + ']'); if (value === '') { target.find("option").remove(); window.modal_form.find('[name=' + key + ']').val(value).trigger('change.select2'); } else { $.when($.get(window.modal_load_options[key]+value, function (data) { target.find("option").remove(); // console.log(data); $(target).select2({ data: $.map(data, function (d) { d.id = d.id; d.text = d.text; return d; }) }).val(value).trigger('change.select2'); })).done(function(){ console.log('Options:['+key+']='+value+'Loaded!'); }); } return; } console.log('Form:['+window.modal_form.attr('id')+'] Options:['+key+']='+value+'Loaded!'); window.modal_form.find('[name=' + key + ']').val(value).trigger('change.select2'); } function domEquipment(key,value) { var dom = window.modal_form.find('[name=' + key + ']'); if (dom.is('select')) { selectOptions(key,value); } else { switch (dom.attr("type")) { case "text": case "hidden": case "textarea": dom.val(value); break; case "radio": case "checkbox": dom.val(value); dom.each(function() { if ($(this).attr('value') == value) { $(this).attr("checked", value); } }); break; } } } function populateForm(data) { console.log(data); $.each(JSON.parse(data), function(key, value) { domEquipment(key,value); }); } function apiModalEditData() { $.get(window.modal_form.attr('action'), function (data) { data=data.replace(/^\[/,'').replace(/\]$/,''); populateForm(data); }); } function resetError() { window.modal_form.find('.form-group').removeClass('has-error'); window.modal_form.find('label[for=inputError]').remove(); } function resetModalForm() { window.modal_form.find('input:text, input:password, textarea').val('').trigger('change'); window.modal_form.find('select').each(function(){ console.log($(this).attr('name')); selectOptions($(this).attr('name'),''); $(this).val('').trigger('change.select2'); }); window.modal_form.find('input:radio, input:checkbox').prop('checked', false).trigger('change'); window.modal_form.serializeArray().forEach(function(element){ console.log('['+element.name+']="'+element.value+'"'); }); // window.modal_form.attr('action',''); resetError(); } $('.grid-row-edit .fa-edit').unbind('click').click(function() { var id = $(this).data('id'); var apiUrl = $(this).data('url')+'/'+id+'?_ajax=1'; window.modal_form = $('#'+$(this).data('form')); window.modal_form.closest('.modal').find('.modal-title').text("{$admin_edit}"); window.modal_form_mode='edit'; resetModalForm(); window.modal_form.attr('action',apiUrl); console.log(window.modal_form.attr('action')); apiModalEditData(); window.modal_form.attr('action',$(this).data('url')+'/'+id); }); $('.grid-row-create').unbind('click').click(function() { var data=$(this).find('.fa-save'); window.modal_form = $('#'+data.data('form')); var apiUrl = data.data('url'); console.log(apiUrl); window.modal_form.closest('.modal').find('.modal-title').text("{$admin_new}"); window.modal_form_mode='create'; resetModalForm(); window.modal_form.attr('action',apiUrl); }); $('.grid-row-delete .fa-trash').unbind('click').click(function() { var id = $(this).data('id'); var apiUrl = $(this).data('url')+'/'+id; window.modal_form = $('#'+$(this).data('form')); swal({ title: "{$admin_delete_confirm}", type: "warning", showCancelButton: true, confirmButtonColor: "#DD6B55", confirmButtonText: "{$admin_confirm}", closeOnConfirm: false, cancelButtonText: "{$admin_cancel}" }, function(){ $.ajax({ method: 'post', url: apiUrl, data: { _method:'delete', _token:LA.token, }, success: function (data) { $.pjax.reload('#pjax-container'); if (typeof data === 'object') { if (data.status) { swal(data.message, '', 'success'); } else { swal(data.message, '', 'error'); } } } }); }); window.modal_form.closest('.modal').modal('hide'); }); $('.modal_form_button').unbind('click').click(function() { console.log($(this)); type=$(this).data('type'); window.modal_form=$('#'+$(this).data('form')); switch(type) { case 'reset': resetButtonClick(); break; case 'submit': submitButtonClick(); } }); function submitButtonClick(){ apiUrl=window.modal_form.attr('action'); fields={}; window.modal_form.serializeArray().forEach(function(element){ fields[element.name]=element.value; }); if (window.modal_form_mode=='edit') { fields['_method']='PUT'; } fields['_ajax']=1; console.log(fields); console.log(apiUrl); $.ajaxSetup({ headers: { 'X-CSRF-TOKEN': LA.token } }); $.ajax({ url: apiUrl, method: (window.modal_form_mode=='edit')?'put':'post', data: fields, success: function(data) { console.log(data); resetError(); if (typeof data === 'object') { if (data.status) { window.modal_form.closest('.modal').modal('hide'); $.pjax.reload('#pjax-container'); swal(data.message, '', 'success'); } else { for (var key in data.message) { if (data.message.hasOwnProperty(key)) { window.modal_form.find('[name='+key+']').closest('.form-group').addClass('has-error'); window.modal_form.find('[name='+key+']').closest('.col-sm-8').append(data.message[key]); console.log(key+':'); console.log(data.message[key]); } }; //swal(data.message, '', 'error'); } } }, error: function (xhr, ajaxOptions, thrownError) { console.log(xhr); console.log(thrownError); swal("Internal Error!\\n"+xhr.status+':'+xhr.statusText, '', 'error'); } }); } function resetButtonClick(){ console.log(window.modal_form_mode); var apiUrl=window.modal_form.attr('action'); resetModalForm(); if (window.modal_form_mode=='edit') { window.modal_form.attr('action',apiUrl+'?_ajax=1'); console.log(window.modal_form.attr('action')); apiModalEditData(); window.modal_form.attr('action',apiUrl); } } SCRIPT; Admin::script($script); return $render; } public function __toString() { return $this->render(); } }
{$project_dir}/vendor/encore/laravel-admin/src/DetailGrid.php
<?php namespace Encore\Admin; use Closure; use Encore\Admin\Grid; use Encore\Admin\Grid\Tools\AbstractTool; use Illuminate\Database\Eloquent\Model as Eloquent; use Illuminate\Support\Facades\Input; class DetailGridCreateButton extends AbstractTool { /** * Create a new CreateButton instance. * * @param Grid $grid */ public function __construct(Grid $grid) { $this->grid = $grid; } /** * Render CreateButton. * * @return string */ public function render() { return "<div class='btn-group pull-right' style='margin-right: 10px'><a href='' class='btn btn-sm btn-success grid-row-create' data-toggle='modal' data-target='#Main___MODAL_FORM_NAME__'><i class='fa fa-save' data-type='create' data-url='__URL__' data-form='__MODAL_FORM_NAME__'></i> ".trans('admin.new')."</a></div>"; } } class DetailGrid { protected $form_name; public $grid; protected function accessProtected($obj, $prop) { $reflection = new \ReflectionClass($obj); $property = $reflection->getProperty($prop); $property->setAccessible(true); return $property->getValue($obj); } public function getModel($model) { if ($model instanceof Eloquent) { return $model; } if (is_string($model) && class_exists($model)) { return $this->getModel(new $model()); } throw new InvalidArgumentException("{$model} is not a valid model"); } protected function formatFormName($form_name) { return 'Modal_Form_'.str_replace(' ','_',trim(ucwords(preg_replace('/([^(A-Za-z0-9)])+/',' ',$form_name)))); } public function __construct(Eloquent $model, Closure $callback, $form_name='default') { $this->form_name= $this->formatFormName($form_name); $this->grid = new Grid($model,$callback); $this->grid->disableCreation(); } public function setFormName($form_name) { $this->form_name = $this->formatFormName($form_name); } public function render() { //dd($this->grid); $this->grid->tools->prepend(new DetailGridCreateButton($this->grid)); //dd($this->grid); $render=$this->grid->render(); $render=str_replace('__MODAL_FORM_NAME__',"{$this->form_name}",$render); $render=str_replace('__URL__',"{$this->grid->resource()}",$render); //dd($render); return $render; } public function __toString() { return $this->render(); } }
{$project_dir}/resources/views/admin/extensions/modal_form.blade.php
<div class="modal" id="Main___MODAL_FORM_NAME__" tabindex="-1" role="dialog" aria-labelledby="__MODAL_FORM_NAME__Label" aria-hidden="true"> <div class="modal-dialog" role="document"> <div class="modal-content"> <div class="modal-header text-center"> <button type="button" class="close" data-dismiss="#Main___MODAL_FORM_NAME__" aria-label="Close" > <span aria-hidden="true">×</span> </button> <h4 class="modal-title w-100 font-weight-bold">{{ $form->title() }}</h4> </div> <div class="modal-body mx-3"> <!-- /.box-header --> <!-- form start --> @if($form->hasRows()) {!! $form->open() !!} @else {!! $form->open(['class' => "form-horizontal ModalForm"]) !!} @endif <div class="box-body"> @if(!$tabObj->isEmpty()) @include('admin::form.tab', compact('tabObj')) @else <div class="fields-group"> @if($form->hasRows()) @foreach($form->getRows() as $row) {!! $row->render() !!} @endforeach @else @foreach($form->fields() as $field) {!! $field->render() !!} @endforeach @endif </div> @endif </div> <!-- /.box-body --> <div class="box-footer"> @if( ! $form->isMode(\Encore\Admin\Form\Builder::MODE_VIEW) || ! $form->option('enableSubmit')) <input type="hidden" name="_token" value="{{ csrf_token() }}"> @endif <div class="col-md-{{$width['label']}}"> </div> <div class="col-md-{{$width['field']}}"> <div class="btn-group pull-right"> <button type="button" class="btn btn-info pull-right modal_form_button" data-type="submit" data-form="__MODAL_FORM_NAME__" data-loading-text="<i class='fa fa-spinner fa-spin '></i> __SUBMIT__">__SUBMIT__</button> </div> <div class="btn-group pull-left"> <button type="button" class="btn btn-warning modal_form_button" data-type="reset" data-form="__MODAL_FORM_NAME__">__RESET__</button> </div> </div> @foreach($form->getHiddenFields() as $hiddenField) {!! $hiddenField->render() !!} @endforeach </div> <!-- /.box-footer --> {!! $form->close() !!} </div> </div> </div> </div> <!-- /.modal -->
然后先创建DetailGrid所对应的Controller 比如说,你的供应商对应多个站点,你就先建一个SiteController,实现功能是创建一个针对某个供应商的CRUD页面,注意要设好你的rules和对应的messages,因为之后的ModalForm校验就是通过这个Controller的.然后引入ModalForm的trait替换掉原来的ModelForm的trait.这个新的trait文件主要是来处理接受从ModalForm和DetailGrid发起的CRUD事件请求,同时兼顾目前Controller的网页请求的完整性. 这个ModalForm.php的trait文件存放位置是: {$project_dir}/app/Admin/Controllers/Extensions/includes/ModalForm.php
<?php namespace App\Admin\Controllers\Extensions\includes; trait ModalForm { protected function accessProtected($obj, $prop) { $reflection = new \ReflectionClass($obj); $property = $reflection->getProperty($prop); $property->setAccessible(true); return $property->getValue($obj); } public function ajaxValidators($form) { $rules=[]; $messages=[]; foreach ($form->builder()->fields() as $field) { $rules[$this->accessProtected($field,'id')]=$this->accessProtected($field,'rules'); foreach($this->accessProtected($field,'validationMessages') as $rule=>$message) { $messages[$this->accessProtected($field,'id').'.'.$rule]=$message; } } $validator = \Validator::make(\Illuminate\Support\Facades\Input::all(),$rules,$messages); if ($validator->fails()) { $messages=[]; foreach($validator->getMessageBag()->toArray() as $key=>$errors) { $messages[$key]=''; foreach($errors as $error) { $messages[$key].="<label class='control-label' for='inputError'><i class='fa fa-times-circle-o'></i> {$error}<br/></label>"; } } return response([ 'status' => false, 'message' => $messages, ]); } return false; } /** * Display the specified resource. * * @param int $id * * @return \Illuminate\Http\Response */ public function show($id) { if (array_key_exists('_ajax',\Illuminate\Support\Facades\Input::all())) { $model=$this->form()->model(); $ret=$model->where($model->getKeyName(),'=',$id)->first(); return $ret?json_encode($ret):'{}'; } return $this->edit($id); } /** * Update the specified resource in storage. * * @param int $id * * @return \Illuminate\Http\Response */ public function update($id) { if (array_key_exists('_ajax',\Illuminate\Support\Facades\Input::all())) { $form=$this->form($id); if ($response=$this->ajaxValidators($form)) { return $response; } $this->form($id)->update($id); return response([ 'status' => true, 'message' => trans('admin.update_succeeded'), ]); } return $this->form($id)->update($id); } /** * Remove the specified resource from storage. * * @param int $id * * @return \Illuminate\Http\Response */ public function destroy($id) { if ($this->form()->destroy($id)) { return response()->json([ 'status' => true, 'message' => trans('admin.delete_succeeded'), ]); } else { return response()->json([ 'status' => false, 'message' => trans('admin.delete_failed'), ]); } } /** * Store a newly created resource in storage. * * @return \Illuminate\Http\Response */ public function store() { if (array_key_exists('_ajax',\Illuminate\Support\Facades\Input::all())) { $form=$this->form(); if ($response=$this->ajaxValidators($form)) { return $response; } $this->form()->store(); return response([ 'status' => true, 'message' => trans('admin.update_succeeded'), ]); } return $this->form()->store(); } }
然后创建你的Master-Detail表单,由于必须要有Master的记录才可以进行Detail记录的创建,所以我们主要是修改edit的方法,同时加上两个新的函数detailgrid和modalform.示例代码如下: 对edit方法的修改
public function edit($id) { return Admin::content(function (Content $content) use ($id) { $content->header('服务机构信息'); $content->description('服务机构信息维护'); $content->body($this->form($id)->edit($id).$this->detailGrid($id).$this->modalForm($id)); }); }
同时会应用到下面的一个include文件,没办法写外挂都这样!!! 重构Grid行Action按钮的代码 {$project_dir}/app/Admin/Controllers/Extensions/includes/DetailGridActions.php
<?php $actions->disableEdit(); $actions->disableDelete(); $actions->prepend("<a href='javascript:void(0);' class='grid-row-delete'><i class='fa fa-trash' data-url='{$actions->getResource()}' data-form='__MODAL_FORM_NAME__' data-id='{$actions->getKey()}'></i></a>\n"); $actions->prepend("<a href='javascript:void(0);'' data-toggle='modal' data-target='#Main___MODAL_FORM_NAME__' class='grid-row-edit'><i class='fa fa-edit' data-url='{$actions->getResource()}' data-form='__MODAL_FORM_NAME__' data-id='{$actions->getKey()}'></i></a>\n"); ?>
新增的两个方法,分别对应DetailGrid和ModalForm
protected function detailGrid($pid) { return new DetailGrid(new BackendAssitListViewSites, function (Grid $grid) use($pid){ $grid->resource(admin_base_path('/site')); $grid->model()->where('supp_id','=',$pid); $grid->supp_name('服务机构')->sortable(); $grid->province_name('省')->sortable(); $grid->city_name('市')->sortable(); $grid->area_name('区')->sortable(); $grid->site_name('站点名称')->sortable(); $grid->site_addr('站点地址')->sortable(); $grid->contact_info('联系人信息'); /**** ***** 下面的设置非常重要是告诉用来重构调用ModalForm的编辑和删除按钮!!! *****/ $grid->actions(function ($actions){ include(__DIR__.'/Extensions/includes/DetailGridActions.php'); }); }); } protected function modalForm($pid=null) { return new ModalForm(new \App\Models\Site, function (Form $form) use($pid) { /**** ***** 下面的设置非常重要是告诉AJAX从这个Resource地址获取CRUD操作!!! *****/ $form->resource(admin_base_path('/site')); /**** ***** 下面两行的设置非常重要是告诉AJAX从对应的子表主键和父表主键!!! *****/ $form->hidden('site_id'); $form->hidden('supp_id', '服务机构')->value($pid); /**** ***** 其实下面的代码可以从对应的Controler拷贝过来,而且可以移除rules *****/ $form->text('site_name','站点名称') ->rules('required', ['required'=>'必要字段站点名称不能为空!', 'unique' =>'出现重复站点名称!']); $form->select('province', '省') ->options(\App\Models\AreaCode::provinces()->pluck('area_name','area_id')) ->load('city',admin_base_path('/api/cities')) ->rules('required',['required'=>'必要字段不能为空!']); $form->select('city', '市') ->load('area',admin_base_path('/api/areas')) ->rules('required',['required'=>'必要字段不能为空!']) ->options(function($id) { return \App\Models\AreaCode::cities($id)->pluck('area_name','area_id'); }); $form->select('area', '区') ->rules('required',['required'=>'必要字段不能为空!']) ->options(function($id) { return \App\Models\AreaCode::areas($id)->pluck('area_name','area_id'); }); $form->text('site_addr','站点地址') ->rules('required',['required'=>'必要字段不能为空!']); $form->select('contact_id','机构联系人') ->options(\App\Models\Contact::contactInfo()->pluck('contact_info','contact_id')) ->rules('required',['required'=>'必要字段机构联系人不能为空!',]); }); }
截两张图:
由于master-detail表单形式在一对多和多对多的数据结构中常常使用.所以在不改动目前架构代码的情况下加了个外挂,希望z-song可以加入下个版本,如果能直接修改Form和Grid模块通过对Form加上属性Modal属性和相应方法,对Grid加上属性Detail等属性和相应方法,应该比外挂更容易实现,而且bug会少很多.
下面先说我的外挂组成:分别是ModalForm.php和DetailGrid.php两个外挂类型用来实现modal form和内嵌表格的主要功能和设置,其中ModalForm.php会引用客制化view:admin.extensions.modal_form 三个文件分别的存储位置是,其中{$project_dir}是你项目的目录地址 {$project_dir}/vendor/encore/laravel-admin/src/ModalForm.php
{$project_dir}/vendor/encore/laravel-admin/src/DetailGrid.php
{$project_dir}/resources/views/admin/extensions/modal_form.blade.php
然后先创建DetailGrid所对应的Controller 比如说,你的供应商对应多个站点,你就先建一个SiteController,实现功能是创建一个针对某个供应商的CRUD页面,注意要设好你的rules和对应的messages,因为之后的ModalForm校验就是通过这个Controller的.然后引入ModalForm的trait替换掉原来的ModelForm的trait.这个新的trait文件主要是来处理接受从ModalForm和DetailGrid发起的CRUD事件请求,同时兼顾目前Controller的网页请求的完整性. 这个ModalForm.php的trait文件存放位置是: {$project_dir}/app/Admin/Controllers/Extensions/includes/ModalForm.php
然后创建你的Master-Detail表单,由于必须要有Master的记录才可以进行Detail记录的创建,所以我们主要是修改edit的方法,同时加上两个新的函数detailgrid和modalform.示例代码如下: 对edit方法的修改
同时会应用到下面的一个include文件,没办法写外挂都这样!!! 重构Grid行Action按钮的代码 {$project_dir}/app/Admin/Controllers/Extensions/includes/DetailGridActions.php
新增的两个方法,分别对应DetailGrid和ModalForm