Files
xk-api-yii/admin/components/UI/Form.php
2024-09-19 11:32:39 +08:00

987 lines
28 KiB
PHP

<?php
namespace admin\components\UI;
use admin\components\UI\Form\Node;
use admin\components\UI\Widget\Icon;
use admin\components\UI\yii\Str;
use Dotenv\Validator;
use yii\db\ActiveRecord;
use Illuminate\Support\Collection;
/**
* 表单UI
* Class Form
* @package backend\components\UI
* @method Form\Cascader cascader(string $name, string $field, $data = null, string $has = '')
* @method Form\CheckBox checkbox(string $name, string $field, $data = null, string $has = '')
* @method Form\Choice choice(string $name, string $field, string $has = '')
* @method Form\Color color(string $name, string $field, string $has = '')
* @method Form\Data data(string $name, string $field, string $has = '')
* @method Form\Date date(string $name, string $field, string $has = '')
* @method Form\Daterange daterange(string $name, string $field, string $has = '')
* @method Form\Datetime datetime(string $name, string $field, string $has = '')
* @method Form\Editor editor(string $name, string $field, string $has = '')
* @method Form\Email email(string $name, string $field, string $has = '')
* @method Form\File file(string $name, string $field, string $has = '')
* @method Form\Image image(string $name, string $field, string $has = '')
* @method Form\Images images(string $name, string $field, string $has = '')
* @method Form\Ip ip(string $name, string $field, string $has = '')
* @method Form\Password password(string $name, string $field, string $has = '')
* @method Form\Radio radio(string $name, string $field, $data = null, string $has = '')
* @method Form\Select select(string $name, string $field, $data = null, string $has = '')
* @method Form\Tree tree(string $name, string $field, $data = null, string $has = '')
* @method Form\Toggle toggle(string $name, string $field, string $has = '')
* @method Form\Tags tags(string $name, string $field, string $has = '')
* @method Form\Tel tel(string $name, string $field, string $has = '')
* @method Form\Text text(string $name, string $field, string $has = '')
* @method Form\Number number(string $name, string $field, string $has = '')
* @method Form\Time time(string $name, string $field, string $has = '')
* @method Form\Url url(string $name, string $field, string $has = '')
* @method Form\Textarea textarea(string $name, string $field, string $has = '')
* @method Form\Location location(string $name, string $field, string $has = '')
*/
class Form
{
public $model;
public $modelElo;
public $info;
protected string $title = '';
protected bool $back = true;
protected array $attr = [];
protected array $extend = [];
protected string $method = 'post';
protected string $action = '';
protected array $keys = [];
protected array $row = [];
protected array $flow = [];
protected array $assign = [];
protected array $script = [];
protected array $scriptReturn = [];
protected array $sideNode = [];
protected array $side = ['left'=>[],'right'=>[]];
protected bool $dialog = false;
protected bool $vertical = true;
protected array $map = [];
public Collection $element;
/**
* Form constructor.
* @param $data
* @param bool $model
*/
public function __construct($data = null, bool $model = true)
{
if (!$model) {
// 虚拟数据
$this->info = $data;
} else {
// 数据模型
if ($data instanceof ActiveRecord) {
$this->model = $data;
$this->modelElo = $data;
$this->info = $data->getAttributes();
} else {
$this->info = $data;
}
}
$this->element = Collection::make();
if (\Yii::$app->request->getHeaders()->has('x-dialog')) {
$this->dialog = true;
}
}
/**
* 设置条件主键
* @param $key
* @param $value
*/
public function setKey($key, $value): void
{
if ($key && $value) {
$this->keys[$key] = $value;
}
if (!$this->model) {
return;
}
$this->setInfo();
}
/**
* 获取当前数据
* @return array|ActiveRecord
*/
public function info()
{
return $this->info;
}
/**
* 模型对象
* @return ActiveRecord
*/
public function model(): ActiveRecord
{
return $this->model;
}
/**
* 模型对象
* @return ActiveRecord
*/
public function modelElo(): ?ActiveRecord
{
return $this->modelElo;
}
/**
* 获取元素集合
* @param null $class
*/
public function getElement($class = null, $num = 0): Collection
{
if ($class) {
$i = 0;
foreach ($this->element as $vo) {
if ($vo instanceof $class) {
if ($i === $num) {
return $vo;
}
$i++;
}
}
}
return $this->element;
}
/**
* 表单标题
* @param string $title
* @param bool $back
* @return $this
*/
public function title(string $title, bool $back = true): self
{
$this->title = $title;
$this->back = $back;
return $this;
}
/**
* 附加脚本
* @param string $content
* @param string $return
* @return $this
*/
public function script(string $content = '', string $return = ''): self
{
$this->script[] = $content;
$this->scriptReturn[] = $return;
return $this;
}
/**
* 附加属性
* @param $name
* @param $value
* @return $this
*/
public function attr($name, $value): Form
{
$this->attr[] = $name . '="' . $value . '"';
return $this;
}
/**
* 多行组件
* @return Form\Row
*/
public function row(): Form\Row
{
$data = new Form\Row();
$data->dialog($this->dialog);
$data->vertical($this->vertical);
$this->element->push($data);
return $data;
}
/**
* 切换组件
* @return Form\Tab
*/
public function tab(): Form\Tab
{
$data = new Form\Tab();
$data->dialog($this->dialog);
$data->vertical($this->vertical);
$this->element->push($data);
return $data;
}
/**
* 卡片组件
* @param $callback
* @return Form\Card
*/
public function card($callback): Form\Card
{
$data = new Form\Card($callback);
$data->dialog($this->dialog);
$data->vertical($this->vertical);
$this->element->push($data);
return $data;
}
/**
* Html内容
* @param $name
* @param $callback
* @return Form\Html
*/
public function html($name, $callback): Form\Html
{
$data = new Form\Html($name, $callback);
$data->dialog($this->dialog);
$data->vertical($this->vertical);
$this->element->push($data);
return $data;
}
/**
* 布局组件
* @param $callback
* @return Form\Layout
*/
public function layout($callback): Form\Layout
{
$data = new Form\Layout($callback);
$data->dialog($this->dialog);
$this->element->push($data);
return $data;
}
// 边栏元素
public function side($callback, string $direction = 'left'): self
{
$this->sideNode[] = [
'callback' => $callback,
'direction' => $direction
];
return $this;
}
/**
* 设置字段映射
* @param array $map
* @return $this
*/
public function map(array $map): self
{
$this->map = array_merge($this->map, $map);
return $this;
}
/**
* 获取表单数据
*/
public function renderData($info)
{
$collection = Collection::make();
$this->element->map(function ($item) use ($collection, $info) {
$data = $item->getData($info);
foreach ($data as $key => $vo) {
$collection->put($key, $vo);
}
});
if ($this->map) {
foreach ($this->map as $k => $v) {
$key = is_int($k) ? str_replace(['.', '->'], '_', $v) : $k;
$vo = is_callable($v) ? call_user_func($v, $info) : Tools::parsingArrData($info,$v);
$collection->put($key, $vo);
}
}
return $collection->toArray();
}
/**
* @return array
*/
public function renderForm(): array
{
return $this->element->map(function ($vo, $key) {
$sort = $vo->getSort();
$sort = $sort ?? $key;
$groupRule = $vo->getGroup();
$group = [];
foreach ($groupRule as $rule) {
if (is_array($rule['value'])) {
$value = json_encode($rule['value']);
$group[] = "{$value}.indexOf(data.{$rule['name']}) !== -1";
}else {
$group[] = "data.{$rule['name']} == '{$rule['value']}'";
}
}
$group = $group ? implode(' || ', $group) : null;
if ($vo instanceof Form\Composite) {
$node = [
'nodeName' => 'div',
'child' => $vo->getRender(),
'sort' => $sort,
];
if ($group) {
$node['vIf'] = $group;
}
return array_merge($node, $vo->getLayoutAttr());
}
$helpNode = [];
$prompt = $vo->getPrompt();
$help = $vo->getHelp();
if ($prompt) {
$helpNode = [
'nodeName' => 'a-tooltip',
'class' => 'ml-3',
'position' => 'top',
'content' => $vo->getPrompt(),
'child' => [
'nodeName' => 'span',
'child' => [
'nodeName' => 'icon-question-circle'
]
],
];
}
if ($help) {
$helpNode = [
'nodeName' => 'div',
'class' => 'text-gray-300 pt-2 pb-2 ml-3',
'child' => $help
];
}
$helpLine = $vo->getHelpLine();
$must = $vo->getMust();
$item = [
'nodeName' => 'a-form-item',
'label' => $vo->getName(),
'field' => $vo->getField(),
'vIf' => $group,
'sort' => $sort,
'child' => [
$vo->getRender(),
$helpLine ? [
'vSlot:help' => '',
'nodeName' => 'div',
'child' => $helpLine
] : [],
$helpNode ? [
'nodeName' => 'div',
'class' => 'ml-2',
'child' => $helpNode
] : []
]
];
if ($must) {
$item['rules'] = [
[
'required' => true,
'message' => '请填写' . $vo->getName()
]
];
}
return $item;
})->filter()->sortBy('sort')->values()->toArray();
}
/**
* @return mixed|void|null
* @throws \Exception
*/
public function setInfo()
{
if ($this->info) {
return $this->info;
}
if ($this->keys) {
$model = $this->model();
foreach ($this->keys as $key => $value) {
$model->where($key, $value);
}
$info = $model->find()->one();
if (empty($info)) {
throw new \Exception('内容不存在');
}
} else {
$info = [];
}
$this->info = $info;
}
/**
* 提交类型
* @param string $name
* @return $this
*/
public function method(string $name = 'post'): self
{
$this->method = $name;
return $this;
}
/**
* 指定模板变量
* @param string $name
* @param null $value
* @return $this
*/
public function assign(string $name, $value = null): self
{
$this->assign[$name] = $value;
return $this;
}
/**
* 是否弹窗
* @param bool $status
* @return $this
*/
public function dialog(bool $status): self
{
$this->dialog = $status;
$this->vertical = true;
return $this;
}
/**
* 纵向表单
* @param bool $status
* @return $this
*/
public function vertical(bool $status): self
{
$this->vertical = $status;
return $this;
}
/**
* 获取弹窗状态
* @return bool
*/
public function getDialog(): bool
{
return $this->dialog;
}
/**
* 保存链接
* @param $uri
* @return $this
*/
public function action($uri): self
{
$this->action = $uri;
return $this;
}
/**
* 渲染表单(数组)
* @return array
*/
public function renderArray()
{
$params = \Yii::$app->request->getQueryParams();
$action = route($this->action, $params);
// 提交地址
//if ($this->action) {
// $action = $this->action;
//} else {
// $params = \Yii::$app->request->getQueryParams();
// if ($this->modelElo) {
// $key = $this->modelElo->getKeyName();
// $id = $this->info->$key;
// $params['id'] = $id;
// }
// $action = route($this->action, $params);
//}
$node = new Node($action, $this->method, $this->title);
$node->dialog($this->dialog);
$node->vertical($this->vertical);
$node->back($this->back);
// 表单元素·
$node->element($this->renderForm());
// 表单数据
$node->data($this->renderData($this->info));
// 边栏元素
foreach ($this->sideNode as $vo) {
$node->side($vo['callback'], $vo['direction']);
}
// 处理附加js
foreach ($this->script as $key => $value) {
$node->script($value, $this->scriptReturn[$key]);
}
return $node->render();
}
public function renderFormWithoutNode(){
$params = \Yii::$app->request->getQueryParams();
$action = route($this->action, $params);
// 提交地址
//if ($this->action) {
// $action = $this->action;
//} else {
// $params = \Yii::$app->request->getQueryParams();
// if ($this->modelElo) {
// $key = $this->modelElo->getKeyName();
// $id = $this->info->$key;
// $params['id'] = $id;
// }
// $action = route($this->action, $params);
//}
// 处理附加js
$script = [];
foreach ($this->script as $key => $value) {
if ($value instanceof \Closure) {
$script[] = $value();
} else {
$script[] = $value;
}
}
return [
'nodeName' => 'app-form',
'url' => $action,
'method' => $this->method,
'value' => $this->renderData($this->info),
'layout' => $this->vertical ? 'vertical' : 'horizontal',
'back' => $this->back,
'child' => [
'nodeName' => 'div',
'class' => 'flex',
'vSlot' => '{value: data, submitStatus: loading}',
'child' => $this->dialog ? $this->renderDialog() : $this->renderPage()
]
];
}
/**
* 获取提交数据
* @param $time
* @return Collection
*/
public function getInput($time): Collection
{
// 获取提交数据
$data = \Yii::$app->request->post();
// 提交数据处理
if ($this->flow['submit']) {
foreach ($this->flow['submit'] as $item) {
$data = $item($data, $time);
}
}
// 过滤数据
$collection = Collection::make();
$this->element->map(function ($item) use ($collection, $time) {
$inputs = $item->getInput($time);
foreach ($inputs as $key => $vo) {
$collection->put($key, $vo);
}
});
//验证数据
$rules = [];
$msgs = [];
$collection->map(function ($item) use (&$rules, &$msgs) {
if ($item['verify']['rule']) {
$rules = $rules + $item['verify']['rule'];
}
if ($item['verify']['msg']) {
$msgs = $msgs + $item['verify']['msg'];
}
});
$validator = \Validator::make($data, $rules, $msgs);
if ($this->flow['validator']) {
foreach ($this->flow['validator'] as $vo) {
$vo($validator);
}
}
$validator->validate();
// 格式化数据
return $collection->map(function ($item) {
$value = $item['value'];
if ($item['format']) {
foreach ($item['format'] as $vo) {
$value = call_user_func($vo, $item['value']);
}
}
return ['value' => $value, 'has' => $item['has'], 'pivot' => $item['pivot']];
});
}
/**
* 流程时间
* @var array
*/
protected array $prepared = [];
/**
* 主键值
* @var null
*/
public $modelId = null;
/**
* 保存数据
* @return null $modelId
*/
public function save()
{
// 获取主键数据
$id = 0;
if ($this->modelElo) {
$id = $this->keys[$this->modelElo->getPrimaryKey()];
}
// 保存类型
$type = $id ? 'edit' : 'add';
// 获取提交数据
$data = $this->getInput($type);
// 提取提交数据
$formatData = [];
foreach ($data as $key => $vo) {
$formatData[$key] = $vo['value'];
}
$formatData = collect($formatData);
// 非模型返回集合
if (!$this->modelElo) {
return $formatData;
}
// 获取模型对象
if ($type === 'add') {
$model = $this->modelElo;
} else {
$model = $this->modelElo->find($id);
}
// 保存数据库
\Yii::$app->db->transation(function () use ($model, $data, $type, $formatData) {
// 保存前置回调
if ($this->flow['front']) {
foreach ($this->flow['front'] as $item) {
$ret = $item($formatData, $type, $model);
if ($ret instanceof ActiveRecord) {
$model = $ret;
}
}
}
// 树形处理 已废弃 先设置 scoped 数据 再设置上级数据
/*if ($model->parent_id) {
if (method_exists($model, 'appendToNode')) {
$model = $model->appendToNode($this->modelElo->find($formatData['parent_id']));
}
}*/
$data->map(function ($item, $key) use ($model) {
$has = $item['has'];
// 查询关联对象
if (method_exists($model, $has) && !is_null($item['value'])) {
$relation = $model->$has();
// 多对多
if ($relation instanceof \Illuminate\Database\Eloquent\Relations\BelongsToMany) {
$this->prepared[] = static function ($model) use ($item) {
$sync = is_array($item['value']) ? $item['value'] : [$item['value']];
$syncFormat = [];
if ($item['pivot']) {
foreach ($sync as $vo) {
$syncFormat[$vo] = $item['pivot'];
}
$sync = $syncFormat;
}
$model->{$item['has']}()->sync($sync);
};
}
} else if ($model::getTableSchema()->getColumn($key)) {
// 过滤无用字段
$model->$key = $item['value'];
}
});
// 保存前置回调
if ($this->flow['before']) {
foreach ($this->flow['before'] as $item) {
$ret = $item($formatData, $type, $model);
if ($ret instanceof ActiveRecord) {
$model = $ret;
}
}
}
$model->save();
// 同步关联数据
foreach ($this->prepared as $callback) {
$callback($model);
}
// 保存后置回调
if ($this->flow['after']) {
foreach ($this->flow['after'] as $item) {
$item($formatData, $type, $model);
}
}
});
$this->modelId = $model->getKey();
return $this->modelId;
}
/**
* 处理数据之前提交
* @param $callback
* @return $this
*/
public function front($callback): Form
{
$this->flow['front'][] = $callback;
return $this;
}
/**
* 验证表单扩展
* @param $callback
* @return $this
*/
public function validator($callback): Form
{
$this->flow['validator'][] = $callback;
return $this;
}
/**
* 提交之前回调
* @param $callback
* @return $this
*/
public function submit($callback): Form
{
$this->flow['submit'][] = $callback;
return $this;
}
/**
* 保存之前回调
* @param $callback
* @return $this
*/
public function before($callback): Form
{
$this->flow['before'][] = $callback;
return $this;
}
/**
* 保存后回调
* @param $callback
* @return $this
*/
public function after($callback): Form
{
$this->flow['after'][] = $callback;
return $this;
}
/**
* 扩展元素
* @param $method
* @param $className
*/
public function extend($method, $className): void
{
$this->extend[$method] = $className;
}
/**
* 前端事件
* @param $table
* @param $name
* @param $type
* @return array|false
*/
public function callbackEvent($table, $name, $type, $data = null)
{
if (!$this->modelId) {
return false;
}
$rowsData = $data ?: $this->modelElo->where($this->modelElo->getPrimaryKey(), $this->modelId)->get();
$list = $table->renderRowData($rowsData, false);
$parentKey = null;
if ($table->getTree()) {
$parentKey = $this->modelElo->find($this->modelId)->parent_id;
}
$event = new Event($name);
foreach ($list as $item) {
$event->add($type, $this->modelId, $item, $parentKey !== false ? ['parentKey' => $parentKey] : []);
}
return $event->render();
}
/**
* 回调类库
* @param $method
* @param $arguments
* @return mixed
* @throws \Exception
*/
public function __call($method, $arguments)
{
$class = 'backend\\components\\UI\\Form\\' . ucfirst($method);
if (!class_exists($class)) {
if (!$this->extend[$method]) {
throw new \Exception('There is no form method "' . $method . '"');
}
$class = $this->extend[$method];
}
$object = new $class(...$arguments);
$object->dialog($this->dialog);
$this->element->push($object);
return $object;
}
private function renderPage(): array
{
return [
$this->side['left'] ? [
'nodeName' => 'div',
'class' => 'flex-none flex h-screen flex-col',
'child' => $this->side['left']
] : [],
[
'nodeName' => 'div',
'class' => 'flex flex-col lg:h-screen flex-grow w-10',
'child' => [
[
'nodeName' => 'div',
'child' => $this->renderForm()
],
[
'nodeName' => 'div',
'class' => 'flex items-center justify-end gap-2 flex-row ',
'child' => [
$this->back ? [
'nodeName' => 'route',
'type' => 'back',
'child' => [
'type' => "outline",
'nodeName' => 'a-button',
'child' => '返回',
]
] : [],
[
'nodeName' => 'a-button',
'html-type' => 'submit',
'vBind:loading' => "loading",
'type' => 'primary',
'child' => $this->back ? '提交' : '保存',
],
]
],
],
],
$this->side['right']? [
'nodeName' => 'div',
'class' => 'flex-none flex h-screen flex-col',
'child' => $this->side['right']
] : [],
];
}
/**
* 渲染弹窗
* @return array
*/
private function renderDialog(): array
{
return [
'nodeName' => 'app-dialog',
'title' => $this->title ?: '信息详情',
'class' => 'flex-grow',
'child' => [
[
'nodeName' => 'div',
'vSlot:default' => '',
'class' => 'flex',
'child' => [
$this->side['left'] ? [
'nodeName' => 'div',
'class' => 'flex-none',
'child' => $this->side['left']
] : [],
[
'nodeName' => 'div',
'class' => 'flex-grow p-5 pb-0',
'child' => $this->renderForm()
],
$this->side['right'] ? [
'nodeName' => 'div',
'class' => 'flex-none',
'child' => $this->side['right']
] : []
]
],
[
'nodeName' => 'div',
'vSlot:footer' => '',
'class' => 'arco-modal-footer',
'child' => [
[
'nodeName' => 'route',
'type' => 'back',
'child' => [
'nodeName' => 'a-button',
'child' => '取消'
]
],
[
'nodeName' => 'a-button',
'type' => 'primary',
'html-type' => 'submit',
'vBind:loading' => "loading",
'child' => '提交'
],
]
]
]
];
}
}