Browse Source

feat(game): 添加物品合成与分解配方配置表

- 在 JsonController 中添加 recipe 和 dismantle 配置表的处理
- 在 GameConfigController 中添加合成与分解配方配置表的查看功能
- 实现配方配置表信息的获取和展示
- 添加 JSON 数据的格式化和搜索功能
Your Name 7 months ago
parent
commit
4527ef928f

+ 4 - 0
app/Module/AppGame/HttpControllers/JsonController.php

@@ -4,11 +4,13 @@ namespace App\Module\AppGame\HttpControllers;
 
 use App\Http\Controllers\Controller;
 use App\Module\Game\DCache\ChestJsonConfig;
+use App\Module\Game\DCache\DismantleJsonConfig;
 use App\Module\Game\DCache\FarmHouseJsonConfig;
 use App\Module\Game\DCache\FarmLandJsonConfig;
 use App\Module\Game\DCache\FundCurrencyJsonConfig;
 use App\Module\Game\DCache\ItemJsonConfig;
 use App\Module\Game\DCache\PetJsonConfig;
+use App\Module\Game\DCache\RecipeJsonConfig;
 use Illuminate\Support\Facades\Log;
 use UCore\Exception\HandleNotException;
 use Uraus\Kku\Common\RESPONSE_CODE;
@@ -39,6 +41,8 @@ class JsonController extends Controller
             'farm_house' => FarmHouseJsonConfig::class,
             'farm_land' => FarmLandJsonConfig::class,
             'currencies' => FundCurrencyJsonConfig::class,
+            'recipe' => RecipeJsonConfig::class,
+            'dismantle' => DismantleJsonConfig::class,
         ];
 
         // 检查请求的配置表是否存在

+ 645 - 1
app/Module/Game/AdminControllers/GameConfigController.php

@@ -3,14 +3,18 @@
 namespace App\Module\Game\AdminControllers;
 
 use App\Module\Game\DCache\ChestJsonConfig;
+use App\Module\Game\DCache\DismantleJsonConfig;
 use App\Module\Game\DCache\FarmHouseJsonConfig;
 use App\Module\Game\DCache\FarmLandJsonConfig;
 use App\Module\Game\DCache\FundCurrencyJsonConfig;
 use App\Module\Game\DCache\ItemJsonConfig;
 use App\Module\Game\DCache\PetJsonConfig;
+use App\Module\Game\DCache\RecipeJsonConfig;
 use App\Module\GameItems\AdminControllers\Tools\RefreshCheckTool;
 use App\Module\GameItems\AdminControllers\Tools\SyncChetsJsonTool;
+use App\Module\GameItems\AdminControllers\Tools\SyncDismantleJsonTool;
 use App\Module\GameItems\AdminControllers\Tools\SyncItemsJsonTool;
+use App\Module\GameItems\AdminControllers\Tools\SyncRecipeJsonTool;
 use Carbon\Carbon;
 use Dcat\Admin\Layout\Content;
 use Dcat\Admin\Layout\Row;
@@ -148,6 +152,25 @@ class GameConfigController extends AdminController
                     $this->getChestConfigInfo()
                 ));
             })
