demo.laravel-admin.org
demo.laravel-admin.org copied to clipboard
基于目前架构的Master-Detail实现方法分享
由于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'=>'必要字段机构联系人不能为空!',]);
});
}
截两张图: