GameConfigController.php 36 KB

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