+            ->body(function (Row $row) {
+                // 合成配方配置表卡片
+                $row->column(6, $this->createConfigCard(
+                    '物品合成配方配置表',
+                    'recipe.json',
+                    'gameitems:generate-recipe-json',
+                    SyncRecipeJsonTool::make(),
+                    $this->getRecipeConfigInfo()
+                ));
+
+                // 分解配方配置表卡片
+                $row->column(6, $this->createConfigCard(
+                    '物品分解配方配置表',
+                    'dismantle.json',
+                    'gameitems:generate-dismantle-json',
+                    SyncDismantleJsonTool::make(),
+                    $this->getDismantleConfigInfo()
+                ));
+            })
             ->body(function (Row $row) {
                 // 宠物配置表卡片
                 $row->column(6, $this->createConfigCard(
@@ -233,7 +256,7 @@ class GameConfigController extends AdminController
         }
 
         // 构建查看JSON文件的链接
-        $jsonViewLink = "<a href='/json/{$key}.json' target='_blank' class='btn btn-sm btn-primary' style='margin-top:8px;'>查看JSON内容</a>";
+        $jsonViewLink = "<a href='game-jsonconfigs/view-json/{$key}' target='_blank' class='btn btn-sm btn-primary' style='margin-top:8px;'>查看JSON内容</a>";
 
         $card->footer("<code>文件: {$filename}</code><br><code>命令: php artisan {$command}</code><br>{$jsonViewLink}");
 
@@ -257,6 +280,38 @@ class GameConfigController extends AdminController
         return $info;
     }
 
+    /**
+     * 获取物品合成配方配置表信息
+     *
+     * @return array
+     */
+    protected function getRecipeConfigInfo()
+    {
+        $data = RecipeJsonConfig::getData();
+        $info = [
+            '生成时间'  => isset($data['generated_ts']) ? Datetime::ts2string($data['generated_ts']) : '未生成',
+            '配方数量'  => isset($data['recipes']) ? count($data['recipes']) : 0,
+        ];
+
+        return $info;
+    }
+
+    /**
+     * 获取物品分解配方配置表信息
+     *
+     * @return array
+     */
+    protected function getDismantleConfigInfo()
+    {
+        $data = DismantleJsonConfig::getData();
+        $info = [
+            '生成时间'  => isset($data['generated_ts']) ? Datetime::ts2string($data['generated_ts']) : '未生成',
+            '规则数量'  => isset($data['dismantle_rules']) ? count($data['dismantle_rules']) : 0,
+        ];
+
+        return $info;
+    }
+
     /**
      * 获取宝箱配置表信息
      *
@@ -359,5 +414,594 @@ class GameConfigController extends AdminController
         return $info;
     }
 
+    /**
+     * 友好地显示JSON配置数据
+     *
+     * @param string $key 配置表键名
+     * @return Content
+     */
+    #[Get('game-jsonconfigs/view-json/{key}')]
+    public function viewJson($key, Content $content)
+    {
+        // 配置表映射关系
+        $map = [
+            'items' => [ItemJsonConfig::class, '物品配置表'],
+            'chest' => [ChestJsonConfig::class, '宝箱配置表'],
+            'recipe' => [RecipeJsonConfig::class, '物品合成配方配置表'],
+            'dismantle' => [DismantleJsonConfig::class, '物品分解配方配置表'],
+            'pets' => [PetJsonConfig::class, '宠物配置表'],
+            'farm_house' => [FarmHouseJsonConfig::class, '农场房屋配置表'],
+            'farm_land' => [FarmLandJsonConfig::class, '土地配置表'],
+            'currencies' => [FundCurrencyJsonConfig::class, '货币配置表'],
+        ];
+
+        // 检查请求的配置表是否存在
+        if (!isset($map[$key])) {
+            return $content
+                ->title('错误')
+                ->description('配置表查看')
+                ->body(new Card('错误', '配置表不存在'));
+        }
+
+        try {
+            // 获取配置表数据
+            $configClass = $map[$key][0];
+            $title = $map[$key][1];
+            $data = $configClass::getData();
+
+            // 如果数据为空,返回错误
+            if (empty($data)) {
+                return $content
+                    ->title('错误')
+                    ->description('配置表查看')
+                    ->body(new Card('错误', '配置表数据为空'));
+            }
+
+            // 创建JSON查看器
+            $jsonViewerId = 'json-viewer-' . uniqid();
+
+            // 格式化JSON数据
+            $formattedJson = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
+            if ($formattedJson === false) {
+                $formattedJson = json_encode(["error" => "无法解析JSON数据"], JSON_PRETTY_PRINT);
+            }
+
+            // 转义HTML特殊字符
+            $escapedJson = htmlspecialchars($formattedJson, ENT_QUOTES, 'UTF-8');
+
+            $html = <<<HTML
+<div>
+    <style>
+        .json-viewer {
+            max-height: 80vh;
+            overflow: auto;
+            background-color: #f8f9fa;
+            border-radius: 4px;
+            padding: 15px;
+            font-family: monospace;
+            white-space: pre;
+            font-size: 14px;
+            line-height: 1.5;
+        }
+        .json-key { color: #a52a2a; }
+        .json-string { color: #008000; }
+        .json-number { color: #0000ff; }
+        .json-boolean { color: #b22222; }
+        .json-null { color: #808080; }
+    </style>
+
+    <div class="mb-2">
+        <div class="input-group" style="margin-bottom: 10px;">
+            <input type="text" class="form-control" id="search-{$jsonViewerId}" placeholder="搜索...">
+            <div class="input-group-append">
+                <button class="btn btn-default" id="search-btn-{$jsonViewerId}">搜索</button>
+            </div>
+        </div>
+        <div class="btn-group">
+            <button class="btn btn-sm btn-default" id="toggle-{$jsonViewerId}">折叠/展开</button>
+            <button class="btn btn-sm btn-default" id="copy-{$jsonViewerId}">复制JSON</button>
+        </div>
+    </div>
+
+    <pre id="{$jsonViewerId}" class="json-viewer">{$escapedJson}</pre>
+
+    <script>
+        $(document).ready(function() {
+            // 高亮JSON语法
+            function highlightJson() {
+                var jsonContent = document.getElementById('{$jsonViewerId}');
+                var jsonText = jsonContent.textContent;
+
+                // 使用简单的正则表达式进行高亮
+                var highlighted = jsonText
+                    // 高亮键
+                    .replace(/"([^"]+)"(?=\s*:)/g, '<span class="json-key">"$1"</span>')
+                    // 高亮字符串值
+                    .replace(/:\s*"([^"]*)"/g, ': <span class="json-string">"$1"</span>')
+                    // 高亮数字
+                    .replace(/:\s*(-?\d+(\.\d+)?)/g, ': <span class="json-number">$1</span>')
+                    // 高亮布尔值和null
+                    .replace(/:\s*(true|false|null)/g, ': <span class="json-boolean">$1</span>');
+
+                jsonContent.innerHTML = highlighted;
+            }
+
+            // 复制JSON按钮
+            document.getElementById('copy-{$jsonViewerId}').addEventListener('click', function() {
+                var jsonContent = document.getElementById('{$jsonViewerId}');
+                var jsonText = jsonContent.textContent;
+
+                var tempTextarea = document.createElement('textarea');
+                tempTextarea.value = jsonText;
+                document.body.appendChild(tempTextarea);
+                tempTextarea.select();
+                document.execCommand('copy');
+                document.body.removeChild(tempTextarea);
+
+                alert('JSON已复制到剪贴板');
+            });
+
+            // 折叠/展开功能
+            document.getElementById('toggle-{$jsonViewerId}').addEventListener('click', function() {
+                var jsonViewer = document.getElementById('{$jsonViewerId}');
+                var isCollapsed = jsonViewer.classList.contains('collapsed');
+
+                if (isCollapsed) {
+                    // 展开
+                    jsonViewer.classList.remove('collapsed');
+                    jsonViewer.style.maxHeight = '80vh';
+                    this.textContent = '折叠';
+                } else {
+                    // 折叠
+                    jsonViewer.classList.add('collapsed');
+                    jsonViewer.style.maxHeight = '200px';
+                    this.textContent = '展开';
+                }
+            });
+
+            // 初始化
+            highlightJson();
+
+            // 搜索功能
+            document.getElementById('search-btn-{$jsonViewerId}').addEventListener('click', function() {
+                var searchText = document.getElementById('search-{$jsonViewerId}').value.trim();
+                if (!searchText) return;
+
+                // 展开JSON查看器
+                var jsonViewer = document.getElementById('{$jsonViewerId}');
+                jsonViewer.classList.remove('collapsed');
+                jsonViewer.style.maxHeight = '80vh';
+                document.getElementById('toggle-{$jsonViewerId}').textContent = '折叠';
 
+                // 移除之前的高亮
+                var content = jsonViewer.innerHTML;
+                content = content.replace(/<mark class="highlight">(.*?)<\/mark>/g, '$1');
+
+                // 高亮搜索文本
+                if (searchText) {
+                    var regex = new RegExp('(' + searchText.replace(/[.*+?^$\{\}()|[\]\\]/g, '\\$&') + ')', 'gi');
+                    content = content.replace(regex, '<mark class="highlight" style="background-color: yellow; padding: 2px;">$1</mark>');
+                }
+
+                jsonViewer.innerHTML = content;
+
+                // 滚动到第一个匹配项
+                var firstHighlight = jsonViewer.querySelector('mark.highlight');
+                if (firstHighlight) {
+                    firstHighlight.scrollIntoView({
+                        behavior: 'smooth',
+                        block: 'center'
+                    });
+                } else {
+                    alert('未找到匹配项');
+                }
+            });
+
+            // 绑定回车键搜索
+            document.getElementById('search-{$jsonViewerId}').addEventListener('keypress', function(e) {
+                if (e.key === 'Enter') {
+                    document.getElementById('search-btn-{$jsonViewerId}').click();
+                }
+            });
+
+            // 默认折叠
+            document.getElementById('toggle-{$jsonViewerId}').textContent = '展开';
+            document.getElementById('{$jsonViewerId}').classList.add('collapsed');
+            document.getElementById('{$jsonViewerId}').style.maxHeight = '200px';
+        });
+    </script>
+</div>
+HTML;
+
+            // 创建卡片
+            $card = new Card($title, $html);
+
+            // 添加原始JSON链接
+            $card->tool('<a href="/json/'.$key.'.json" target="_blank" class="btn btn-sm btn-default">查看原始JSON</a>');
+
+            return $content
+                ->title('配置表查看')
+                ->description($title)
+                ->body($card);
+        } catch (\Exception $e) {
+            // 返回错误响应
+            return $content
+                ->title('错误')
+                ->description('配置表查看')
+                ->body(new Card('错误', '获取配置表数据失败: ' . $e->getMessage()));
+        }
+    }
+
+    /**
+     * 创建JSON查看器
+     *
+     * @param mixed $data 要显示的数据(数组或对象)
+     * @return string
+     */
+    protected function createJsonViewer($data)
+    {
+        // 生成唯一ID,避免多个查看器冲突
+        $viewerId = 'json-viewer-' . uniqid();
+
+        // 确保数据是格式化的JSON字符串
+        $jsonString = is_string($data) ? $data : json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
+
+        // 转义HTML特殊字符
+        $escapedJson = htmlspecialchars($jsonString, ENT_QUOTES, 'UTF-8');
+
+        // 使用简单的方式显示JSON数据
+        $html = <<<HTML
+<div id="{$viewerId}" class="json-viewer">
+    <style>
+        .json-viewer {
+            font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace;
+            font-size: 14px;
+            line-height: 1.5;
+            background-color: #f8f9fa;
+            border-radius: 4px;
+            padding: 15px;
+            overflow: auto;
+            max-height: 80vh;
+        }
+        .json-viewer pre {
+            margin: 0;
+            padding: 0;
+            white-space: pre-wrap;
+            word-wrap: break-word;
+        }
+        /* 工具栏样式 */
+        .json-toolbar {
+            margin-bottom: 10px;
+            display: flex;
+            gap: 10px;
+        }
+        .json-toolbar button {
+            padding: 5px 10px;
+            background-color: #f0f0f0;
+            border: 1px solid #ddd;
+            border-radius: 4px;
+            cursor: pointer;
+        }
+        .json-toolbar button:hover {
+            background-color: #e0e0e0;
+        }
+        /* JSON语法高亮 */
+        .json-key { color: #a52a2a; }
+        .json-string { color: #008000; }
+        .json-number { color: #0000ff; }
+        .json-boolean { color: #b22222; }
+        .json-null { color: #808080; }
+
+        /* 搜索高亮 */
+        .json-highlight {
+            background-color: #ffff00;
+            padding: 2px;
+            border-radius: 2px;
+        }
+
+        /* 折叠/展开控件样式 */
+        .json-toggle {
+            cursor: pointer;
+            user-select: none;
+        }
+        .json-toggle:before {
+            content: "▼";
+            display: inline-block;
+            margin-right: 5px;
+            color: #555;
+            font-size: 10px;
+        }
+        .json-toggle.collapsed:before {
+            content: "►";
+        }
+        .json-collapsed {
+            display: none;
+        }
+        .json-placeholder {
+            color: #777;
+            font-style: italic;
+        }
+    </style>
+
+    <div class="json-toolbar">
+        <input type="text" id="{$viewerId}-search" placeholder="搜索..." style="padding: 5px; margin-right: 10px; width: 200px;">
+        <button id="{$viewerId}-expand-all-btn">展开全部</button>
+        <button id="{$viewerId}-collapse-all-btn">折叠全部</button>
+        <button id="{$viewerId}-copy-json-btn">复制JSON</button>
+    </div>
+
+    <pre id="{$viewerId}-content">{$escapedJson}</pre>
+
+    <script>
+        $(document).ready(function() {
+            // 获取当前查看器的ID
+            var viewerId = '{$viewerId}';
+
+            // 解析JSON并添加折叠功能
+            function processJSON() {
+                try {
+                    // 获取原始JSON文本
+                    var jsonContent = document.getElementById(viewerId + '-content');
+                    var jsonText = jsonContent.textContent;
+                    var jsonObj = JSON.parse(jsonText);
+
+                    // 将JSON对象转换为HTML
+                    var html = formatJSON(jsonObj, 0);
+                    jsonContent.innerHTML = html;
+
+                    // 添加折叠/展开事件处理
+                    $('#' + viewerId + ' .json-toggle').click(function() {
+                        $(this).toggleClass('collapsed');
+                        var target = $(this).next('.json-collapsible');
+                        target.toggleClass('json-collapsed');
+
+                        // 如果折叠,显示占位符
+                        var placeholder = $(this).next().next('.json-placeholder');
+                        if (placeholder.length) {
+                            placeholder.toggle();
+                        }
+                    });
+
+                    // 默认折叠所有嵌套超过1层的对象
+                    collapseLevel(2);
+                } catch (e) {
+                    console.error('JSON解析错误:', e);
+                    // 如果解析失败,回退到简单的语法高亮
+                    simpleHighlight();
+                }
+            }
+
+            // 简单的语法高亮(作为备选方案)
+            function simpleHighlight() {
+                var jsonContent = document.getElementById(viewerId + '-content');
+                var jsonText = jsonContent.textContent;
+
+                // 使用简单的正则表达式进行高亮
+                var highlighted = jsonText
+                    // 高亮键
+                    .replace(/"([^"]+)"(?=\s*:)/g, '<span class="json-key">"$1"</span>')
+                    // 高亮字符串值
+                    .replace(/:\s*"([^"]*)"/g, ': <span class="json-string">"$1"</span>')
+                    // 高亮数字
+                    .replace(/:\s*(-?\d+(\.\d+)?)/g, ': <span class="json-number">$1</span>')
+                    // 高亮布尔值和null
+                    .replace(/:\s*(true|false|null)/g, ': <span class="json-boolean">$1</span>');
+
+                jsonContent.innerHTML = highlighted;
+            }
+
+            // 格式化JSON对象为HTML
+            function formatJSON(obj, level) {
+                var indent = Array(level + 1).join('    '); // 兼容性更好的缩进方法
+                var html = '';
+
+                if (obj === null) {
+                    return '<span class="json-null">null</span>';
+                }
+
+                if (typeof obj === 'boolean') {
+                    return '<span class="json-boolean">' + obj + '</span>';
+                }
+
+                if (typeof obj === 'number') {
+                    return '<span class="json-number">' + obj + '</span>';
+                }
+
+                if (typeof obj === 'string') {
+                    return '<span class="json-string">"' + escapeHTML(obj) + '"</span>';
+                }
+
+                if (Array.isArray(obj)) {
+                    if (obj.length === 0) {
+                        return '[]';
+                    }
+
+                    html += '<span class="json-toggle"></span>[<span class="json-collapsible">';
+
+                    for (var i = 0; i < obj.length; i++) {
+                        html += '\\n' + indent + '    ' + formatJSON(obj[i], level + 1);
+                        if (i < obj.length - 1) {
+                            html += ',';
+                        }
+                    }
+
+                    html += '\\n' + indent + '</span>]<span class="json-placeholder json-collapsed"> [...] </span>';
+                    return html;
+                }
+
+                if (typeof obj === 'object') {
+                    var keys = Object.keys(obj);
+                    if (keys.length === 0) {
+                        return '{}';
+                    }
+
+                    html += '<span class="json-toggle"></span>{<span class="json-collapsible">';
+
+                    for (var i = 0; i < keys.length; i++) {
+                        var key = keys[i];
+                        html += '\\n' + indent + '    <span class="json-key">"' + escapeHTML(key) + '"</span>: ' + formatJSON(obj[key], level + 1);
+                        if (i < keys.length - 1) {
+                            html += ',';
+                        }
+                    }
+
+                    html += '\\n' + indent + '</span>}<span class="json-placeholder json-collapsed"> {...} </span>';
+                    return html;
+                }
+
+                return String(obj);
+            }
+
+            // 转义HTML特殊字符
+            function escapeHTML(str) {
+                return str
+                    .replace(/&/g, '&amp;')
+                    .replace(/</g, '&lt;')
+                    .replace(/>/g, '&gt;')
+                    .replace(/"/g, '&quot;')
+                    .replace(/'/g, '&#039;');
+            }
+
+            // 折叠指定层级以下的所有元素
+            function collapseLevel(level) {
+                $('#' + viewerId + ' .json-toggle').each(function() {
+                    // 计算当前元素的嵌套层级
+                    var currentLevel = $(this).parents('.json-collapsible').length;
+                    if (currentLevel >= level - 1) {
+                        if (!$(this).hasClass('collapsed')) {
+                            $(this).addClass('collapsed');
+                            $(this).next('.json-collapsible').addClass('json-collapsed');
+                            $(this).next().next('.json-placeholder').show();
+                        }
+                    }
+                });
+            }
+
+            // 展开所有元素
+            function expandAll() {
+                $('#' + viewerId + ' .json-toggle').removeClass('collapsed');
+                $('#' + viewerId + ' .json-collapsible').removeClass('json-collapsed');
+                $('#' + viewerId + ' .json-placeholder').hide();
+            }
+
+            // 折叠所有元素
+            function collapseAll() {
+                $('#' + viewerId + ' .json-toggle').addClass('collapsed');
+                $('#' + viewerId + ' .json-collapsible').addClass('json-collapsed');
+                $('#' + viewerId + ' .json-placeholder').show();
+            }
+
+            // 搜索并高亮匹配的文本
+            function searchAndHighlight(searchText) {
+                try {
+                    // 创建正则表达式,忽略大小写
+                    var regex = new RegExp(searchText, 'gi');
+
+                    // 搜索所有文本节点
+                    $('#' + viewerId + ' .json-collapsible').each(function() {
+                        var $this = $(this);
+                        var content = $this.text();
+
+                        if (content.match(regex)) {
+                            // 展开包含匹配文本的节点
+                            var $toggle = $this.prev('.json-toggle');
+                            if ($toggle.hasClass('collapsed')) {
+                                $toggle.removeClass('collapsed');
+                                $this.removeClass('json-collapsed');
+                                $this.next('.json-placeholder').hide();
+                            }
+
+                            // 展开所有父节点
+                            $this.parents('.json-collapsible').each(function() {
+                                var $parentToggle = $(this).prev('.json-toggle');
+                                if ($parentToggle.hasClass('collapsed')) {
+                                    $parentToggle.removeClass('collapsed');
+                                    $(this).removeClass('json-collapsed');
+                                    $(this).next('.json-placeholder').hide();
+                                }
+                            });
+                        }
+                    });
+
+                    // 高亮匹配的文本
+                    $('#' + viewerId + ' .json-key, #' + viewerId + ' .json-string, #' + viewerId + ' .json-number, #' + viewerId + ' .json-boolean, #' + viewerId + ' .json-null').each(function() {
+                        var $this = $(this);
+                        var content = $this.text();
+
+                        if (content.match(regex)) {
+                            var highlightedContent = content.replace(regex, function(match) {
+                                return '<span class="json-highlight">' + match + '</span>';
+                            });
+                            $this.html(highlightedContent);
+                        }
+                    });
+
+                    // 滚动到第一个匹配项
+                    var $firstHighlight = $('#' + viewerId + ' .json-highlight').first();
+                    if ($firstHighlight.length) {
+                        var container = document.getElementById(viewerId);
+                        var highlightOffset = $firstHighlight.offset().top;
+                        var containerOffset = $(container).offset().top;
+                        var scrollTop = highlightOffset - containerOffset - 100;
+
+                        $(container).animate({
+                            scrollTop: scrollTop
+                        }, 300);
+                    }
+                } catch (e) {
+                    console.error('搜索错误:', e);
+                }
+            }
+
+            // 初始化
+            processJSON();
+
+            // 绑定工具栏按钮事件
+            $('#' + viewerId + '-expand-all-btn').click(expandAll);
+            $('#' + viewerId + '-collapse-all-btn').click(collapseAll);
+
+            // 搜索功能
+            $('#' + viewerId + '-search').on('input', function() {
+                var searchText = $(this).val().trim();
+
+                // 移除所有高亮
+                $('#' + viewerId + ' .json-highlight').removeClass('json-highlight');
+
+                if (searchText.length > 0) {
+                    // 搜索并高亮匹配的文本
+                    searchAndHighlight(searchText);
+                }
+            });
+
+            // 复制JSON按钮
+            $('#' + viewerId + '-copy-json-btn').click(function() {
+                var jsonContent = document.getElementById(viewerId + '-content');
+                var jsonText = jsonContent.textContent || jsonContent.innerText;
+
+                // 创建一个临时元素来存储纯文本JSON
+                var tempTextarea = $('<textarea>');
+                $('body').append(tempTextarea);
+
+                // 尝试解析和格式化JSON
+                try {
+                    var jsonObj = JSON.parse(jsonText.replace(/[\u0000-\u001F]+/g, ' '));
+                    tempTextarea.val(JSON.stringify(jsonObj, null, 4));
+                } catch (e) {
+                    // 如果解析失败,使用原始文本
+                    tempTextarea.val(jsonText);
+                }
+
+                tempTextarea.select();
+                document.execCommand('copy');
+                tempTextarea.remove();
+
+                alert('JSON已复制到剪贴板');
+            });
+        });
+    </script>
+</div>
+HTML;
+
+        return $html;
+    }
 }

+ 53 - 0
app/Module/Game/DCache/DismantleJsonConfig.php

@@ -0,0 +1,53 @@
+<?php
+
+namespace App\Module\Game\DCache;
+
+use App\Module\GameItems\Commands\GenerateDismantleJsonCommand;
+use App\Module\LCache\DQueueJob;
+
+/**
+ * 物品分解配方配置表缓存
+ */
+class DismantleJsonConfig extends DQueueJob
+{
+    /**
+     * 获取新数据
+     * 
+     * @param array $parameter 参数
+     * @return mixed
+     */
+    static public function getNewData(array $parameter = [])
+    {
+        return GenerateDismantleJsonCommand::generateJson();
+    }
+
+    /**
+     * 获取缓存时间(秒)
+     * 
+     * @return int
+     */
+    static public function getTtl(): int
+    {
+        return 3600; // 1小时
+    }
+
+    /**
+     * 获取防重复执行时间(秒)
+     * 
+     * @return int
+     */
+    static public function getPreventDuplication(): int
+    {
+        return 600; // 10分钟
+    }
+
+    /**
+     * 获取必需参数索引
+     * 
+     * @return array
+     */
+    static public function getRequiredArgIndex(): array
+    {
+        return [];
+    }
+}

+ 53 - 0
app/Module/Game/DCache/RecipeJsonConfig.php

@@ -0,0 +1,53 @@
+<?php
+
+namespace App\Module\Game\DCache;
+
+use App\Module\GameItems\Commands\GenerateRecipeJsonCommand;
+use App\Module\LCache\DQueueJob;
+
+/**
+ * 物品合成配方配置表缓存
+ */
+class RecipeJsonConfig extends DQueueJob
+{
+    /**
+     * 获取新数据
+     * 
+     * @param array $parameter 参数
+     * @return mixed
+     */
+    static public function getNewData(array $parameter = [])
+    {
+        return GenerateRecipeJsonCommand::generateJson();
+    }
+
+    /**
+     * 获取缓存时间(秒)
+     * 
+     * @return int
+     */
+    static public function getTtl(): int
+    {
+        return 3600; // 1小时
+    }
+
+    /**
+     * 获取防重复执行时间(秒)
+     * 
+     * @return int
+     */
+    static public function getPreventDuplication(): int
+    {
+        return 600; // 10分钟
+    }
+
+    /**
+     * 获取必需参数索引
+     * 
+     * @return array
+     */
+    static public function getRequiredArgIndex(): array
+    {
+        return [];
+    }
+}

+ 129 - 0
app/Module/GameItems/AdminControllers/Tools/SyncDismantleJsonTool.php

@@ -0,0 +1,129 @@
+<?php
+
+namespace App\Module\GameItems\AdminControllers\Tools;
+
+use App\Module\Game\DCache\DismantleJsonConfig;
+use App\Module\GameItems\Models\ItemDismantleRule;
+use Dcat\Admin\Grid\Tools\AbstractTool;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Log;
+
+/**
+ * 物品分解配方配置表同步工具
+ * 
+ * 用于在后台管理界面中同步物品分解配方配置表数据到JSON文件
+ */
+class SyncDismantleJsonTool extends AbstractTool
+{
+    /**
+     * 是否显示按钮
+     * 
+     * @var bool
+     */
+    protected $shouldDisplay;
+    
+    /**
+     * 按钮样式
+     * 
+     * @var string
+     */
+    protected $style = 'btn btn-primary waves-effect';
+
+    /**
+     * 构造函数
+     * 
+     * @param bool $shouldDisplay 是否显示按钮
+     */
+    public function __construct(bool $shouldDisplay = true)
+    {
+        $this->shouldDisplay = $shouldDisplay;
+    }
+
+    /**
+     * 按钮标题
+     * 
+     * @return string
+     */
+    public function title()
+    {
+        return '生成JSON';
+    }
+
+    /**
+     * 确认提示
+     * 
+     * @return string
+     */
+    public function confirm()
+    {
+        return '确定要生成物品分解配方配置JSON数据吗?';
+    }
+
+    /**
+     * 处理请求
+     * 
+     * @param Request $request
+     * @return mixed
+     */
+    public function handle(Request $request)
+    {
+        try {
+            // 直接调用命令生成JSON
+            $process = new \Symfony\Component\Process\Process(['php', 'artisan', 'gameitems:generate-dismantle-json']);
+            $process->setWorkingDirectory(base_path());
+            $process->run();
+
+            if (!$process->isSuccessful()) {
+                Log::error('Generate dismantle.json failed: ' . $process->getErrorOutput());
+                return $this->response()->error('生成失败:' . $process->getErrorOutput());
+            }
+
+            // 强制刷新缓存
+            DismantleJsonConfig::getData([], true);
+
+            return $this->response()->success('生成成功')->refresh();
+        } catch (\Exception $e) {
+            Log::error('Generate dismantle.json exception: '.$e->getMessage());
+            return $this->response()->error('生成失败:'.$e->getMessage());
+        }
+    }
+
+    /**
+     * 渲染按钮
+     * 
+     * @return string
+     */
+    public function render()
+    {
+        if (!$this->shouldDisplay) {
+            return '';
+        }
+
+        return parent::render();
+    }
+
+    /**
+     * 判断是否应该显示按钮
+     * 
+     * @return bool
+     */
+    public static function shouldDisplay(): bool
+    {
+        // 获取缓存数据
+        $json = DismantleJsonConfig::getData();
+        
+        // 如果没有生成时间戳,说明需要生成
+        if (!isset($json['generated_ts'])) {
+            return true;
+        }
+        
+        // 获取生成时间和最后更新时间
+        $generatedAt = \Carbon\Carbon::createFromTimestamp($json['generated_ts']);
+        
+        // 获取分解规则的最后更新时间
+        $lastUpdated = \Carbon\Carbon::parse(ItemDismantleRule::max('updated_at') ?: '2000-01-01');
+        
+        // 如果生成时间早于最后更新时间,说明需要重新生成
+        return $generatedAt->lt($lastUpdated);
+    }
+}

+ 129 - 0
app/Module/GameItems/AdminControllers/Tools/SyncRecipeJsonTool.php

@@ -0,0 +1,129 @@
+<?php
+
+namespace App\Module\GameItems\AdminControllers\Tools;
+
+use App\Module\Game\DCache\RecipeJsonConfig;
+use App\Module\GameItems\Models\ItemRecipe;
+use Dcat\Admin\Grid\Tools\AbstractTool;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Log;
+
+/**
+ * 物品合成配方配置表同步工具
+ * 
+ * 用于在后台管理界面中同步物品合成配方配置表数据到JSON文件
+ */
+class SyncRecipeJsonTool extends AbstractTool
+{
+    /**
+     * 是否显示按钮
+     * 
+     * @var bool
+     */
+    protected $shouldDisplay;
+    
+    /**
+     * 按钮样式
+     * 
+     * @var string
+     */
+    protected $style = 'btn btn-primary waves-effect';
+
+    /**
+     * 构造函数
+     * 
+     * @param bool $shouldDisplay 是否显示按钮
+     */
+    public function __construct(bool $shouldDisplay = true)
+    {
+        $this->shouldDisplay = $shouldDisplay;
+    }
+
+    /**
+     * 按钮标题
+     * 
+     * @return string
+     */
+    public function title()
+    {
+        return '生成JSON';
+    }
+
+    /**
+     * 确认提示
+     * 
+     * @return string
+     */
+    public function confirm()
+    {
+        return '确定要生成物品合成配方配置JSON数据吗?';
+    }
+
+    /**
+     * 处理请求
+     * 
+     * @param Request $request
+     * @return mixed
+     */
+    public function handle(Request $request)
+    {
+        try {
+            // 直接调用命令生成JSON
+            $process = new \Symfony\Component\Process\Process(['php', 'artisan', 'gameitems:generate-recipe-json']);
+            $process->setWorkingDirectory(base_path());
+            $process->run();
+
+            if (!$process->isSuccessful()) {
+                Log::error('Generate recipe.json failed: ' . $process->getErrorOutput());
+                return $this->response()->error('生成失败:' . $process->getErrorOutput());
+            }
+
+            // 强制刷新缓存
+            RecipeJsonConfig::getData([], true);
+
+            return $this->response()->success('生成成功')->refresh();
+        } catch (\Exception $e) {
+            Log::error('Generate recipe.json exception: '.$e->getMessage());
+            return $this->response()->error('生成失败:'.$e->getMessage());
+        }
+    }
+
+    /**
+     * 渲染按钮
+     * 
+     * @return string
+     */
+    public function render()
+    {
+        if (!$this->shouldDisplay) {
+            return '';
+        }
+
+        return parent::render();
+    }
+
+    /**
+     * 判断是否应该显示按钮
+     * 
+     * @return bool
+     */
+    public static function shouldDisplay(): bool
+    {
+        // 获取缓存数据
+        $json = RecipeJsonConfig::getData();
+        
+        // 如果没有生成时间戳,说明需要生成
+        if (!isset($json['generated_ts'])) {
+            return true;
+        }
+        
+        // 获取生成时间和最后更新时间
+        $generatedAt = \Carbon\Carbon::createFromTimestamp($json['generated_ts']);
+        
+        // 获取合成配方的最后更新时间
+        $lastUpdated = \Carbon\Carbon::parse(ItemRecipe::max('updated_at') ?: '2000-01-01');
+        
+        // 如果生成时间早于最后更新时间,说明需要重新生成
+        return $generatedAt->lt($lastUpdated);
+    }
+}

+ 144 - 0
app/Module/GameItems/Commands/GenerateDismantleJsonCommand.php

@@ -0,0 +1,144 @@
+<?php
+
+namespace App\Module\GameItems\Commands;
+
+use App\Module\Game\DCache\DismantleJsonConfig;
+use Illuminate\Console\Command;
+use App\Module\GameItems\Models\ItemDismantleRule;
+use App\Module\GameItems\Models\ItemDismantleResult;
+use Illuminate\Support\Facades\File;
+use Illuminate\Support\Facades\Log;
+
+/**
+ * 生成物品分解配方配置表JSON数据命令
+ *
+ * 该命令用于从数据库中的物品分解规则表生成JSON数据文件,供客户端使用。
+ * 生成的JSON文件包含分解规则的基本信息,如规则ID、适用物品、分解结果等。
+ * 该命令通常在分解规则数据更新后运行,以确保客户端获取最新的分解规则数据。
+ */
+class GenerateDismantleJsonCommand extends Command
+{
+    /**
+     * 命令名称和签名
+     *
+     * @var string
+     */
+    protected $signature = 'gameitems:generate-dismantle-json';
+
+    /**
+     * 命令描述
+     *
+     * @var string
+     */
+    protected $description = 'Generate dismantle.json from ItemDismantleRule table';
+
+    /**
+     * 生成分解规则JSON数据
+     *
+     * @return array|bool 生成的数据或失败标志
+     */
+    public static function generateJson()
+    {
+        try {
+            // 查询ItemDismantleRule表中的数据,并预加载关联数据
+            $rules = ItemDismantleRule::query()
+                ->with(['item', 'category', 'results.resultItem'])
+                ->where('is_active', 1)
+                ->get()
+                ->map(function ($rule) {
+                    // 处理分解结果数据
+                    $results = $rule->results->map(function ($result) {
+                        return [
+                            'result_item_id' => $result->result_item_id,
+                            'result_item_name' => $result->resultItem->name ?? '未知物品',
+                            'min_quantity' => $result->min_quantity,
+                            'max_quantity' => $result->max_quantity,
+                            'base_chance' => $result->base_chance,
+                            'rarity_factor' => $result->rarity_factor,
+                            'quality_factor' => $result->quality_factor,
+                        ];
+                    })->toArray();
+
+                    // 构建规则数据
+                    $ruleData = [
+                        'id' => $rule->id,
+                        'priority' => $rule->priority,
+                        'min_rarity' => $rule->min_rarity,
+                        'max_rarity' => $rule->max_rarity,
+                        'results' => $results,
+                    ];
+
+                    // 根据规则类型添加不同的字段
+                    if ($rule->item_id) {
+                        $ruleData['rule_type'] = 'item';
+                        $ruleData['item_id'] = $rule->item_id;
+                        $ruleData['item_name'] = $rule->item->name ?? '未知物品';
+                    } else {
+                        $ruleData['rule_type'] = 'category';
+                        $ruleData['category_id'] = $rule->category_id;
+                        $ruleData['category_name'] = $rule->category->name ?? '未知分类';
+                    }
+
+                    return $ruleData;
+                })
+                ->toArray();
+
+            // 准备完整数据,包含生成时间
+            $data = [
+                'generated_ts' => time(),
+                'dismantle_rules' => $rules
+            ];
+
+            // 保存到文件
+            self::saveJsonToFile($data);
+
+            return $data;
+        } catch (\Exception $e) {
+            Log::error('Generate dismantle.json failed: ' . $e->getMessage());
+            return false;
+        }
+    }
+
+    /**
+     * 将JSON数据保存到文件
+     *
+     * @param array $data 要保存的数据
+     * @return bool 是否保存成功
+     */
+    protected static function saveJsonToFile(array $data): bool
+    {
+        try {
+            // 确保目录存在
+            $directory = 'public/json';
+            if (!File::exists($directory)) {
+                File::makeDirectory($directory, 0755, true);
+            }
+
+            // 将数据保存为JSON文件
+            $jsonContent = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
+            $filePath = $directory . '/dismantle.json';
+            File::put($filePath, $jsonContent);
+
+            Log::info('Dismantle JSON file saved to: ' . $filePath);
+            return true;
+        } catch (\Exception $e) {
+            Log::error('Save dismantle.json to file failed: ' . $e->getMessage());
+            return false;
+        }
+    }
+
+    /**
+     * 执行命令
+     */
+    public function handle()
+    {
+        $this->info('Generating dismantle JSON data...');
+        
+        if (DismantleJsonConfig::getData([], true)) {
+            $this->info('Successfully generated dismantle.json with timestamp');
+            $this->info('JSON file saved to public/json/dismantle.json');
+        } else {
+            $this->error('Failed to generate dismantle.json');
+        }
+    }
+}

+ 137 - 0
app/Module/GameItems/Commands/GenerateRecipeJsonCommand.php

@@ -0,0 +1,137 @@
+<?php
+
+namespace App\Module\GameItems\Commands;
+
+use App\Module\Game\DCache\RecipeJsonConfig;
+use Illuminate\Console\Command;
+use App\Module\GameItems\Models\ItemRecipe;
+use App\Module\GameItems\Models\ItemRecipeMaterial;
+use Illuminate\Support\Facades\File;
+use Illuminate\Support\Facades\Log;
+
+/**
+ * 生成物品合成配方配置表JSON数据命令
+ *
+ * 该命令用于从数据库中的物品合成配方表生成JSON数据文件,供客户端使用。
+ * 生成的JSON文件包含合成配方的基本信息,如ID、名称、产出物品、所需材料等。
+ * 该命令通常在合成配方数据更新后运行,以确保客户端获取最新的配方数据。
+ */
+class GenerateRecipeJsonCommand extends Command
+{
+    /**
+     * 命令名称和签名
+     *
+     * @var string
+     */
+    protected $signature = 'gameitems:generate-recipe-json';
+
+    /**
+     * 命令描述
+     *
+     * @var string
+     */
+    protected $description = 'Generate recipe.json from ItemRecipe table';
+
+    /**
+     * 生成合成配方JSON数据
+     *
+     * @return array|bool 生成的数据或失败标志
+     */
+    public static function generateJson()
+    {
+        try {
+            // 查询ItemRecipe表中的数据,并预加载关联数据
+            $recipes = ItemRecipe::query()
+                ->with(['resultItem', 'materials.item'])
+                ->where('is_active', 1)
+                ->get()
+                ->map(function ($recipe) {
+                    // 处理材料数据
+                    $materials = $recipe->materials->map(function ($material) {
+                        return [
+                            'item_id' => $material->item_id,
+                            'item_name' => $material->item->name ?? '未知物品',
+                            'quantity' => $material->quantity,
+                            'is_consumed' => $material->is_consumed,
+                        ];
+                    })->toArray();
+
+                    // 构建配方数据
+                    return [
+                        'id' => $recipe->id,
+                        'name' => $recipe->name,
+                        'result_item_id' => $recipe->result_item_id,
+                        'result_item_name' => $recipe->resultItem->name ?? '未知物品',
+                        'result_min_quantity' => $recipe->result_min_quantity,
+                        'result_max_quantity' => $recipe->result_max_quantity,
+                        'success_rate' => $recipe->success_rate,
+                        'coin_cost' => $recipe->coin_cost,
+                        'level_required' => $recipe->level_required,
+                        'is_default_unlocked' => $recipe->is_default_unlocked,
+                        'unlock_condition' => $recipe->unlock_condition,
+                        'cooldown_seconds' => $recipe->cooldown_seconds,
+                        'category_id' => $recipe->category_id,
+                        'materials' => $materials,
+                    ];
+                })
+                ->toArray();
+
+            // 准备完整数据,包含生成时间
+            $data = [
+                'generated_ts' => time(),
+                'recipes' => $recipes
+            ];
+
+            // 保存到文件
+            self::saveJsonToFile($data);
+
+            return $data;
+        } catch (\Exception $e) {
+            Log::error('Generate recipe.json failed: ' . $e->getMessage());
+            return false;
+        }
+    }
+
+    /**
+     * 将JSON数据保存到文件
+     *
+     * @param array $data 要保存的数据
+     * @return bool 是否保存成功
+     */
+    protected static function saveJsonToFile(array $data): bool
+    {
+        try {
+            // 确保目录存在
+            $directory = 'public/json';
+            if (!File::exists($directory)) {
+                File::makeDirectory($directory, 0755, true);
+            }
+
+            // 将数据保存为JSON文件
+            $jsonContent = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
+            $filePath = $directory . '/recipe.json';
+            File::put($filePath, $jsonContent);
+
+            Log::info('Recipe JSON file saved to: ' . $filePath);
+            return true;
+        } catch (\Exception $e) {
+            Log::error('Save recipe.json to file failed: ' . $e->getMessage());
+            return false;
+        }
+    }
+
+    /**
+     * 执行命令
+     */
+    public function handle()
+    {
+        $this->info('Generating recipe JSON data...');
+        
+        if (RecipeJsonConfig::getData([], true)) {
+            $this->info('Successfully generated recipe.json with timestamp');
+            $this->info('JSON file saved to public/json/recipe.json');
+        } else {
+            $this->error('Failed to generate recipe.json');
+        }
+    }
+}

+ 5 - 0
noai.md

@@ -76,3 +76,8 @@ app/Module下 Repository 类,应该位于各模块的 Repositorys 目录,找出
 在 UCore/Commands 下写一个命令,生成app目录的文件列表,到 app/tree.md 
 
 后台Helper 是用来处理`可复用`逻辑的,检查已有的后台Helper,移除没有`复用`价值的逻辑,编写有`复用`价值的Helper
+
+php artisan tinker --execute="App\Module\System\Models\AdminMenu::all();"
+
+物品的合成配方配置表
+物品的分解配方配置表