Crud.php 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992
  1. <?php
  2. namespace app\admin\controller\crud;
  3. use Throwable;
  4. use ba\Exception;
  5. use ba\Filesystem;
  6. use think\facade\Db;
  7. use ba\TableManager;
  8. use app\admin\model\CrudLog;
  9. use app\common\library\Menu;
  10. use app\admin\model\AdminLog;
  11. use app\admin\model\AdminRule;
  12. use app\common\controller\Backend;
  13. use app\admin\library\crud\Helper;
  14. class Crud extends Backend
  15. {
  16. /**
  17. * 模型文件数据
  18. * @var array
  19. */
  20. protected array $modelData = [];
  21. /**
  22. * 控制器文件数据
  23. * @var array
  24. */
  25. protected array $controllerData = [];
  26. /**
  27. * index.vue文件数据
  28. * @var array
  29. */
  30. protected array $indexVueData = [];
  31. /**
  32. * form.vue文件数据
  33. * @var array
  34. */
  35. protected array $formVueData = [];
  36. /**
  37. * 语言翻译前缀
  38. * @var string
  39. */
  40. protected string $webTranslate = '';
  41. /**
  42. * 语言包数据
  43. * @var array
  44. */
  45. protected array $langTsData = [];
  46. /**
  47. * 当designType为以下值时:
  48. * 1. 出入库字符串到数组转换
  49. * 2. 默认值转数组
  50. * @var array
  51. */
  52. protected array $dtStringToArray = ['checkbox', 'selects', 'remoteSelects', 'city', 'images', 'files'];
  53. protected array $noNeedPermission = ['logStart', 'getFileData', 'parseFieldData', 'generateCheck', 'uploadCompleted'];
  54. public function initialize(): void
  55. {
  56. parent::initialize();
  57. }
  58. /**
  59. * 开始生成
  60. * @throws Throwable
  61. */
  62. public function generate(): void
  63. {
  64. $type = $this->request->post('type', '');
  65. $table = $this->request->post('table', []);
  66. $fields = $this->request->post('fields', [], 'clean_xss,htmlspecialchars_decode_improve');
  67. if (!$table || !$fields || !isset($table['name']) || !$table['name']) {
  68. $this->error(__('Parameter error'));
  69. }
  70. try {
  71. // 记录日志
  72. $crudLogId = Helper::recordCrudStatus([
  73. 'table' => $table,
  74. 'fields' => $fields,
  75. 'status' => 'start',
  76. ]);
  77. // 表名称
  78. $tableName = TableManager::tableName($table['name'], false, $table['databaseConnection']);
  79. if ($type == 'create' || $table['rebuild'] == 'Yes') {
  80. // 数据表存在则删除
  81. TableManager::phinxTable($tableName, [], true, $table['databaseConnection'])->drop()->save();
  82. }
  83. // 处理表设计
  84. [$tablePk] = Helper::handleTableDesign($table, $fields);
  85. // 表注释
  86. $tableComment = mb_substr($table['comment'], -1) == '表' ? mb_substr($table['comment'], 0, -1) . '管理' : $table['comment'];
  87. // 生成文件信息解析
  88. $modelFile = Helper::parseNameData($table['isCommonModel'] ? 'common' : 'admin', $tableName, 'model', $table['modelFile']);
  89. $validateFile = Helper::parseNameData($table['isCommonModel'] ? 'common' : 'admin', $tableName, 'validate', $table['validateFile']);
  90. $controllerFile = Helper::parseNameData('admin', $tableName, 'controller', $table['controllerFile']);
  91. $webViewsDir = Helper::parseWebDirNameData($tableName, 'views', $table['webViewsDir']);
  92. $webLangDir = Helper::parseWebDirNameData($tableName, 'lang', $table['webViewsDir']);
  93. // 语言翻译前缀
  94. $this->webTranslate = implode('.', $webLangDir['lang']) . '.';
  95. // 快速搜索字段
  96. if (!in_array($tablePk, $table['quickSearchField'])) {
  97. $table['quickSearchField'][] = $tablePk;
  98. }
  99. $quickSearchFieldZhCnTitle = [];
  100. // 模型数据
  101. $this->modelData['append'] = [];
  102. $this->modelData['methods'] = [];
  103. $this->modelData['fieldType'] = [];
  104. $this->modelData['createTime'] = '';
  105. $this->modelData['updateTime'] = '';
  106. $this->modelData['beforeInsertMixins'] = [];
  107. $this->modelData['beforeInsert'] = '';
  108. $this->modelData['afterInsert'] = '';
  109. $this->modelData['connection'] = $table['databaseConnection'];
  110. $this->modelData['name'] = $tableName;
  111. $this->modelData['className'] = $modelFile['lastName'];
  112. $this->modelData['namespace'] = $modelFile['namespace'];
  113. $this->modelData['relationMethodList'] = [];
  114. // 控制器数据
  115. $this->controllerData['use'] = [];
  116. $this->controllerData['attr'] = [];
  117. $this->controllerData['methods'] = [];
  118. $this->controllerData['filterRule'] = '';
  119. $this->controllerData['className'] = $controllerFile['lastName'];
  120. $this->controllerData['namespace'] = $controllerFile['namespace'];
  121. $this->controllerData['tableComment'] = $tableComment;
  122. $this->controllerData['modelName'] = $modelFile['lastName'];
  123. $this->controllerData['modelNamespace'] = $modelFile['namespace'];
  124. // index.vue数据
  125. $this->indexVueData['enableDragSort'] = false;
  126. $this->indexVueData['defaultItems'] = [];
  127. $this->indexVueData['tableColumn'] = [
  128. [
  129. 'type' => 'selection',
  130. 'align' => 'center',
  131. 'operator' => 'false',
  132. ],
  133. ];
  134. $this->indexVueData['dblClickNotEditColumn'] = ['undefined'];
  135. $this->indexVueData['optButtons'] = ['edit', 'delete'];
  136. $this->indexVueData['defaultOrder'] = '';
  137. // form.vue数据
  138. $this->formVueData['bigDialog'] = false;
  139. $this->formVueData['formFields'] = [];
  140. $this->formVueData['formValidatorRules'] = [];
  141. $this->formVueData['imports'] = [];
  142. // 语言包数据
  143. $this->langTsData = [
  144. 'en' => [],
  145. 'zh-cn' => [],
  146. ];
  147. // 简化的字段数据
  148. $fieldsMap = [];
  149. foreach ($fields as $key => $field) {
  150. $fieldsMap[$field['name']] = $field['designType'];
  151. // 分析字段
  152. Helper::analyseField($field);
  153. Helper::getDictData($this->langTsData['en'], $field, 'en');
  154. Helper::getDictData($this->langTsData['zh-cn'], $field, 'zh-cn');
  155. // 快速搜索字段
  156. if (in_array($field['name'], $table['quickSearchField'])) {
  157. $quickSearchFieldZhCnTitle[] = $this->langTsData['zh-cn'][$field['name']] ?? $field['name'];
  158. }
  159. // 不允许双击编辑的字段
  160. if ($field['designType'] == 'switch') {
  161. $this->indexVueData['dblClickNotEditColumn'][] = $field['name'];
  162. }
  163. // 列字典数据
  164. $columnDict = $this->getColumnDict($field);
  165. // 表单项
  166. if (in_array($field['name'], $table['formFields'])) {
  167. $this->formVueData['formFields'][] = $this->getFormField($field, $columnDict, $table['databaseConnection']);
  168. }
  169. // 表格列
  170. if (in_array($field['name'], $table['columnFields'])) {
  171. $this->indexVueData['tableColumn'][] = $this->getTableColumn($field, $columnDict);
  172. }
  173. // 关联表数据解析
  174. if (in_array($field['designType'], ['remoteSelect', 'remoteSelects'])) {
  175. $this->parseJoinData($field, $table);
  176. }
  177. // 模型方法
  178. $this->parseModelMethods($field, $this->modelData);
  179. // 控制器/模型等文件的一些杂项属性解析
  180. $this->parseSundryData($field, $table);
  181. if (!in_array($field['name'], $table['formFields'])) {
  182. $this->controllerData['attr']['preExcludeFields'][] = $field['name'];
  183. }
  184. }
  185. // 快速搜索提示
  186. $this->langTsData['en']['quick Search Fields'] = implode(',', $table['quickSearchField']);
  187. $this->langTsData['zh-cn']['quick Search Fields'] = implode('、', $quickSearchFieldZhCnTitle);
  188. $this->controllerData['attr']['quickSearchField'] = $table['quickSearchField'];
  189. // 开启字段排序
  190. $weighKey = array_search('weigh', $fieldsMap);
  191. if ($weighKey !== false) {
  192. $this->indexVueData['enableDragSort'] = true;
  193. $this->modelData['afterInsert'] = Helper::assembleStub('mixins/model/afterInsert', [
  194. 'field' => $weighKey
  195. ]);
  196. }
  197. // 表格的操作列
  198. $this->indexVueData['tableColumn'][] = [
  199. 'label' => "t('Operate')",
  200. 'align' => 'center',
  201. 'width' => $this->indexVueData['enableDragSort'] ? 140 : 100,
  202. 'render' => 'buttons',
  203. 'buttons' => 'optButtons',
  204. 'operator' => 'false',
  205. ];
  206. if ($this->indexVueData['enableDragSort']) {
  207. array_unshift($this->indexVueData['optButtons'], 'weigh-sort');
  208. }
  209. // 写入语言包代码
  210. Helper::writeWebLangFile($this->langTsData, $webLangDir);
  211. // 写入模型代码
  212. Helper::writeModelFile($tablePk, $fieldsMap, $this->modelData, $modelFile);
  213. // 写入控制器代码
  214. Helper::writeControllerFile($this->controllerData, $controllerFile);
  215. // 写入验证器代码
  216. $validateContent = Helper::assembleStub('mixins/validate/validate', [
  217. 'namespace' => $validateFile['namespace'],
  218. 'className' => $validateFile['lastName'],
  219. ]);
  220. Helper::writeFile($validateFile['parseFile'], $validateContent);
  221. // 写入index.vue代码
  222. $this->indexVueData['tablePk'] = $tablePk;
  223. $this->indexVueData['webTranslate'] = $this->webTranslate;
  224. Helper::writeIndexFile($this->indexVueData, $webViewsDir, $controllerFile);
  225. // 写入form.vue代码
  226. Helper::writeFormFile($this->formVueData, $webViewsDir, $fields, $this->webTranslate);
  227. // 生成菜单
  228. Helper::createMenu($webViewsDir, $tableComment);
  229. Helper::recordCrudStatus([
  230. 'id' => $crudLogId,
  231. 'status' => 'success',
  232. ]);
  233. } catch (Exception $e) {
  234. Helper::recordCrudStatus([
  235. 'id' => $crudLogId ?? 0,
  236. 'status' => 'error',
  237. ]);
  238. $this->error($e->getMessage());
  239. } catch (Throwable $e) {
  240. Helper::recordCrudStatus([
  241. 'id' => $crudLogId ?? 0,
  242. 'status' => 'error',
  243. ]);
  244. if (env('app_debug', false)) throw $e;
  245. $this->error($e->getMessage());
  246. }
  247. $this->success('', [
  248. 'crudLog' => CrudLog::find($crudLogId),
  249. ]);
  250. }
  251. /**
  252. * 从log开始
  253. * @throws Throwable
  254. */
  255. public function logStart(): void
  256. {
  257. $id = $this->request->post('id');
  258. $type = $this->request->post('type', '');
  259. if ($type == 'Cloud history') {
  260. // 云端 历史记录
  261. $client = get_ba_client();
  262. $response = $client->request('GET', '/api/v6.Crud/info', [
  263. 'query' => [
  264. 'id' => $id,
  265. 'server' => 1,
  266. 'ba-user-token' => $this->request->post('token', ''),
  267. ]
  268. ]);
  269. $body = $response->getBody();
  270. $statusCode = $response->getStatusCode();
  271. $content = $body->getContents();
  272. if ($content == '' || stripos($content, '<title>系统发生错误</title>') !== false || $statusCode != 200) {
  273. $this->error(__('Failed to load cloud data'));
  274. }
  275. $json = json_decode($content, true);
  276. if (json_last_error() != JSON_ERROR_NONE) {
  277. $this->error(__('Failed to load cloud data'));
  278. }
  279. if (is_array($json)) {
  280. if ($json['code'] != 1) {
  281. $this->error($json['msg']);
  282. }
  283. $info = $json['data']['info'];
  284. }
  285. } else {
  286. // 本地记录
  287. $info = CrudLog::find($id)->toArray();
  288. }
  289. if (!isset($info) || !$info) {
  290. $this->error(__('Record not found'));
  291. }
  292. // 数据表是否有数据
  293. $connection = TableManager::getConnection($info['table']['databaseConnection'] ?? '');
  294. $tableName = TableManager::tableName($info['table']['name'], false, $connection);
  295. $adapter = TableManager::phinxAdapter(true, $connection);
  296. if ($adapter->hasTable($tableName)) {
  297. $info['table']['empty'] = Db::connect($connection)
  298. ->name($tableName)
  299. ->limit(1)
  300. ->select()
  301. ->isEmpty();
  302. } else {
  303. $info['table']['empty'] = true;
  304. }
  305. AdminLog::instance()->setTitle(__('Log start'));
  306. $this->success('', [
  307. 'table' => $info['table'],
  308. 'fields' => $info['fields'],
  309. 'sync' => $info['sync'],
  310. ]);
  311. }
  312. /**
  313. * 删除CRUD记录和生成的文件
  314. * @throws Throwable
  315. */
  316. public function delete(): void
  317. {
  318. $id = $this->request->post('id');
  319. $info = CrudLog::find($id)->toArray();
  320. if (!$info) {
  321. $this->error(__('Record not found'));
  322. }
  323. $webLangDir = Helper::parseWebDirNameData($info['table']['name'], 'lang', $info['table']['webViewsDir']);
  324. $files = [
  325. $webLangDir['en'] . '.ts',
  326. $webLangDir['zh-cn'] . '.ts',
  327. $info['table']['webViewsDir'] . '/' . 'index.vue',
  328. $info['table']['webViewsDir'] . '/' . 'popupForm.vue',
  329. $info['table']['controllerFile'],
  330. $info['table']['modelFile'],
  331. $info['table']['validateFile'],
  332. ];
  333. try {
  334. foreach ($files as &$file) {
  335. $file = Filesystem::fsFit(root_path() . $file);
  336. if (file_exists($file)) {
  337. unlink($file);
  338. }
  339. Filesystem::delEmptyDir(dirname($file));
  340. }
  341. // 删除菜单
  342. Menu::delete(Helper::getMenuName($webLangDir), true);
  343. Helper::recordCrudStatus([
  344. 'id' => $id,
  345. 'status' => 'delete',
  346. ]);
  347. } catch (Throwable $e) {
  348. $this->error($e->getMessage());
  349. }
  350. $this->success(__('Deleted successfully'));
  351. }
  352. /**
  353. * 获取文件路径数据
  354. * @throws Throwable
  355. */
  356. public function getFileData(): void
  357. {
  358. $table = $this->request->get('table');
  359. $commonModel = $this->request->get('commonModel/b');
  360. if (!$table) {
  361. $this->error(__('Parameter error'));
  362. }
  363. try {
  364. $modelFile = Helper::parseNameData($commonModel ? 'common' : 'admin', $table, 'model');
  365. $validateFile = Helper::parseNameData($commonModel ? 'common' : 'admin', $table, 'validate');
  366. $controllerFile = Helper::parseNameData('admin', $table, 'controller');
  367. $webViewsDir = Helper::parseWebDirNameData($table, 'views');
  368. } catch (Throwable $e) {
  369. $this->error($e->getMessage());
  370. }
  371. // 模型和控制器文件和文件列表
  372. $adminModelFiles = Filesystem::getDirFiles(root_path() . 'app' . DIRECTORY_SEPARATOR . 'admin' . DIRECTORY_SEPARATOR . 'model' . DIRECTORY_SEPARATOR);
  373. $commonModelFiles = Filesystem::getDirFiles(root_path() . 'app' . DIRECTORY_SEPARATOR . 'common' . DIRECTORY_SEPARATOR . 'model' . DIRECTORY_SEPARATOR);
  374. $adminControllerFiles = get_controller_list();
  375. $modelFileList = [];
  376. $controllerFiles = [];
  377. foreach ($adminModelFiles as $item) {
  378. $item = Filesystem::fsFit('app/admin/model/' . $item);
  379. $modelFileList[$item] = $item;
  380. }
  381. foreach ($commonModelFiles as $item) {
  382. $item = Filesystem::fsFit('app/common/model/' . $item);
  383. $modelFileList[$item] = $item;
  384. }
  385. $outExcludeController = [
  386. 'Addon.php',
  387. 'Ajax.php',
  388. 'Dashboard.php',
  389. 'Index.php',
  390. 'Module.php',
  391. 'Terminal.php',
  392. 'routine/AdminInfo.php',
  393. 'routine/Config.php',
  394. ];
  395. foreach ($adminControllerFiles as $item) {
  396. if (in_array($item, $outExcludeController)) {
  397. continue;
  398. }
  399. $item = Filesystem::fsFit('app/admin/controller/' . $item);
  400. $controllerFiles[$item] = $item;
  401. }
  402. $this->success('', [
  403. 'modelFile' => $modelFile['rootFileName'],
  404. 'controllerFile' => $controllerFile['rootFileName'],
  405. 'validateFile' => $validateFile['rootFileName'],
  406. 'controllerFileList' => $controllerFiles,
  407. 'modelFileList' => $modelFileList,
  408. 'webViewsDir' => $webViewsDir['views'],
  409. ]);
  410. }
  411. /**
  412. * 检查是否已有CRUD记录
  413. * @throws Throwable
  414. */
  415. public function checkCrudLog(): void
  416. {
  417. $table = $this->request->get('table');
  418. $connection = $this->request->get('connection');
  419. $connection = $connection ?: config('database.default');
  420. $crudLog = Db::name('crud_log')
  421. ->where('table_name', $table)
  422. ->where('connection', $connection)
  423. ->order('create_time desc')
  424. ->find();
  425. $this->success('', [
  426. 'id' => ($crudLog && $crudLog['status'] == 'success') ? $crudLog['id'] : 0,
  427. ]);
  428. }
  429. /**
  430. * 解析字段数据
  431. * @throws Throwable
  432. */
  433. public function parseFieldData(): void
  434. {
  435. AdminLog::instance()->setTitle(__('Parse field data'));
  436. $type = $this->request->post('type');
  437. $table = $this->request->post('table');
  438. $connection = $this->request->post('connection');
  439. $connection = TableManager::getConnection($connection);
  440. $table = TableManager::tableName($table, true, $connection);
  441. $connectionConfig = TableManager::getConnectionConfig($connection);
  442. if ($type == 'db') {
  443. $sql = 'SELECT * FROM `information_schema`.`tables` '
  444. . 'WHERE TABLE_SCHEMA = ? AND table_name = ?';
  445. $tableInfo = Db::connect($connection)->query($sql, [$connectionConfig['database'], $table]);
  446. if (!$tableInfo) {
  447. $this->error(__('Record not found'));
  448. }
  449. // 数据表是否有数据
  450. $adapter = TableManager::phinxAdapter(false, $connection);
  451. if ($adapter->hasTable($table)) {
  452. $empty = Db::connect($connection)
  453. ->table($table)
  454. ->limit(1)
  455. ->select()
  456. ->isEmpty();
  457. } else {
  458. $empty = true;
  459. }
  460. $this->success('', [
  461. 'columns' => Helper::parseTableColumns($table, false, $connection),
  462. 'comment' => $tableInfo[0]['TABLE_COMMENT'] ?? '',
  463. 'empty' => $empty,
  464. ]);
  465. }
  466. }
  467. /**
  468. * 生成前检查
  469. * @throws Throwable
  470. */
  471. public function generateCheck(): void
  472. {
  473. $table = $this->request->post('table');
  474. $connection = $this->request->post('connection');
  475. $webViewsDir = $this->request->post('webViewsDir', '');
  476. $controllerFile = $this->request->post('controllerFile', '');
  477. if (!$table) {
  478. $this->error(__('Parameter error'));
  479. }
  480. AdminLog::instance()->setTitle(__('Generate check'));
  481. try {
  482. $webViewsDir = Helper::parseWebDirNameData($table, 'views', $webViewsDir);
  483. $controllerFile = Helper::parseNameData('admin', $table, 'controller', $controllerFile)['rootFileName'];
  484. } catch (Throwable $e) {
  485. $this->error($e->getMessage());
  486. }
  487. // 数据表是否存在
  488. $tableList = TableManager::getTableList($connection);
  489. $tableExist = array_key_exists(TableManager::tableName($table, true, $connection), $tableList);
  490. // 控制器是否存在
  491. $controllerExist = file_exists(root_path() . $controllerFile);
  492. // 菜单规则是否存在
  493. $menuName = Helper::getMenuName($webViewsDir);
  494. $menuExist = AdminRule::where('name', $menuName)->value('id');
  495. if ($controllerExist || $tableExist || $menuExist) {
  496. $this->error('', [
  497. 'menu' => $menuExist,
  498. 'table' => $tableExist,
  499. 'controller' => $controllerExist,
  500. ], -1);
  501. }
  502. $this->success();
  503. }
  504. /**
  505. * CRUD 设计记录上传成功标记
  506. * @throws Throwable
  507. */
  508. public function uploadCompleted(): void
  509. {
  510. $syncIds = $this->request->post('syncIds/a', []);
  511. $cancelSync = $this->request->post('cancelSync/b', false);
  512. $crudLogModel = new CrudLog();
  513. if ($cancelSync) {
  514. $logData = $crudLogModel->where('id', 'in', array_keys($syncIds))->select();
  515. foreach ($logData as $logDatum) {
  516. if ($logDatum->sync == $syncIds[$logDatum->id]) {
  517. $logDatum->sync = 0;
  518. $logDatum->save();
  519. }
  520. }
  521. $this->success();
  522. }
  523. $saveData = [];
  524. foreach ($syncIds as $key => $syncId) {
  525. $saveData[] = [
  526. 'id' => $key,
  527. 'sync' => $syncId,
  528. ];
  529. }
  530. $crudLogModel->saveAll($saveData);
  531. $this->success();
  532. }
  533. /**
  534. * 关联表数据解析
  535. * @param $field
  536. * @param $table
  537. * @throws Throwable
  538. */
  539. private function parseJoinData($field, $table): void
  540. {
  541. $dictEn = [];
  542. $dictZhCn = [];
  543. if ($field['form']['relation-fields'] && $field['form']['remote-table']) {
  544. $columns = Helper::parseTableColumns($field['form']['remote-table'], true, $table['databaseConnection']);
  545. $relationFields = explode(',', $field['form']['relation-fields']);
  546. $tableName = TableManager::tableName($field['form']['remote-table'], false, $table['databaseConnection']);
  547. $rnPattern = '/(.*)(_ids|_id)$/';
  548. if (preg_match($rnPattern, $field['name'])) {
  549. $relationName = parse_name(preg_replace($rnPattern, '$1', $field['name']), 1, false);
  550. } else {
  551. $relationName = parse_name($field['name'] . '_table', 1, false);
  552. }
  553. // 建立关联模型代码文件
  554. if (!$field['form']['remote-model'] || !file_exists(root_path() . $field['form']['remote-model'])) {
  555. $joinModelFile = Helper::parseNameData('admin', $tableName, 'model', $field['form']['remote-model']);
  556. if (!file_exists(root_path() . $joinModelFile['rootFileName'])) {
  557. $joinModelData['append'] = [];
  558. $joinModelData['methods'] = [];
  559. $joinModelData['fieldType'] = [];
  560. $joinModelData['createTime'] = '';
  561. $joinModelData['updateTime'] = '';
  562. $joinModelData['beforeInsertMixins'] = [];
  563. $joinModelData['beforeInsert'] = '';
  564. $joinModelData['afterInsert'] = '';
  565. $joinModelData['connection'] = $table['databaseConnection'];
  566. $joinModelData['name'] = $tableName;
  567. $joinModelData['className'] = $joinModelFile['lastName'];
  568. $joinModelData['namespace'] = $joinModelFile['namespace'];
  569. $joinTablePk = 'id';
  570. $joinFieldsMap = [];
  571. foreach ($columns as $column) {
  572. $joinFieldsMap[$column['name']] = $column['designType'];
  573. $this->parseModelMethods($column, $joinModelData);
  574. if ($column['primaryKey']) $joinTablePk = $column['name'];
  575. }
  576. $weighKey = array_search('weigh', $joinFieldsMap);
  577. if ($weighKey !== false) {
  578. $joinModelData['afterInsert'] = Helper::assembleStub('mixins/model/afterInsert', [
  579. 'field' => $joinFieldsMap[$weighKey]
  580. ]);
  581. }
  582. Helper::writeModelFile($joinTablePk, $joinFieldsMap, $joinModelData, $joinModelFile);
  583. }
  584. $field['form']['remote-model'] = $joinModelFile['rootFileName'];
  585. }
  586. if ($field['designType'] == 'remoteSelect') {
  587. // 关联预载入方法
  588. $this->controllerData['attr']['withJoinTable'][$relationName] = $relationName;
  589. // 模型方法代码
  590. $relationData = [
  591. 'relationMethod' => $relationName,
  592. 'relationMode' => 'belongsTo',
  593. 'relationPrimaryKey' => $field['form']['remote-pk'] ?? 'id',
  594. 'relationForeignKey' => $field['name'],
  595. 'relationClassName' => str_replace(['.php', '/'], ['', '\\'], '\\' . $field['form']['remote-model']) . "::class",
  596. ];
  597. $this->modelData['relationMethodList'][$relationName] = Helper::assembleStub('mixins/model/belongsTo', $relationData);
  598. // 查询时显示的字段
  599. if ($relationFields) {
  600. $this->controllerData['relationVisibleFieldList'][$relationData['relationMethod']] = $relationFields;
  601. }
  602. } elseif ($field['designType'] == 'remoteSelects') {
  603. $this->modelData['append'][] = $relationName;
  604. $this->modelData['methods'][] = Helper::assembleStub('mixins/model/getters/remoteSelectLabels', [
  605. 'field' => parse_name($relationName, 1),
  606. 'className' => str_replace(['.php', '/'], ['', '\\'], '\\' . $field['form']['remote-model']),
  607. 'primaryKey' => $field['form']['remote-pk'] ?? 'id',
  608. 'foreignKey' => $field['name'],
  609. 'labelFieldName' => $field['form']['remote-field'] ?? 'name',
  610. ]);
  611. }
  612. foreach ($relationFields as $relationField) {
  613. if (!array_key_exists($relationField, $columns)) continue;
  614. $relationFieldPrefix = $relationName . '.';
  615. $relationFieldLangPrefix = strtolower($relationName) . '__';
  616. Helper::getDictData($dictEn, $columns[$relationField], 'en', $relationFieldLangPrefix);
  617. Helper::getDictData($dictZhCn, $columns[$relationField], 'zh-cn', $relationFieldLangPrefix);
  618. // 不允许双击编辑的字段
  619. if ($columns[$relationField]['designType'] == 'switch') {
  620. $this->indexVueData['dblClickNotEditColumn'][] = $field['name'];
  621. }
  622. // 列字典数据
  623. $columnDict = $this->getColumnDict($columns[$relationField], $relationFieldLangPrefix);
  624. // 表格列
  625. $columns[$relationField]['designType'] = $field['designType'];
  626. $columns[$relationField]['table']['render'] = 'tags';
  627. if ($field['designType'] == 'remoteSelects') {
  628. $columns[$relationField]['table']['operator'] = 'false';
  629. $this->indexVueData['tableColumn'][] = $this->getTableColumn($columns[$relationField], $columnDict, $relationFieldPrefix, $relationFieldLangPrefix);
  630. // 额外生成一个公共搜索,渲染为远程下拉的列
  631. unset($columns[$relationField]['table']['render']);
  632. $columns[$relationField]['table']['label'] = "t('" . $this->webTranslate . $relationFieldLangPrefix . $columns[$relationField]['name'] . "')";
  633. $columns[$relationField]['name'] = $field['name'];
  634. $columns[$relationField]['table']['show'] = 'false';
  635. $columns[$relationField]['table']['operator'] = 'FIND_IN_SET';
  636. $columns[$relationField]['table']['comSearchRender'] = 'remoteSelect';
  637. $columns[$relationField]['table']['remote'] = [
  638. 'pk' => $this->getRemoteSelectPk($field),
  639. 'field' => $field['form']['remote-field'] ?? 'name',
  640. 'remoteUrl' => $this->getRemoteSelectUrl($field),
  641. 'multiple' => 'true',
  642. ];
  643. $this->indexVueData['tableColumn'][] = $this->getTableColumn($columns[$relationField], $columnDict, '', $relationFieldLangPrefix);
  644. } else {
  645. $columns[$relationField]['table']['operator'] = 'LIKE';
  646. $this->indexVueData['tableColumn'][] = $this->getTableColumn($columns[$relationField], $columnDict, $relationFieldPrefix, $relationFieldLangPrefix);
  647. }
  648. }
  649. }
  650. $this->langTsData['en'] = array_merge($this->langTsData['en'], $dictEn);
  651. $this->langTsData['zh-cn'] = array_merge($this->langTsData['zh-cn'], $dictZhCn);
  652. }
  653. /**
  654. * 解析模型方法(设置器、获取器等)
  655. */
  656. private function parseModelMethods($field, &$modelData): void
  657. {
  658. // fieldType
  659. if ($field['designType'] == 'array') {
  660. $modelData['fieldType'][$field['name']] = 'json';
  661. } elseif (!in_array($field['name'], ['create_time', 'update_time', 'updatetime', 'createtime']) && $field['designType'] == 'datetime' && (in_array($field['type'], ['int', 'bigint']))) {
  662. $modelData['fieldType'][$field['name']] = 'timestamp:Y-m-d H:i:s';
  663. }
  664. // beforeInsertMixins
  665. if ($field['designType'] == 'spk') {
  666. $modelData['beforeInsertMixins']['snowflake'] = Helper::assembleStub('mixins/model/mixins/beforeInsertWithSnowflake', []);
  667. }
  668. // methods
  669. $fieldName = parse_name($field['name'], 1);
  670. if (in_array($field['designType'], $this->dtStringToArray)) {
  671. $modelData['methods'][] = Helper::assembleStub('mixins/model/getters/stringToArray', [
  672. 'field' => $fieldName
  673. ]);
  674. $modelData['methods'][] = Helper::assembleStub('mixins/model/setters/arrayToString', [
  675. 'field' => $fieldName
  676. ]);
  677. } elseif ($field['designType'] == 'array') {
  678. $modelData['methods'][] = Helper::assembleStub('mixins/model/getters/jsonDecode', [
  679. 'field' => $fieldName
  680. ]);
  681. } elseif ($field['designType'] == 'time') {
  682. $modelData['methods'][] = Helper::assembleStub('mixins/model/setters/time', [
  683. 'field' => $fieldName
  684. ]);
  685. } elseif ($field['designType'] == 'editor') {
  686. $modelData['methods'][] = Helper::assembleStub('mixins/model/getters/htmlDecode', [
  687. 'field' => $fieldName
  688. ]);
  689. } elseif ($field['designType'] == 'spk') {
  690. $modelData['methods'][] = Helper::assembleStub('mixins/model/getters/string', [
  691. 'field' => $fieldName
  692. ]);
  693. } elseif (in_array($field['type'], ['float', 'decimal', 'double'])) {
  694. $modelData['methods'][] = Helper::assembleStub('mixins/model/getters/float', [
  695. 'field' => $fieldName
  696. ]);
  697. }
  698. if ($field['designType'] == 'city') {
  699. $modelData['append'][] = $field['name'] . '_text';
  700. $modelData['methods'][] = Helper::assembleStub('mixins/model/getters/cityNames', [
  701. 'field' => $fieldName . 'Text',
  702. 'originalFieldName' => $field['name'],
  703. ]);
  704. }
  705. }
  706. /**
  707. * 控制器/模型等文件的一些杂项属性解析
  708. */
  709. private function parseSundryData($field, $table): void
  710. {
  711. if ($field['designType'] == 'editor') {
  712. $this->formVueData['bigDialog'] = true; // 加宽 dialog
  713. $this->controllerData['filterRule'] = "\n" . Helper::tab(2) . '$this->request->filter(\'clean_xss\');'; // 修改变量过滤规则
  714. }
  715. // 默认排序字段
  716. if ($table['defaultSortField'] && $table['defaultSortType']) {
  717. $defaultSortField = "{$table['defaultSortField']},{$table['defaultSortType']}";
  718. if ($defaultSortField == 'id,desc') {
  719. $this->controllerData['attr']['defaultSortField'] = '';
  720. } else {
  721. $this->controllerData['attr']['defaultSortField'] = $defaultSortField;
  722. $this->indexVueData['defaultOrder'] = Helper::buildDefaultOrder($table['defaultSortField'], $table['defaultSortType']);
  723. }
  724. }
  725. // 自定义了权重字段名称
  726. if ($field['originalDesignType'] == 'weigh' && $field['name'] != 'weigh') {
  727. $this->controllerData['attr']['weighField'] = $field['name'];
  728. }
  729. }
  730. /**
  731. * 组装前台表单的数据
  732. * @throws Throwable
  733. */
  734. private function getFormField($field, $columnDict, ?string $dbConnection = null): array
  735. {
  736. // 表单项属性
  737. $formField = [
  738. ':label' => 't(\'' . $this->webTranslate . $field['name'] . '\')',
  739. 'type' => $field['designType'],
  740. 'v-model' => 'baTable.form.items!.' . $field['name'],
  741. 'prop' => $field['name'],
  742. ];
  743. // 不同输入框的属性处理
  744. if ($columnDict || in_array($field['designType'], ['radio', 'checkbox', 'select', 'selects'])) {
  745. $formField[':input-attr']['content'] = $columnDict;
  746. } elseif ($field['designType'] == 'textarea') {
  747. $formField[':input-attr']['rows'] = (int)($field['form']['rows'] ?? 3);
  748. $formField['@keyup.enter.stop'] = '';
  749. $formField['@keyup.ctrl.enter'] = 'baTable.onSubmit(formRef)';
  750. } elseif ($field['designType'] == 'remoteSelect' || $field['designType'] == 'remoteSelects') {
  751. $formField[':input-attr']['pk'] = $this->getRemoteSelectPk($field);
  752. $formField[':input-attr']['field'] = $field['form']['remote-field'] ?? 'name';
  753. $formField[':input-attr']['remoteUrl'] = $this->getRemoteSelectUrl($field);
  754. } elseif ($field['designType'] == 'number') {
  755. $formField[':input-attr']['step'] = (int)($field['form']['step'] ?? 1);
  756. } elseif ($field['designType'] == 'icon') {
  757. $formField[':input-attr']['placement'] = 'top';
  758. } elseif ($field['designType'] == 'editor') {
  759. $formField['@keyup.enter.stop'] = '';
  760. $formField['@keyup.ctrl.enter'] = 'baTable.onSubmit(formRef)';
  761. }
  762. // placeholder
  763. if (!in_array($field['designType'], ['image', 'images', 'file', 'files', 'switch'])) {
  764. if (in_array($field['designType'], ['radio', 'checkbox', 'datetime', 'year', 'date', 'time', 'select', 'selects', 'remoteSelect', 'remoteSelects', 'city', 'icon'])) {
  765. $formField[':placeholder'] = "t('Please select field', { field: t('" . $this->webTranslate . $field['name'] . "') })";
  766. } else {
  767. $formField[':placeholder'] = "t('Please input field', { field: t('" . $this->webTranslate . $field['name'] . "') })";
  768. }
  769. }
  770. // 默认值
  771. if ($field['defaultType'] == 'INPUT') {
  772. $this->indexVueData['defaultItems'][$field['name']] = $field['default'];
  773. }
  774. // 部分生成类型的默认值需要额外处理
  775. if ($field['designType'] == 'editor') {
  776. $this->indexVueData['defaultItems'][$field['name']] = ($field['defaultType'] == 'INPUT' && $field['default']) ? $field['default'] : '';
  777. } elseif ($field['designType'] == 'array') {
  778. $this->indexVueData['defaultItems'][$field['name']] = "[]";
  779. } elseif ($field['defaultType'] == 'INPUT' && in_array($field['designType'], $this->dtStringToArray) && str_contains($field['default'], ',')) {
  780. $this->indexVueData['defaultItems'][$field['name']] = Helper::buildSimpleArray(explode(',', $field['default']));
  781. } elseif ($field['defaultType'] == 'INPUT' && in_array($field['designType'], ['number', 'float'])) {
  782. $this->indexVueData['defaultItems'][$field['name']] = (float)$field['default'];
  783. }
  784. // 无意义的默认值
  785. if (isset($field['default']) && in_array($field['designType'], ['switch', 'number', 'float', 'remoteSelect']) && $field['default'] == 0) {
  786. unset($this->indexVueData['defaultItems'][$field['name']]);
  787. }
  788. return $formField;
  789. }
  790. private function getRemoteSelectPk($field): string
  791. {
  792. $pk = $field['form']['remote-pk'] ?? 'id';
  793. if (!str_contains($pk, '.')) {
  794. if ($field['form']['remote-source-config-type'] == 'crud' && $field['form']['remote-model']) {
  795. $alias = parse_name(basename(str_replace('\\', '/', $field['form']['remote-model']), '.php'));
  796. } else {
  797. $alias = $field['form']['remote-primary-table-alias'] ?? '';
  798. }
  799. }
  800. return !empty($alias) ? "$alias.$pk" : $pk;
  801. }
  802. private function getRemoteSelectUrl($field): string
  803. {
  804. if ($field['form']['remote-source-config-type'] == 'crud' && $field['form']['remote-controller']) {
  805. $pathArr = [];
  806. $controller = explode(DIRECTORY_SEPARATOR, $field['form']['remote-controller']);
  807. $controller = str_replace('.php', '', $controller);
  808. $redundantDir = [
  809. 'app' => 0,
  810. 'admin' => 1,
  811. 'controller' => 2,
  812. ];
  813. foreach ($controller as $key => $item) {
  814. if (!array_key_exists($item, $redundantDir) || $key !== $redundantDir[$item]) {
  815. $pathArr[] = $item;
  816. }
  817. }
  818. $url = count($pathArr) > 1 ? implode('.', $pathArr) : $pathArr[0];
  819. return '/admin/' . $url . '/index';
  820. }
  821. return $field['form']['remote-url'];
  822. }
  823. private function getTableColumn($field, $columnDict, $fieldNamePrefix = '', $translationPrefix = ''): array
  824. {
  825. $column = [
  826. 'label' => "t('" . $this->webTranslate . $translationPrefix . $field['name'] . "')",
  827. 'prop' => $fieldNamePrefix . $field['name'] . ($field['designType'] == 'city' ? '_text' : ''),
  828. 'align' => 'center',
  829. ];
  830. // 模糊搜索增加一个placeholder
  831. if (isset($field['table']['operator']) && $field['table']['operator'] == 'LIKE') {
  832. $column['operatorPlaceholder'] = "t('Fuzzy query')";
  833. }
  834. // 合并前端预设的字段表格属性
  835. if (isset($field['table']) && $field['table']) {
  836. $column = array_merge($column, $field['table']);
  837. }
  838. // 需要值替换的渲染类型
  839. $columnReplaceValue = ['tag', 'tags', 'switch'];
  840. if (!in_array($field['designType'], ['remoteSelect', 'remoteSelects']) && ($columnDict || (isset($field['table']['render']) && in_array($field['table']['render'], $columnReplaceValue)))) {
  841. $column['replaceValue'] = $columnDict;
  842. }
  843. if (isset($column['render']) && $column['render'] == 'none') {
  844. unset($column['render']);
  845. }
  846. return $column;
  847. }
  848. private function getColumnDict($column, $translationPrefix = ''): array
  849. {
  850. $dict = [];
  851. // 确保字典中无翻译也可以识别到该值
  852. if (in_array($column['type'], ['enum', 'set'])) {
  853. $dataType = str_replace(' ', '', $column['dataType']);
  854. $columnData = substr($dataType, stripos($dataType, '(') + 1, -1);
  855. $columnData = explode(',', str_replace(["'", '"'], '', $columnData));
  856. foreach ($columnData as $columnDatum) {
  857. $dict[$columnDatum] = $column['name'] . ' ' . $columnDatum;
  858. }
  859. }
  860. $dictData = [];
  861. Helper::getDictData($dictData, $column, 'zh-cn', $translationPrefix);
  862. if ($dictData) {
  863. unset($dictData[$translationPrefix . $column['name']]);
  864. foreach ($dictData as $key => $item) {
  865. $keyName = str_replace($translationPrefix . $column['name'] . ' ', '', $key);
  866. $dict[$keyName] = "t('" . $this->webTranslate . $key . "')";
  867. }
  868. }
  869. return $dict;
  870. }
  871. }