GameConfigController.php 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125
  1. <?php
  2. namespace App\Module\Game\AdminControllers;
  3. use App\Module\Game\DCache\ChestJsonConfig;
  4. use App\Module\Game\DCache\DismantleJsonConfig;
  5. use App\Module\Game\DCache\FarmHouseJsonConfig;
  6. use App\Module\Game\DCache\FarmLandJsonConfig;
  7. use App\Module\Game\DCache\FarmSeedJsonConfig;
  8. use App\Module\Game\DCache\FarmShrineJsonConfig;
  9. use App\Module\Game\DCache\FundCurrencyJsonConfig;
  10. use App\Module\Game\DCache\ItemJsonConfig;
  11. use App\Module\Game\DCache\PetConfigJsonConfig;
  12. use App\Module\Game\DCache\PetLevelJsonConfig;
  13. use App\Module\Game\DCache\PetSkillJsonConfig;
  14. use App\Module\Game\DCache\PetJsonConfig;
  15. use App\Module\Game\DCache\RecipeJsonConfig;
  16. use App\Module\Pet\AdminControllers\Tools\SyncPetJsonTool;
  17. use App\Module\Pet\AdminControllers\Tools\RefreshPetJsonTool;
  18. use App\Module\Farm\AdminControllers\Tools\RefreshFarmHouseJsonTool;
  19. use App\Module\Farm\AdminControllers\Tools\RefreshFarmLandJsonTool;
  20. use App\Module\Farm\AdminControllers\Tools\RefreshFarmSeedJsonTool;
  21. use App\Module\Farm\AdminControllers\Tools\RefreshFarmShrineJsonTool;
  22. use App\Module\Farm\AdminControllers\Tools\SyncFarmHouseJsonTool;
  23. use App\Module\Farm\AdminControllers\Tools\SyncFarmLandJsonTool;
  24. use App\Module\Fund\AdminControllers\Tools\RefreshFundCurrencyJsonTool;
  25. use App\Module\Fund\AdminControllers\Tools\SyncFundCurrencyJsonTool;
  26. use App\Module\GameItems\AdminControllers\Tools\RefreshCheckTool;
  27. use App\Module\GameItems\AdminControllers\Tools\SyncChetsJsonTool;
  28. use App\Module\GameItems\AdminControllers\Tools\SyncDismantleJsonTool;
  29. use App\Module\GameItems\AdminControllers\Tools\SyncItemsJsonTool;
  30. use App\Module\GameItems\AdminControllers\Tools\SyncRecipeJsonTool;
  31. use Carbon\Carbon;
  32. use Dcat\Admin\Layout\Content;
  33. use Dcat\Admin\Layout\Row;
  34. use Dcat\Admin\Widgets\Card;
  35. use Dcat\Admin\Widgets\Table;
  36. use Dcat\Admin\Http\Controllers\AdminController;
  37. use Illuminate\Http\Request;
  38. use Illuminate\Support\Facades\Artisan;
  39. use Spatie\RouteAttributes\Attributes\Resource;
  40. use Spatie\RouteAttributes\Attributes\Get;
  41. use Spatie\RouteAttributes\Attributes\Post;
  42. use UCore\Helper\Datetime;
  43. /**
  44. * 游戏配置表管理控制器
  45. *
  46. * 用于显示和管理游戏中的各种配置表
  47. */
  48. #[Resource('game-jsonconfigs', names: 'dcat.admin.game-jsonconfigs')]
  49. class GameConfigController extends AdminController
  50. {
  51. /**
  52. * 刷新宠物配置表
  53. *
  54. * @return \Illuminate\Http\JsonResponse
  55. */
  56. #[Get('game-jsonconfigs/refresh-pets')]
  57. public function refreshPets()
  58. {
  59. try {
  60. // 强制刷新缓存
  61. PetConfigJsonConfig::getData([], true);
  62. PetLevelJsonConfig::getData([], true);
  63. PetSkillJsonConfig::getData([], true);
  64. return response()->json([
  65. 'status' => 'success',
  66. 'message' => '刷新成功'
  67. ]);
  68. } catch (\Exception $e) {
  69. return response()->json([
  70. 'status' => 'error',
  71. 'message' => '刷新失败: ' . $e->getMessage()
  72. ]);
  73. }
  74. }
  75. /**
  76. * 页面标题
  77. *
  78. * @var string
  79. */
  80. protected $title = '游戏配置表管理';
  81. /**
  82. * 页面描述
  83. *
  84. * @var string
  85. */
  86. protected $description = '查看和刷新游戏中的各种配置表';
  87. /**
  88. * 配置表首页
  89. *
  90. * @param Content $content
  91. * @return Content
  92. */
  93. public function index(Content $content)
  94. {
  95. return $content
  96. ->title($this->title)
  97. ->description($this->description)
  98. ->body(function (Row $row) {
  99. // 物品配置表卡片
  100. $row->column(6, $this->createConfigCard(
  101. '物品配置表',
  102. 'items.json',
  103. 'gameitems:generate-json',
  104. SyncItemsJsonTool::make(),
  105. $this->getItemConfigInfo()
  106. ));
  107. // 宝箱配置表卡片
  108. $row->column(6, $this->createConfigCard(
  109. '宝箱配置表',
  110. 'chest.json',
  111. 'gameitems:generate-chest-json',
  112. SyncChetsJsonTool::make(),
  113. $this->getChestConfigInfo()
  114. ));
  115. })
  116. ->body(function (Row $row) {
  117. // 合成配方配置表卡片
  118. $row->column(6, $this->createConfigCard(
  119. '物品合成配方配置表',
  120. 'recipe.json',
  121. 'gameitems:generate-recipe-json',
  122. SyncRecipeJsonTool::make(),
  123. $this->getRecipeConfigInfo()
  124. ));
  125. // 分解配方配置表卡片
  126. $row->column(6, $this->createConfigCard(
  127. '物品分解配方配置表',
  128. 'dismantle.json',
  129. 'gameitems:generate-dismantle-json',
  130. SyncDismantleJsonTool::make(),
  131. $this->getDismantleConfigInfo()
  132. ));
  133. })
  134. ->body(function (Row $row) {
  135. // 宠物基础配置卡片
  136. $row->column(6, $this->createConfigCard(
  137. '宠物基础配置表',
  138. 'pet_config.json',
  139. 'pet:generate-json',
  140. RefreshPetJsonTool::make(),
  141. $this->getPetConfigInfo(\App\Module\Pet\AdminControllers\Tools\RefreshCheckTool::checkSyncStatus('config'))
  142. ));
  143. // 宠物等级配置卡片
  144. $row->column(6, $this->createConfigCard(
  145. '宠物等级配置表',
  146. 'pet_levels.json',
  147. 'pet:generate-json',
  148. SyncPetJsonTool::make(),
  149. $this->getPetLevelConfigInfo(\App\Module\Pet\AdminControllers\Tools\RefreshCheckTool::checkSyncStatus('level'))
  150. ));
  151. })
  152. ->body(function (Row $row) {
  153. // 宠物技能配置卡片
  154. $row->column(6, $this->createConfigCard(
  155. '宠物技能配置表',
  156. 'pet_skills.json',
  157. 'pet:generate-json',
  158. RefreshPetJsonTool::make(),
  159. $this->getPetSkillConfigInfo(\App\Module\Pet\AdminControllers\Tools\RefreshCheckTool::checkSyncStatus('skill'))
  160. ));
  161. // 农场房屋配置表卡片
  162. $row->column(6, $this->createConfigCard(
  163. '农场房屋配置表',
  164. 'farm_house.json',
  165. 'farm:generate-house-json',
  166. RefreshFarmHouseJsonTool::make(),
  167. $this->getFarmHouseConfigInfo()
  168. ));
  169. })
  170. ->body(function (Row $row) {
  171. // 土地配置表卡片
  172. $row->column(6, $this->createConfigCard(
  173. '土地配置表',
  174. 'farm_land.json',
  175. 'farm:generate-land-json',
  176. RefreshFarmLandJsonTool::make(),
  177. $this->getFarmLandConfigInfo()
  178. ));
  179. // 种子配置表卡片
  180. $row->column(6, $this->createConfigCard(
  181. '种子配置表',
  182. 'farm_seed.json',
  183. 'farm:generate-seed-json',
  184. RefreshFarmSeedJsonTool::make(),
  185. $this->getFarmSeedConfigInfo()
  186. ));
  187. })
  188. ->body(function (Row $row) {
  189. // 神像配置表卡片
  190. $row->column(6, $this->createConfigCard(
  191. '神像配置表',
  192. 'farm_shrine.json',
  193. 'farm:generate-shrine-json',
  194. RefreshFarmShrineJsonTool::make(),
  195. $this->getFarmShrineConfigInfo()
  196. ));
  197. })
  198. ->body(function (Row $row) {
  199. // 货币配置表卡片
  200. $row->column(6, $this->createConfigCard(
  201. '货币配置表',
  202. 'currencies.json',
  203. 'fund:generate-currency-json',
  204. RefreshFundCurrencyJsonTool::make(),
  205. $this->getFundCurrencyConfigInfo(\App\Module\Fund\AdminControllers\Tools\RefreshCheckTool::checkSyncStatus())
  206. ));
  207. });
  208. }
  209. /**
  210. * 创建配置表信息卡片
  211. *
  212. * @param string $title 卡片标题
  213. * @param string $filename 文件名
  214. * @param string $command 生成命令
  215. * @param string $refreshUrl 刷新URL
  216. * @param array $info 配置信息
  217. * @return Card
  218. */
  219. protected function createConfigCard($title, $filename, $command, $refresh, $info)
  220. {
  221. $headers = [ '属性', '值' ];
  222. $rows = [];
  223. foreach ($info as $key => $value) {
  224. $rows[] = [ $key, $value ];
  225. }
  226. $card = new Card($title, Table::make($headers, $rows));
  227. $card->tool($refresh);
  228. // 处理文件名,获取第一个文件名(如果有多个文件,只取第一个)
  229. $firstFilename = explode(',', $filename)[0];
  230. $firstFilename = trim($firstFilename);
  231. // 特殊处理各种配置表的映射关系
  232. if ($firstFilename === 'pet_config.json') {
  233. $key = 'pet_config';
  234. } elseif ($firstFilename === 'pet_levels.json') {
  235. $key = 'pet_levels';
  236. } elseif ($firstFilename === 'pet_skills.json') {
  237. $key = 'pet_skills';
  238. } elseif ($firstFilename === 'farm_house.json') {
  239. $key = 'farm_house';
  240. } elseif ($firstFilename === 'farm_land.json') {
  241. $key = 'farm_land';
  242. } elseif ($firstFilename === 'farm_seed.json') {
  243. $key = 'farm_seed';
  244. } elseif ($firstFilename === 'farm_shrine.json') {
  245. $key = 'farm_shrine';
  246. } elseif ($firstFilename === 'currencies.json') {
  247. $key = 'currencies';
  248. } elseif ($firstFilename === 'chest.json') {
  249. $key = 'chest';
  250. } elseif ($firstFilename === 'items.json') {
  251. $key = 'items';
  252. } else {
  253. // 从文件名中提取key(去掉.json后缀)
  254. $key = str_replace('.json', '', $firstFilename);
  255. }
  256. // 构建查看JSON文件的链接
  257. $jsonViewLink = "<a href='game-jsonconfigs/view-json/{$key}' target='_blank' class='btn btn-sm btn-primary' style='margin-top:8px;'>查看JSON内容</a>";
  258. $card->footer("<code>文件: {$filename}</code><br><code>命令: php artisan {$command}</code><br>{$jsonViewLink}");
  259. return $card;
  260. }
  261. /**
  262. * 获取物品配置表信息
  263. *
  264. * @return array
  265. */
  266. protected function getItemConfigInfo()
  267. {
  268. $data = ItemJsonConfig::getData();
  269. $info = [
  270. '生成时间' => Datetime::ts2string($data['generated_ts']),
  271. '物品数量' => isset($data['items']) ? count($data['items']) : 0,
  272. ];
  273. return $info;
  274. }
  275. /**
  276. * 获取物品合成配方配置表信息
  277. *
  278. * @return array
  279. */
  280. protected function getRecipeConfigInfo()
  281. {
  282. $data = RecipeJsonConfig::getData();
  283. $info = [
  284. '生成时间' => isset($data['generated_ts']) ? Datetime::ts2string($data['generated_ts']) : '未生成',
  285. '配方数量' => isset($data['recipes']) ? count($data['recipes']) : 0,
  286. ];
  287. return $info;
  288. }
  289. /**
  290. * 获取物品分解配方配置表信息
  291. *
  292. * @return array
  293. */
  294. protected function getDismantleConfigInfo()
  295. {
  296. $data = DismantleJsonConfig::getData();
  297. $info = [
  298. '生成时间' => isset($data['generated_ts']) ? Datetime::ts2string($data['generated_ts']) : '未生成',
  299. '规则数量' => isset($data['dismantle_rules']) ? count($data['dismantle_rules']) : 0,
  300. ];
  301. return $info;
  302. }
  303. /**
  304. * 获取宝箱配置表信息
  305. *
  306. * @return array
  307. */
  308. protected function getChestConfigInfo()
  309. {
  310. $data = ChestJsonConfig::getData();
  311. $info = [
  312. '生成时间' => Datetime::ts2string($data['generated_ts']),
  313. '宝箱数量' => isset($data['chest']) ? count($data['chest']) : 0,
  314. ];
  315. return $info;
  316. }
  317. /**
  318. * 获取宠物基础配置表信息
  319. *
  320. * @param array $status 配置表状态
  321. * @return array
  322. */
  323. protected function getPetConfigInfo($status = null)
  324. {
  325. $data = PetConfigJsonConfig::getData();
  326. $info = [
  327. '生成时间' => isset($data['generated_ts']) ? Datetime::ts2string($data['generated_ts']) : '未生成',
  328. '宠物数量' => isset($data['pets']) ? count($data['pets']) : 0,
  329. ];
  330. if ($status) {
  331. $info['同步状态'] = $status['is_synced'] ? '已同步' : '需要更新';
  332. }
  333. return $info;
  334. }
  335. /**
  336. * 获取宠物等级配置表信息
  337. *
  338. * @param array $status 配置表状态
  339. * @return array
  340. */
  341. protected function getPetLevelConfigInfo($status = null)
  342. {
  343. $data = PetLevelJsonConfig::getData();
  344. $info = [
  345. '生成时间' => isset($data['generated_ts']) ? Datetime::ts2string($data['generated_ts']) : '未生成',
  346. '等级配置数量' => isset($data['pet_levels']) ? count($data['pet_levels']) : 0,
  347. ];
  348. if ($status) {
  349. $info['同步状态'] = $status['is_synced'] ? '已同步' : '需要更新';
  350. }
  351. return $info;
  352. }
  353. /**
  354. * 获取宠物技能配置表信息
  355. *
  356. * @param array $status 配置表状态
  357. * @return array
  358. */
  359. protected function getPetSkillConfigInfo($status = null)
  360. {
  361. $data = PetSkillJsonConfig::getData();
  362. $info = [
  363. '生成时间' => isset($data['generated_ts']) ? Datetime::ts2string($data['generated_ts']) : '未生成',
  364. '技能配置数量' => isset($data['pet_skills']) ? count($data['pet_skills']) : 0,
  365. ];
  366. if ($status) {
  367. $info['同步状态'] = $status['is_synced'] ? '已同步' : '需要更新';
  368. }
  369. return $info;
  370. }
  371. /**
  372. * 获取农场房屋配置表信息
  373. *
  374. * @return array
  375. */
  376. protected function getFarmHouseConfigInfo()
  377. {
  378. $data = FarmHouseJsonConfig::getData();
  379. $info = [
  380. '生成时间' => Datetime::ts2string($data['generated_ts']),
  381. '房屋配置数量' => isset($data['house_configs']) ? count($data['house_configs']) : 0,
  382. ];
  383. return $info;
  384. }
  385. /**
  386. * 获取土地配置表信息
  387. *
  388. * @return array
  389. */
  390. protected function getFarmLandConfigInfo()
  391. {
  392. $data = FarmLandJsonConfig::getData();
  393. $info = [
  394. '生成时间' => Datetime::ts2string($data['generated_ts']),
  395. '土地类型数量' => isset($data['land_types']) ? count($data['land_types']) : 0,
  396. '升级路径数量' => isset($data['upgrade_paths']) ? count($data['upgrade_paths']) : 0,
  397. ];
  398. return $info;
  399. }
  400. /**
  401. * 获取种子配置表信息
  402. *
  403. * @return array
  404. */
  405. protected function getFarmSeedConfigInfo()
  406. {
  407. $data = FarmSeedJsonConfig::getData();
  408. // 统计总的产出物品数量
  409. $totalOutputs = 0;
  410. if (isset($data['seeds'])) {
  411. foreach ($data['seeds'] as $seed) {
  412. $totalOutputs += count($seed['seed_outputs'] ?? []);
  413. }
  414. }
  415. $info = [
  416. '生成时间' => isset($data['generated_ts']) ? Datetime::ts2string($data['generated_ts']) : '未生成',
  417. '种子数量' => isset($data['seeds']) ? count($data['seeds']) : 0,
  418. '产出物品总数' => $totalOutputs,
  419. ];
  420. return $info;
  421. }
  422. /**
  423. * 获取货币配置表信息
  424. *
  425. * @param array $status 配置表状态
  426. * @return array
  427. */
  428. protected function getFundCurrencyConfigInfo($status = null)
  429. {
  430. $data = FundCurrencyJsonConfig::getData([],true);
  431. // dd($data);
  432. // 计算有关联币种的账户种类数量
  433. $linkedAccountsCount = 0;
  434. if (isset($data['fund'])) {
  435. foreach ($data['fund'] as $fundConfig) {
  436. if (!empty($fundConfig['currency_id'])) {
  437. $linkedAccountsCount++;
  438. }
  439. }
  440. }
  441. $info = [
  442. '生成时间' => Datetime::ts2string($data['generated_ts']),
  443. '币种数量' => isset($data['currencies']) ? count($data['currencies']) : 0,
  444. '账户种类数量' => isset($data['fund']) ? count($data['fund']) : 0,
  445. '已关联币种的账户数量' => $linkedAccountsCount,
  446. ];
  447. if ($status) {
  448. $info['同步状态'] = $status['is_synced'] ? '已同步' : '需要更新';
  449. }
  450. return $info;
  451. }
  452. /**
  453. * 获取神像配置表信息
  454. *
  455. * @return array
  456. */
  457. protected function getFarmShrineConfigInfo()
  458. {
  459. $data = FarmShrineJsonConfig::getData();
  460. $info = [
  461. '生成时间' => isset($data['generated_ts']) ? Datetime::ts2string($data['generated_ts']) : '未生成',
  462. '神像配置数量' => isset($data['shrine_configs']) ? count($data['shrine_configs']) : 0,
  463. ];
  464. return $info;
  465. }
  466. /**
  467. * 友好地显示JSON配置数据
  468. *
  469. * @param string $key 配置表键名
  470. * @return Content
  471. */
  472. #[Get('game-jsonconfigs/view-json/{key}')]
  473. public function viewJson($key, Content $content)
  474. {
  475. // 配置表映射关系
  476. $map = [
  477. 'items' => [ItemJsonConfig::class, '物品配置表'],
  478. 'chest' => [ChestJsonConfig::class, '宝箱配置表'],
  479. 'recipe' => [RecipeJsonConfig::class, '物品合成配方配置表'],
  480. 'dismantle' => [DismantleJsonConfig::class, '物品分解配方配置表'],
  481. 'pet_config' => [PetConfigJsonConfig::class, '宠物基础配置表'],
  482. 'pet_levels' => [PetLevelJsonConfig::class, '宠物等级配置表'],
  483. 'pet_skills' => [PetSkillJsonConfig::class, '宠物技能配置表'],
  484. 'pets' => [PetJsonConfig::class, '宠物配置表(旧版)'],
  485. 'farm_house' => [FarmHouseJsonConfig::class, '农场房屋配置表'],
  486. 'farm_land' => [FarmLandJsonConfig::class, '土地配置表'],
  487. 'farm_seed' => [FarmSeedJsonConfig::class, '种子配置表'],
  488. 'farm_shrine' => [FarmShrineJsonConfig::class, '神像配置表'],
  489. 'currencies' => [FundCurrencyJsonConfig::class, '货币配置表'],
  490. ];
  491. // 检查请求的配置表是否存在
  492. if (!isset($map[$key])) {
  493. return $content
  494. ->title('错误')
  495. ->description('配置表查看')
  496. ->body(new Card('错误', '配置表不存在'));
  497. }
  498. try {
  499. // 获取配置表数据
  500. $configClass = $map[$key][0];
  501. $title = $map[$key][1];
  502. $data = $configClass::getData();
  503. // 如果数据为空,返回错误
  504. if (empty($data)) {
  505. return $content
  506. ->title('错误')
  507. ->description('配置表查看')
  508. ->body(new Card('错误', '配置表数据为空'));
  509. }
  510. // 创建JSON查看器
  511. $jsonViewerId = 'json-viewer-' . uniqid();
  512. // 格式化JSON数据
  513. $formattedJson = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
  514. if ($formattedJson === false) {
  515. $formattedJson = json_encode(["error" => "无法解析JSON数据"], JSON_PRETTY_PRINT);
  516. }
  517. // 转义HTML特殊字符
  518. $escapedJson = htmlspecialchars($formattedJson, ENT_QUOTES, 'UTF-8');
  519. $html = <<<HTML
  520. <div>
  521. <style>
  522. .json-viewer {
  523. max-height: 80vh;
  524. overflow: auto;
  525. background-color: #f8f9fa;
  526. border-radius: 4px;
  527. padding: 15px;
  528. font-family: monospace;
  529. white-space: pre;
  530. font-size: 14px;
  531. line-height: 1.5;
  532. }
  533. .json-key { color: #a52a2a; }
  534. .json-string { color: #008000; }
  535. .json-number { color: #0000ff; }
  536. .json-boolean { color: #b22222; }
  537. .json-null { color: #808080; }
  538. </style>
  539. <div class="mb-2">
  540. <div class="input-group" style="margin-bottom: 10px;">
  541. <input type="text" class="form-control" id="search-{$jsonViewerId}" placeholder="搜索...">
  542. <div class="input-group-append">
  543. <button class="btn btn-default" id="search-btn-{$jsonViewerId}">搜索</button>
  544. </div>
  545. </div>
  546. <div class="btn-group">
  547. <button class="btn btn-sm btn-default" id="toggle-{$jsonViewerId}">折叠/展开</button>
  548. <button class="btn btn-sm btn-default" id="copy-{$jsonViewerId}">复制JSON</button>
  549. </div>
  550. </div>
  551. <pre id="{$jsonViewerId}" class="json-viewer">{$escapedJson}</pre>
  552. <script>
  553. $(document).ready(function() {
  554. // 高亮JSON语法
  555. function highlightJson() {
  556. var jsonContent = document.getElementById('{$jsonViewerId}');
  557. var jsonText = jsonContent.textContent;
  558. // 使用简单的正则表达式进行高亮
  559. var highlighted = jsonText
  560. // 高亮键
  561. .replace(/"([^"]+)"(?=\s*:)/g, '<span class="json-key">"$1"</span>')
  562. // 高亮字符串值
  563. .replace(/:\s*"([^"]*)"/g, ': <span class="json-string">"$1"</span>')
  564. // 高亮数字
  565. .replace(/:\s*(-?\d+(\.\d+)?)/g, ': <span class="json-number">$1</span>')
  566. // 高亮布尔值和null
  567. .replace(/:\s*(true|false|null)/g, ': <span class="json-boolean">$1</span>');
  568. jsonContent.innerHTML = highlighted;
  569. }
  570. // 复制JSON按钮
  571. document.getElementById('copy-{$jsonViewerId}').addEventListener('click', function() {
  572. var jsonContent = document.getElementById('{$jsonViewerId}');
  573. var jsonText = jsonContent.textContent;
  574. var tempTextarea = document.createElement('textarea');
  575. tempTextarea.value = jsonText;
  576. document.body.appendChild(tempTextarea);
  577. tempTextarea.select();
  578. document.execCommand('copy');
  579. document.body.removeChild(tempTextarea);
  580. alert('JSON已复制到剪贴板');
  581. });
  582. // 折叠/展开功能
  583. document.getElementById('toggle-{$jsonViewerId}').addEventListener('click', function() {
  584. var jsonViewer = document.getElementById('{$jsonViewerId}');
  585. var isCollapsed = jsonViewer.classList.contains('collapsed');
  586. if (isCollapsed) {
  587. // 展开
  588. jsonViewer.classList.remove('collapsed');
  589. jsonViewer.style.maxHeight = '80vh';
  590. this.textContent = '折叠';
  591. } else {
  592. // 折叠
  593. jsonViewer.classList.add('collapsed');
  594. jsonViewer.style.maxHeight = '200px';
  595. this.textContent = '展开';
  596. }
  597. });
  598. // 初始化
  599. highlightJson();
  600. // 搜索功能
  601. document.getElementById('search-btn-{$jsonViewerId}').addEventListener('click', function() {
  602. var searchText = document.getElementById('search-{$jsonViewerId}').value.trim();
  603. if (!searchText) return;
  604. // 展开JSON查看器
  605. var jsonViewer = document.getElementById('{$jsonViewerId}');
  606. jsonViewer.classList.remove('collapsed');
  607. jsonViewer.style.maxHeight = '80vh';
  608. document.getElementById('toggle-{$jsonViewerId}').textContent = '折叠';
  609. // 移除之前的高亮
  610. var content = jsonViewer.innerHTML;
  611. content = content.replace(/<mark class="highlight">(.*?)<\/mark>/g, '$1');
  612. // 高亮搜索文本
  613. if (searchText) {
  614. var regex = new RegExp('(' + searchText.replace(/[.*+?^$\{\}()|[\]\\]/g, '\\$&') + ')', 'gi');
  615. content = content.replace(regex, '<mark class="highlight" style="background-color: yellow; padding: 2px;">$1</mark>');
  616. }
  617. jsonViewer.innerHTML = content;
  618. // 滚动到第一个匹配项
  619. var firstHighlight = jsonViewer.querySelector('mark.highlight');
  620. if (firstHighlight) {
  621. firstHighlight.scrollIntoView({
  622. behavior: 'smooth',
  623. block: 'center'
  624. });
  625. } else {
  626. alert('未找到匹配项');
  627. }
  628. });
  629. // 绑定回车键搜索
  630. document.getElementById('search-{$jsonViewerId}').addEventListener('keypress', function(e) {
  631. if (e.key === 'Enter') {
  632. document.getElementById('search-btn-{$jsonViewerId}').click();
  633. }
  634. });
  635. // 默认折叠
  636. document.getElementById('toggle-{$jsonViewerId}').textContent = '展开';
  637. document.getElementById('{$jsonViewerId}').classList.add('collapsed');
  638. document.getElementById('{$jsonViewerId}').style.maxHeight = '200px';
  639. });
  640. </script>
  641. </div>
  642. HTML;
  643. // 创建卡片
  644. $card = new Card($title, $html);
  645. // 添加原始JSON链接
  646. $card->tool('<a href="/json/'.$key.'.json" target="_blank" class="btn btn-sm btn-default">查看原始JSON</a>');
  647. return $content
  648. ->title('配置表查看')
  649. ->description($title)
  650. ->body($card);
  651. } catch (\Exception $e) {
  652. // 返回错误响应
  653. return $content
  654. ->title('错误')
  655. ->description('配置表查看')
  656. ->body(new Card('错误', '获取配置表数据失败: ' . $e->getMessage()));
  657. }
  658. }
  659. /**
  660. * 创建JSON查看器
  661. *
  662. * @param mixed $data 要显示的数据(数组或对象)
  663. * @return string
  664. */
  665. protected function createJsonViewer($data)
  666. {
  667. // 生成唯一ID,避免多个查看器冲突
  668. $viewerId = 'json-viewer-' . uniqid();
  669. // 确保数据是格式化的JSON字符串
  670. $jsonString = is_string($data) ? $data : json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
  671. // 转义HTML特殊字符
  672. $escapedJson = htmlspecialchars($jsonString, ENT_QUOTES, 'UTF-8');
  673. // 使用简单的方式显示JSON数据
  674. $html = <<<HTML
  675. <div id="{$viewerId}" class="json-viewer">
  676. <style>
  677. .json-viewer {
  678. font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace;
  679. font-size: 14px;
  680. line-height: 1.5;
  681. background-color: #f8f9fa;
  682. border-radius: 4px;
  683. padding: 15px;
  684. overflow: auto;
  685. max-height: 80vh;
  686. }
  687. .json-viewer pre {
  688. margin: 0;
  689. padding: 0;
  690. white-space: pre-wrap;
  691. word-wrap: break-word;
  692. }
  693. /* 工具栏样式 */
  694. .json-toolbar {
  695. margin-bottom: 10px;
  696. display: flex;
  697. gap: 10px;
  698. }
  699. .json-toolbar button {
  700. padding: 5px 10px;
  701. background-color: #f0f0f0;
  702. border: 1px solid #ddd;
  703. border-radius: 4px;
  704. cursor: pointer;
  705. }
  706. .json-toolbar button:hover {
  707. background-color: #e0e0e0;
  708. }
  709. /* JSON语法高亮 */
  710. .json-key { color: #a52a2a; }
  711. .json-string { color: #008000; }
  712. .json-number { color: #0000ff; }
  713. .json-boolean { color: #b22222; }
  714. .json-null { color: #808080; }
  715. /* 搜索高亮 */
  716. .json-highlight {
  717. background-color: #ffff00;
  718. padding: 2px;
  719. border-radius: 2px;
  720. }
  721. /* 折叠/展开控件样式 */
  722. .json-toggle {
  723. cursor: pointer;
  724. user-select: none;
  725. }
  726. .json-toggle:before {
  727. content: "▼";
  728. display: inline-block;
  729. margin-right: 5px;
  730. color: #555;
  731. font-size: 10px;
  732. }
  733. .json-toggle.collapsed:before {
  734. content: "►";
  735. }
  736. .json-collapsed {
  737. display: none;
  738. }
  739. .json-placeholder {
  740. color: #777;
  741. font-style: italic;
  742. }
  743. </style>
  744. <div class="json-toolbar">
  745. <input type="text" id="{$viewerId}-search" placeholder="搜索..." style="padding: 5px; margin-right: 10px; width: 200px;">
  746. <button id="{$viewerId}-expand-all-btn">展开全部</button>
  747. <button id="{$viewerId}-collapse-all-btn">折叠全部</button>
  748. <button id="{$viewerId}-copy-json-btn">复制JSON</button>
  749. </div>
  750. <pre id="{$viewerId}-content">{$escapedJson}</pre>
  751. <script>
  752. $(document).ready(function() {
  753. // 获取当前查看器的ID
  754. var viewerId = '{$viewerId}';
  755. // 解析JSON并添加折叠功能
  756. function processJSON() {
  757. try {
  758. // 获取原始JSON文本
  759. var jsonContent = document.getElementById(viewerId + '-content');
  760. var jsonText = jsonContent.textContent;
  761. var jsonObj = JSON.parse(jsonText);
  762. // 将JSON对象转换为HTML
  763. var html = formatJSON(jsonObj, 0);
  764. jsonContent.innerHTML = html;
  765. // 添加折叠/展开事件处理
  766. $('#' + viewerId + ' .json-toggle').click(function() {
  767. $(this).toggleClass('collapsed');
  768. var target = $(this).next('.json-collapsible');
  769. target.toggleClass('json-collapsed');
  770. // 如果折叠,显示占位符
  771. var placeholder = $(this).next().next('.json-placeholder');
  772. if (placeholder.length) {
  773. placeholder.toggle();
  774. }
  775. });
  776. // 默认折叠所有嵌套超过1层的对象
  777. collapseLevel(2);
  778. } catch (e) {
  779. console.error('JSON解析错误:', e);
  780. // 如果解析失败,回退到简单的语法高亮
  781. simpleHighlight();
  782. }
  783. }
  784. // 简单的语法高亮(作为备选方案)
  785. function simpleHighlight() {
  786. var jsonContent = document.getElementById(viewerId + '-content');
  787. var jsonText = jsonContent.textContent;
  788. // 使用简单的正则表达式进行高亮
  789. var highlighted = jsonText
  790. // 高亮键
  791. .replace(/"([^"]+)"(?=\s*:)/g, '<span class="json-key">"$1"</span>')
  792. // 高亮字符串值
  793. .replace(/:\s*"([^"]*)"/g, ': <span class="json-string">"$1"</span>')
  794. // 高亮数字
  795. .replace(/:\s*(-?\d+(\.\d+)?)/g, ': <span class="json-number">$1</span>')
  796. // 高亮布尔值和null
  797. .replace(/:\s*(true|false|null)/g, ': <span class="json-boolean">$1</span>');
  798. jsonContent.innerHTML = highlighted;
  799. }
  800. // 格式化JSON对象为HTML
  801. function formatJSON(obj, level) {
  802. var indent = Array(level + 1).join(' '); // 兼容性更好的缩进方法
  803. var html = '';
  804. if (obj === null) {
  805. return '<span class="json-null">null</span>';
  806. }
  807. if (typeof obj === 'boolean') {
  808. return '<span class="json-boolean">' + obj + '</span>';
  809. }
  810. if (typeof obj === 'number') {
  811. return '<span class="json-number">' + obj + '</span>';
  812. }
  813. if (typeof obj === 'string') {
  814. return '<span class="json-string">"' + escapeHTML(obj) + '"</span>';
  815. }
  816. if (Array.isArray(obj)) {
  817. if (obj.length === 0) {
  818. return '[]';
  819. }
  820. html += '<span class="json-toggle"></span>[<span class="json-collapsible">';
  821. for (var i = 0; i < obj.length; i++) {
  822. html += '\\n' + indent + ' ' + formatJSON(obj[i], level + 1);
  823. if (i < obj.length - 1) {
  824. html += ',';
  825. }
  826. }
  827. html += '\\n' + indent + '</span>]<span class="json-placeholder json-collapsed"> [...] </span>';
  828. return html;
  829. }
  830. if (typeof obj === 'object') {
  831. var keys = Object.keys(obj);
  832. if (keys.length === 0) {
  833. return '{}';
  834. }
  835. html += '<span class="json-toggle"></span>{<span class="json-collapsible">';
  836. for (var i = 0; i < keys.length; i++) {
  837. var key = keys[i];
  838. html += '\\n' + indent + ' <span class="json-key">"' + escapeHTML(key) + '"</span>: ' + formatJSON(obj[key], level + 1);
  839. if (i < keys.length - 1) {
  840. html += ',';
  841. }
  842. }
  843. html += '\\n' + indent + '</span>}<span class="json-placeholder json-collapsed"> {...} </span>';
  844. return html;
  845. }
  846. return String(obj);
  847. }
  848. // 转义HTML特殊字符
  849. function escapeHTML(str) {
  850. return str
  851. .replace(/&/g, '&amp;')
  852. .replace(/</g, '&lt;')
  853. .replace(/>/g, '&gt;')
  854. .replace(/"/g, '&quot;')
  855. .replace(/'/g, '&#039;');
  856. }
  857. // 折叠指定层级以下的所有元素
  858. function collapseLevel(level) {
  859. $('#' + viewerId + ' .json-toggle').each(function() {
  860. // 计算当前元素的嵌套层级
  861. var currentLevel = $(this).parents('.json-collapsible').length;
  862. if (currentLevel >= level - 1) {
  863. if (!$(this).hasClass('collapsed')) {
  864. $(this).addClass('collapsed');
  865. $(this).next('.json-collapsible').addClass('json-collapsed');
  866. $(this).next().next('.json-placeholder').show();
  867. }
  868. }
  869. });
  870. }
  871. // 展开所有元素
  872. function expandAll() {
  873. $('#' + viewerId + ' .json-toggle').removeClass('collapsed');
  874. $('#' + viewerId + ' .json-collapsible').removeClass('json-collapsed');
  875. $('#' + viewerId + ' .json-placeholder').hide();
  876. }
  877. // 折叠所有元素
  878. function collapseAll() {
  879. $('#' + viewerId + ' .json-toggle').addClass('collapsed');
  880. $('#' + viewerId + ' .json-collapsible').addClass('json-collapsed');
  881. $('#' + viewerId + ' .json-placeholder').show();
  882. }
  883. // 搜索并高亮匹配的文本
  884. function searchAndHighlight(searchText) {
  885. try {
  886. // 创建正则表达式,忽略大小写
  887. var regex = new RegExp(searchText, 'gi');
  888. // 搜索所有文本节点
  889. $('#' + viewerId + ' .json-collapsible').each(function() {
  890. var $this = $(this);
  891. var content = $this.text();
  892. if (content.match(regex)) {
  893. // 展开包含匹配文本的节点
  894. var $toggle = $this.prev('.json-toggle');
  895. if ($toggle.hasClass('collapsed')) {
  896. $toggle.removeClass('collapsed');
  897. $this.removeClass('json-collapsed');
  898. $this.next('.json-placeholder').hide();
  899. }
  900. // 展开所有父节点
  901. $this.parents('.json-collapsible').each(function() {
  902. var $parentToggle = $(this).prev('.json-toggle');
  903. if ($parentToggle.hasClass('collapsed')) {
  904. $parentToggle.removeClass('collapsed');
  905. $(this).removeClass('json-collapsed');
  906. $(this).next('.json-placeholder').hide();
  907. }
  908. });
  909. }
  910. });
  911. // 高亮匹配的文本
  912. $('#' + viewerId + ' .json-key, #' + viewerId + ' .json-string, #' + viewerId + ' .json-number, #' + viewerId + ' .json-boolean, #' + viewerId + ' .json-null').each(function() {
  913. var $this = $(this);
  914. var content = $this.text();
  915. if (content.match(regex)) {
  916. var highlightedContent = content.replace(regex, function(match) {
  917. return '<span class="json-highlight">' + match + '</span>';
  918. });
  919. $this.html(highlightedContent);
  920. }
  921. });
  922. // 滚动到第一个匹配项
  923. var $firstHighlight = $('#' + viewerId + ' .json-highlight').first();
  924. if ($firstHighlight.length) {
  925. var container = document.getElementById(viewerId);
  926. var highlightOffset = $firstHighlight.offset().top;
  927. var containerOffset = $(container).offset().top;
  928. var scrollTop = highlightOffset - containerOffset - 100;
  929. $(container).animate({
  930. scrollTop: scrollTop
  931. }, 300);
  932. }
  933. } catch (e) {
  934. console.error('搜索错误:', e);
  935. }
  936. }
  937. // 初始化
  938. processJSON();
  939. // 绑定工具栏按钮事件
  940. $('#' + viewerId + '-expand-all-btn').click(expandAll);
  941. $('#' + viewerId + '-collapse-all-btn').click(collapseAll);
  942. // 搜索功能
  943. $('#' + viewerId + '-search').on('input', function() {
  944. var searchText = $(this).val().trim();
  945. // 移除所有高亮
  946. $('#' + viewerId + ' .json-highlight').removeClass('json-highlight');
  947. if (searchText.length > 0) {
  948. // 搜索并高亮匹配的文本
  949. searchAndHighlight(searchText);
  950. }
  951. });
  952. // 复制JSON按钮
  953. $('#' + viewerId + '-copy-json-btn').click(function() {
  954. var jsonContent = document.getElementById(viewerId + '-content');
  955. var jsonText = jsonContent.textContent || jsonContent.innerText;
  956. // 创建一个临时元素来存储纯文本JSON
  957. var tempTextarea = $('<textarea>');
  958. $('body').append(tempTextarea);
  959. // 尝试解析和格式化JSON
  960. try {
  961. var jsonObj = JSON.parse(jsonText.replace(/[\u0000-\u001F]+/g, ' '));
  962. tempTextarea.val(JSON.stringify(jsonObj, null, 4));
  963. } catch (e) {
  964. // 如果解析失败,使用原始文本
  965. tempTextarea.val(jsonText);
  966. }
  967. tempTextarea.select();
  968. document.execCommand('copy');
  969. tempTextarea.remove();
  970. alert('JSON已复制到剪贴板');
  971. });
  972. });
  973. </script>
  974. </div>
  975. HTML;
  976. return $html;
  977. }
  978. }