v1.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  1. <?php
  2. namespace zin;
  3. require_once dirname(__DIR__) . DS . 'btn' . DS . 'v1.php';
  4. require_once dirname(__DIR__) . DS . 'backbtn' . DS . 'v1.php';
  5. require_once dirname(__DIR__) . DS . 'content' . DS . 'v1.php';
  6. require_once dirname(__DIR__) . DS . 'history' . DS . 'v1.php';
  7. require_once dirname(__DIR__) . DS . 'entitytitle' . DS . 'v1.php';
  8. require_once dirname(__DIR__) . DS . 'formbase' . DS . 'v1.php';
  9. require_once dirname(__DIR__) . DS . 'formgroup' . DS . 'v1.php';
  10. require_once dirname(__DIR__) . DS . 'formrow' . DS . 'v1.php';
  11. class detailForm extends wg
  12. {
  13. /**
  14. * @var mixed[]
  15. */
  16. protected static $defineProps = array
  17. (
  18. /* ID,如果不指定则自动生成(使用 zin 部件 GID)。 */
  19. 'id ' => '?string="$GID"',
  20. /* 对象类型,例如 `story`、`task` 等,如果不指定则已当前的模块名称作为对象类型。 */
  21. 'objectType' => '?string',
  22. /* 对象 ID,如果不指定则尝试使用当前页面上的 `${$objectType}->id` 或者 `${$objectType}ID` 的值,例如 `$task->id` 或 `$taskID`。 */
  23. 'objectID' => '?int',
  24. /* 对象标题颜色。 */
  25. 'color' => '?string',
  26. /* 对象,如果不指定则尝试使用当前页面上的 `${$objectType}` 的值,例如 `$task`。 */
  27. 'object' => '?object',
  28. /* 标题,如果不指定则尝试使用当前页面上的 `${$objectType}->title` 或 `${$objectType}->name` 的值,例如 `$story->title`、`$task->name` 。 */
  29. 'title' => '?string',
  30. /* 标题前缀。 */
  31. 'titlePrefix' => '?string|bool',
  32. /* 表单操作按钮,如果不指定则使用默认行为的 “保存” 和 “返回” 按钮。 */
  33. 'actions' => '?array',
  34. /* 表单操作按钮栏类名。 */
  35. 'actionsClass' => '?string',
  36. /* 右上方工具栏的定义。 */
  37. 'toolbar' => '?array',
  38. /* 表单提交地址。 */
  39. 'url' => '?string',
  40. /* Ajax 表单选项。 */
  41. 'ajax' => '?array',
  42. /* 表单项标签默认数据。 */
  43. 'labelData' => 'null|array|object',
  44. /* 返回按钮,可以为:`true` 显示默认的返回按钮,`false` 不显示返回按钮,或者指定返回按钮 backBtn 的 back 属性,或者通过数组指定 backBtn 所有属性。 */
  45. 'backBtn' => '?bool|string|array',
  46. /* 主要栏表单字段列表。 */
  47. 'fields' => 'null|string|array|fieldList',
  48. /* 提交备注。 */
  49. 'comment' => '?string|bool|array=true',
  50. /* 侧边栏表单项分组标题。 */
  51. 'groupTitles'=> '?array',
  52. /* 右侧侧边栏宽度,如果不指定则默认为 `370`。 */
  53. 'sideWidth' => '?int=370',
  54. /* 左侧主栏表单项标签宽度。 */
  55. 'sideLabelWidth' => '?int|string',
  56. /* 左侧主栏目历史记录,如果设定为 `true` 显示当前对象默认的理智记录,否如果设置为 `false` 不显示历史记录,如果设置为数组则作为 `history()` 部件的属性来创建历史记录。 */
  57. 'history' => '?array|bool=true'
  58. );
  59. /**
  60. * @var mixed[]
  61. */
  62. protected static $defineBlocks = array
  63. (
  64. 'header' => array(),
  65. 'title' => array(),
  66. 'main' => array('map' => 'content'),
  67. 'side' => array('map' => 'tabs'),
  68. 'actions' => array('map' => 'btn'),
  69. 'toolbar' => array('map' => 'btnGroup,toolbar')
  70. );
  71. public static function getPageCSS()
  72. {
  73. return <<<'CSS'
  74. .detail-form-main .form-label {position: sticky; top: 0; background: var(--color-canvas); z-index: 1; margin-bottom: 4px}
  75. .detail-form-main .form-label .text {font-size: 14px; font-weight: bold;}
  76. .detail-form-main {border-right: 1px solid var(--color-border)}
  77. .detail-form-main .form-grid .form-label.required:after {order: 1}
  78. .detail-form-side {border-left: 1px solid var(--color-border); margin-left: -1px}
  79. .detail-form-main .form-grid {padding: 12px 16px}
  80. .detail-form-history {padding: 12px}
  81. .detail-form-side .form-row {padding: 8px 0}
  82. .detail-form-side .form-group {align-items: center; min-height: 20px}
  83. .detail-form-side .form-label {height: 20px; top: auto}
  84. .detail-form-side .form-control-static {padding: 0; min-height: 20px}
  85. CSS;
  86. }
  87. /**
  88. * @var mixed[]|null
  89. */
  90. protected $fieldMap;
  91. /**
  92. * @var object|null
  93. */
  94. protected $object;
  95. /**
  96. * @var null|mixed[]|object
  97. */
  98. protected $labelData = null;
  99. protected function created()
  100. {
  101. global $app, $lang;
  102. $objectType = $this->prop('objectType');
  103. $objectID = $this->prop('objectID');
  104. $object = $this->prop('object');
  105. if(!$objectType) $objectType = $app->rawModule;
  106. if(!$object) $object = data($objectType);
  107. if(!$objectID) $objectID = $object ? $object->id : data($objectType . 'ID');
  108. if(!$objectType || !$objectID || !$object)
  109. {
  110. $this->triggerError('The objectType, objectID or object property of widget "detail" is undefined.');
  111. }
  112. if(!$this->prop('objectType')) $this->setProp('objectType', $objectType);
  113. if(!$this->prop('objectID')) $this->setProp('objectID', $objectID);
  114. if(!$this->prop('object')) $this->setProp('object', $object);
  115. if(!$this->prop('backBtn')) $this->setProp('backBtn', !isInModal());
  116. if($object)
  117. {
  118. if(!$this->hasProp('title')) $this->setProp('title', isset($object->name) ? $object->name : $object->title);
  119. if(!$this->hasProp('color') && isset($object->color)) $this->setProp('color', $object->color);
  120. }
  121. if(!$this->hasProp('titlePrefix') && isset($lang->$objectType->edit)) $this->setProp('titlePrefix', $lang->$objectType->edit);
  122. if(!$this->hasProp('labelData') && isset($lang->$objectType)) $this->setProp('labelData', $lang->$objectType);
  123. }
  124. protected function buildTitle()
  125. {
  126. list($object, $objectID, $title, $color, $objectType, $titlePrefix) = $this->prop(array('object', 'objectID', 'title', 'color', 'objectType', 'titlePrefix'));
  127. $titleBlock = $this->block('title');
  128. return new entityTitle
  129. (
  130. setClass('min-w-0'),
  131. set::id($objectID),
  132. set::object($object),
  133. set::title($title),
  134. set::titleClass('text-lg text-clip font-bold'),
  135. set::type($objectType),
  136. set::color($color),
  137. $titlePrefix ? to::prefix(span(setClass('text-gray'), $titlePrefix)) : null,
  138. $titleBlock
  139. );
  140. }
  141. protected function buildToolbar()
  142. {
  143. $toolbar = $this->prop('toolbar');
  144. $toolbarBlock = $this->block('toolbar');
  145. if(!$toolbarBlock && !$toolbar) return null;
  146. $toolbarProps = array_is_list($toolbar) ? array('items' => $toolbar) : $toolbar;
  147. return div
  148. (
  149. setClass('detail-form-toolbar'),
  150. $toolbarProps ? toolbar(set::urlFormatter($this->prop('urlFormatter')), set($toolbarProps)) : null,
  151. $toolbarBlock
  152. );
  153. }
  154. /**
  155. * @param mixed[]|null $props
  156. */
  157. protected function buildBackBtn($props = null)
  158. {
  159. global $lang;
  160. $backBtn = $this->prop('backBtn');
  161. if($backBtn === false) return null;
  162. if(is_string($backBtn)) $backBtn = array('back' => $backBtn);
  163. elseif(!is_array($backBtn)) $backBtn = array();
  164. if($props) $backBtn = array_merge($backBtn, $props);
  165. return new backBtn
  166. (
  167. set::icon('back'),
  168. set::text($lang->goback),
  169. set($backBtn)
  170. );
  171. }
  172. protected function buildHeader()
  173. {
  174. return div
  175. (
  176. setClass('detail-form-header row gap-2 items-center flex-none'),
  177. $this->buildBackBtn(array('type' => 'primary-outline', 'class' => 'mr-2 size-md')),
  178. $this->buildTitle(),
  179. $this->block('header'),
  180. div(setClass('flex-auto')),
  181. $this->buildToolbar()
  182. );
  183. }
  184. protected function buildHistory()
  185. {
  186. $history = $this->prop('history');
  187. if($history === false) return null;
  188. if(!is_array($history)) $history = array();
  189. return new history
  190. (
  191. set::className('detail-form-history overflow-visible ring-0 border-t'),
  192. set::objectType($this->prop('objectType')),
  193. set::objectID($this->prop('objectID')),
  194. set::commentBtn(false),
  195. set($history)
  196. );
  197. }
  198. /**
  199. * @param string $name
  200. */
  201. protected function getFieldLabel($name)
  202. {
  203. $labelData = $this->labelData;
  204. $lblName = 'lbl' . ucfirst($name);
  205. if(is_object($labelData)) return isset($labelData->$lblName) ? $labelData->$lblName : (isset($labelData->$name) ? $labelData->$name : null);
  206. if(is_array($labelData)) return isset($labelData[$lblName]) ? $labelData[$lblName] : (isset($labelData[$name]) ? $labelData[$name] : null);
  207. return null;
  208. }
  209. /**
  210. * @param string $name
  211. */
  212. protected function getFieldValue($name)
  213. {
  214. $object = $this->object;
  215. if(!$object) return null;
  216. return isset($object->$name) ? strval($object->$name) : null;
  217. }
  218. /**
  219. * @param mixed[] $field
  220. * @param bool $useRow
  221. */
  222. protected function buildField($field, $useRow = false)
  223. {
  224. $name = $field['name'];
  225. if(!isset($field['value'])) $field['value'] = $this->getFieldValue($name);
  226. if(!isset($field['label'])) $field['label'] = $this->getFieldLabel($name);
  227. $control = isset($field['control']) ? $field['control'] : null;
  228. if(is_null($control)) $control = array('name' => $name);
  229. if(is_string($control)) $control = array('control' => $control, 'name' => $name);
  230. if(is_array($control) && !isset($control['id'])) $control['id'] = '';
  231. $field['control'] = $control;
  232. $view = new formGroup(set($field));
  233. if($useRow) $view = new formRow($view);
  234. return $view;
  235. }
  236. /**
  237. * @param string|mixed[] $groupOrFields
  238. * @param bool $useRow
  239. */
  240. protected function buildFields($groupOrFields = 'main', $useRow = false)
  241. {
  242. if(is_string($groupOrFields))
  243. {
  244. if(!isset($this->fieldMap[$groupOrFields])) return null;
  245. $fields = $this->fieldMap[$groupOrFields];
  246. }
  247. else
  248. {
  249. $fields = $groupOrFields;
  250. }
  251. if(!$fields) return null;
  252. $items = array();
  253. foreach($fields as $field)
  254. {
  255. $item = $this->buildField($field, $useRow);
  256. if(empty($field)) continue;
  257. $items[] = $item;
  258. }
  259. return $items;
  260. }
  261. protected function buildComment()
  262. {
  263. $comment = $this->prop('comment');
  264. if(!$comment) return null;
  265. $props = array('name' => 'comment', 'control' => 'editor');
  266. if(is_string($comment)) $props['name'] = $comment;
  267. elseif(is_array($comment)) $props = array_merge($props, $comment);
  268. if(!isset($props['label']))
  269. {
  270. global $lang;
  271. $label = $this->getFieldLabel('comment');
  272. $props['label'] = $label ? $label : $lang->comment;
  273. }
  274. return new formGroup(set($props));
  275. }
  276. protected function buildMain()
  277. {
  278. return div
  279. (
  280. setClass('detail-form-main flex-auto'),
  281. div
  282. (
  283. setClass('form-grid'),
  284. $this->buildFields(),
  285. $this->buildComment()
  286. ),
  287. $this->block('main'),
  288. $this->buildHistory()
  289. );
  290. }
  291. protected function buildSideGroups()
  292. {
  293. $groupTitles = $this->prop('groupTitles', array());
  294. $items = array();
  295. foreach($this->fieldMap as $group => $fields)
  296. {
  297. if($group === 'main') continue;
  298. $items[] = div
  299. (
  300. setClass('form-horz col pt-2 pr-5'),
  301. div
  302. (
  303. setClass('text-md font-bold sticky top-0 canvas py-2 px-4 z-5'),
  304. isset($groupTitles[$group]) ? $groupTitles[$group] : $group
  305. ),
  306. $this->buildFields($fields, true)
  307. );
  308. }
  309. return div
  310. (
  311. setClass('detail-form-side-groups pb-3'),
  312. setCssVar('--form-horz-label-width', $this->prop('sideLabelWidth')),
  313. $items
  314. );
  315. }
  316. protected function buildSide()
  317. {
  318. return div
  319. (
  320. setClass('detail-form-side flex-none col gap-1'),
  321. setStyle('width', $this->prop('sideWidth') . 'px'),
  322. $this->buildSideGroups(),
  323. $this->block('side')
  324. );
  325. }
  326. protected function buildBody()
  327. {
  328. return div
  329. (
  330. setClass('detail-form-body row items-start'),
  331. $this->buildMain(),
  332. $this->buildSide()
  333. );
  334. }
  335. protected function beforeBuild()
  336. {
  337. list($object, $labelData, $fields) = $this->prop(array('object', 'labelData', 'fields'));
  338. $this->object = $object;
  339. $this->labelData = $labelData ? $labelData : array();
  340. $this->fieldMap = array();
  341. if($fields instanceof fieldList) $fields = $fields->toArray();
  342. foreach($fields as $field)
  343. {
  344. if(!$field) continue;
  345. if($field instanceof setting) $field = $field->toArray();
  346. if(is_object($field)) $field = get_object_vars($field);
  347. if(!isset($field['name'])) continue;
  348. $group = (isset($field['group']) && $field['group']) ? $field['group'] : 'main';
  349. if(!isset($this->fieldMap[$group])) $this->fieldMap[$group] = array();
  350. $this->fieldMap[$group][$field['name']] = $field;
  351. }
  352. }
  353. protected function build()
  354. {
  355. $this->beforeBuild();
  356. list($objectType, $objectID, $backBtn) = $this->prop(array('objectType', 'objectID', 'backBtn'));
  357. $formProps = array_keys(formBase::definedPropsList());
  358. $backProps = array();
  359. if(is_string($backBtn)) $backProps['back'] = $backBtn;
  360. elseif(is_array($backBtn)) $backProps = $backBtn;
  361. return div
  362. (
  363. setClass('detail-form col gap-3'),
  364. setData('id', $objectID),
  365. setData('type', $objectType),
  366. set($this->getRestProps()),
  367. $this->buildHeader(),
  368. new formBase
  369. (
  370. setClass('canvas shadow ring rounded gap-0'),
  371. set($this->props->pick($formProps)),
  372. set($backProps),
  373. set::actionsClass('py-4 sticky border-t bottom-0 canvas z-5', isset($formProps['actionsClass']) ? $formProps['actionsClass'] : null),
  374. $this->buildBody(),
  375. $this->children()
  376. )
  377. );
  378. }
  379. }