<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>应用管理</title>
    <link rel="stylesheet" href="{{ asset('CmsProUi/component/pear/css/pear.css') }}">
    <link rel="stylesheet" href="{{ asset('CmsProUi/font-awesome/4.7.0/css/font-awesome.min.css') }}">
    <link rel="stylesheet" href="{{ asset('Admin/css/admin.css') }}">
    <link rel="stylesheet" href="{{ asset('Admin/css/variables.css') }}">
    <link rel="stylesheet" href="{{ asset('Admin/css/reset.css') }}">
    <style>
    .layui-layer-loading .layui-layer-content { white-space: nowrap; }
    .layui-btn+.layui-btn { margin-left: 0px; }
    .app-card-wrap { display: grid; grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); gap: 16px; padding: 10px 0; }
    .app-card { background: #fff; border-radius: 8px; border: 1px solid #e6e6e6; padding: 16px 10px; transition: all 0.2s; position: relative; display: flex; flex-direction: column; gap: 10px; height:100%; box-sizing:border-box; }
    .app-card:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
    .app-card-top { display:flex; gap:14px; align-items:flex-start; }
    .app-card-icon { width: 56px; height:56px; border-radius: 14px; display:flex; align-items:center; justify-content:center; font-size:24px; color:#fff; flex-shrink:0; background:#f0f0f0; overflow:hidden; }
    .app-card-icon img { width:100%;height:100%;object-fit:cover;border-radius:14px; }
    .app-card-body { flex:1; min-width:0; }
    .app-card-head { display:flex;flex-direction:column;align-items:flex-start;gap:4px;margin-bottom:6px; }
    .app-card-head-row { display:flex;align-items:center;gap:8px;flex-wrap:wrap; }
    .app-card-name { font-size:16px;font-weight:600;color:#333; }
    .app-card-backup-btn { margin-left:auto;font-size:12px;padding:2px 8px;border-radius:4px;cursor:pointer;background:#e3f2fd;color:#1976d2;border:1px solid #bbdefb;transition:all 0.2s; }
    .app-card-backup-btn:hover { background:#1976d2;color:#fff; }
    .app-card-version { background:#e8f5e9;color:#4caf50;padding:2px 8px;border-radius:4px;font-size:12px;font-weight:500; }
    .app-card-desc { font-size:13px;color:#999;line-height:1.7;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden; margin-bottom:4px; }
    .app-card-badge { display:inline-block;padding:2px 8px;border-radius:10px;font-size:12px; }
    .app-card-badge.enabled { background:#e8f5e9;color:#4caf50; }
    .app-card-badge.installed { background:#fff3e0;color:#ff9800; }
    .app-card-badge.disabled { background:#fce4ec;color:#f44336; }
    .app-card-system { position:absolute;top:10px;right:10px;background:#e3f2fd;color:#1976d2;padding:2px 6px;border-radius:3px;font-size:11px; }
    .app-card-actions { display:flex;gap:6px;flex-wrap:nowrap; margin-top:auto; }
    .app-stats { display: flex; gap: 20px; margin-bottom: 20px; padding: 15px 20px; background: #fff; border-radius: 8px; border: 1px solid #e6e6e6; }
    .app-stats-item { font-size: 13px; color: #666; }
    .app-stats-item span { font-weight: 600; color: #333; font-size: 18px; margin-right: 4px; }
    .app-empty { text-align: center; padding: 60px 0; color: #999; }
    .app-empty i { font-size: 48px; margin-bottom: 12px; display: block; }
    .app-upload-zone { border: 2px dashed #d9d9d9; border-radius: 8px; padding: 30px 20px; text-align: center; transition: all 0.3s; cursor: pointer; background: #fafafa; }
    .app-upload-zone:hover, .app-upload-zone.dragover { border-color: #1e9fff; background: #f0f8ff; }
    .app-upload-zone.dragover .app-upload-icon { color: #1e9fff; transform: scale(1.1); }
    .app-upload-icon { font-size: 40px; color: #c0c4cc; margin-bottom: 10px; transition: all 0.3s; }
    .app-upload-text { font-size: 14px; color: #333; margin-bottom: 6px; }
    .app-upload-or { font-size: 12px; color: #999; margin: 8px 0; }
    .app-upload-hint { font-size: 12px; color: #c0c4cc; margin-top: 10px; }
    .app-upload-file { margin-top: 12px; }
    .app-upload-file-info { display: flex; align-items: center; gap: 12px; padding: 12px 15px; background: #f6f8fa; border-radius: 6px; border: 1px solid #e6e6e6; }
    .app-upload-file-detail { flex: 1; }
    .app-upload-file-name { font-size: 13px; color: #333; font-weight: 500; word-break: break-all; }
    .app-upload-file-size { font-size: 12px; color: #999; margin-top: 2px; }
    .app-upload-file-remove { background: none; border: none; color: #999; cursor: pointer; font-size: 16px; padding: 4px 8px; border-radius: 4px; transition: all 0.2s; }
    .app-upload-file-remove:hover { color: #ff5722; background: #fff0f0; }
    .app-tabs { display: flex; gap: 0; margin-bottom: 20px; border-bottom: 2px solid #e6e6e6; }
    .app-tab { padding: 10px 24px; font-size: 14px; color: #666; cursor: pointer; border: none; background: none; position: relative; transition: all 0.2s; font-weight: 500; }
    .app-tab:hover { color: #1e9fff; }
    .app-tab.active { color: #1e9fff; }
    .app-tab.active::after { content: ''; position: absolute; bottom: -2px; left: 0; right: 0; height: 2px; background: #1e9fff; }
    </style>
    @include('admin.partials.permission-script')
</head>
<body>
<div class="pear-container">
    <div class="layui-card">
        <div class="layui-card-body">
            <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:-20px;">
                <div class="app-stats" id="appStats">
                    <div class="app-stats-item"><span id="totalCount">0</span>个应用</div>
                    <div class="app-stats-item"><span id="installedCount">0</span>已安装</div>
                    <div class="app-stats-item"><span id="uninstalledCount">0</span>未安装</div>
                    <div class="app-stats-item"><span id="enabledCount">0</span>已启用</div>
                    <div class="app-stats-item"><span id="disabledCount">0</span>已禁用</div>
                </div>
                <div>
                    <button class="layui-btn layui-btn-sm" id="btnUpload"><i class="fa fa-upload"></i> 上传安装</button>
                    <button class="layui-btn layui-btn-sm layui-btn-normal" id="btnCheckUpdates"><i class="fa fa-refresh"></i> 检查更新</button>
                </div>
            </div>
            <div class="app-tabs" id="appTabs">
                <button class="app-tab active" data-tab="installed">已安装</button>
                <button class="app-tab" data-tab="uninstalled">未安装</button>
            </div>
            <div class="app-card-wrap" id="appList"></div>
            <div class="app-empty" id="appEmpty" style="display:none;">
                <i class="fa fa-puzzle-piece"></i>
                <p id="emptyText">暂无已安装的应用</p>
                <p style="font-size:12px;" id="emptyHint">点击"上传安装"或前往"应用市场"安装应用</p>
            </div>
        </div>
    </div>
</div>



<script src="{{ asset('CmsProUi/component/layui/layui.js') }}"></script>
<script src="{{ asset('CmsProUi/component/pear/pear.js') }}"></script>
<script>
layui.use(['jquery', 'layer', 'form'], function(){
    var $ = layui.jquery,
        layer = layui.layer,
        form = layui.form;

    $.ajaxSetup({
        headers: {
            'X-CSRF-TOKEN': '{{ csrf_token() }}'
        },
        statusCode: {
            401: function() {
                layer.msg('登录已过期，请重新登录', {icon: 2, time: 1500}, function() {
                    window.location.href = '/admin/login?redirect=' + encodeURIComponent(window.location.pathname + window.location.search);
                });
            }
        }
    });

    function formatSize(bytes) {
        if (bytes < 1024) return bytes + ' B';
        if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
        return (bytes / 1048576).toFixed(1) + ' MB';
    }

    function resolveIconUrl(url) {
        if (!url) return '';
        if (/^https?:\/\//i.test(url)) return url;
        return '//v5.cmspro.cn' + (url.charAt(0) === '/' ? url : '/' + url);
    }

    function renderAppIcon(icon, iconUrl) {
        if (iconUrl) return '<img src="'+iconUrl+'" onerror="this.style.display=\'none\';this.nextElementSibling||(this.parentNode.innerHTML+=\'<i class=\\\'fa fa-puzzle-piece\\\'></i>\');">';
        if (!icon) return '<i class="fa fa-puzzle-piece"></i>';
        if (/^(fa |layui-icon)/.test(icon)) return '<i class="' + icon + '"></i>';
        return '<img src="'+resolveIconUrl(icon)+'" onerror="this.style.display=\'none\';this.nextElementSibling||(this.parentNode.innerHTML+=\'<i class=\\\'fa fa-puzzle-piece\\\'></i>\');">';
    }

    function openUploadDialog(title, submitUrl, onSuccess, extraData) {
        var fileInput = $('<input type="file" accept=".zip" style="position:absolute;top:0;left:0;width:0;height:0;opacity:0;">');
        $('body').append(fileInput);

        var contentHtml = '<div style="padding:20px;">'
            + '<div class="app-upload-zone" id="dlgUploadZone">'
            + '<div class="app-upload-icon"><i class="fa fa-cloud-upload"></i></div>'
            + '<div class="app-upload-text">拖拽 .zip 文件到此处</div>'
            + '<div class="app-upload-or">或</div>'
            + '<button type="button" class="layui-btn layui-btn-sm layui-btn-normal" id="dlgBtnSelectFile"><i class="fa fa-folder-open"></i> 选择文件</button>'
            + '<div class="app-upload-hint">仅支持 .zip 格式的应用包</div>'
            + '</div>'
            + '<div class="app-upload-file" id="dlgUploadFileInfo" style="display:none;">'
            + '<div class="app-upload-file-info">'
            + '<i class="fa fa-file-archive-o" style="font-size:28px;color:#ff9800;"></i>'
            + '<div class="app-upload-file-detail">'
            + '<div class="app-upload-file-name" id="dlgUploadFileName"></div>'
            + '<div class="app-upload-file-size" id="dlgUploadFileSize"></div>'
            + '</div>'
            + '<button type="button" class="app-upload-file-remove" id="dlgBtnRemoveFile"><i class="fa fa-times"></i></button>'
            + '</div></div>'
            + '<div style="text-align:center;margin-top:15px;">'
            + '<button type="button" class="layui-btn layui-btn-disabled" id="dlgBtnSubmit" disabled><i class="fa fa-download"></i> ' + (title || '开始安装') + '</button>'
            + '</div></div>';

        layer.open({
            type: 1,
            title: title || '上传安装应用',
            area: ['480px', '400px'],
            content: contentHtml,
            end: function() {
                fileInput.remove();
            },
            success: function(layero) {
                var selectedFile = null;
                var uploadZone = layero.find('#dlgUploadZone');
                var fileInfo = layero.find('#dlgUploadFileInfo');
                var btnSubmit = layero.find('#dlgBtnSubmit');

                function setFile(file) {
                    if (!file || !file.name.endsWith('.zip')) {
                        layer.msg('请选择 .zip 格式的应用包', {icon:2});
                        return;
                    }
                    selectedFile = file;
                    layero.find('#dlgUploadFileName').text(file.name);
                    layero.find('#dlgUploadFileSize').text(formatSize(file.size));
                    uploadZone.hide();
                    fileInfo.show();
                    btnSubmit.prop('disabled', false).removeClass('layui-btn-disabled');
                }

                function clearFile() {
                    selectedFile = null;
                    fileInput.val('');
                    uploadZone.show();
                    fileInfo.hide();
                    btnSubmit.prop('disabled', true).addClass('layui-btn-disabled');
                }

                layero.find('#dlgBtnSelectFile').on('click', function(e) {
                    e.stopPropagation();
                    fileInput.trigger('click');
                });

                uploadZone.on('click', function() {
                    fileInput.trigger('click');
                });

                fileInput.on('change', function() {
                    if (this.files && this.files.length) setFile(this.files[0]);
                });

                uploadZone.on('dragover', function(e) {
                    e.preventDefault();
                    e.stopPropagation();
                    $(this).addClass('dragover');
                }).on('dragleave drop', function(e) {
                    e.preventDefault();
                    e.stopPropagation();
                    $(this).removeClass('dragover');
                }).on('drop', function(e) {
                    var files = e.originalEvent.dataTransfer.files;
                    if (files.length) setFile(files[0]);
                });

                layero.find('#dlgBtnRemoveFile').on('click', function() {
                    clearFile();
                });

                btnSubmit.on('click', function() {
                    if (!selectedFile) return;
                    var formData = new FormData();
                    formData.append('package', selectedFile);
                    if (extraData) {
                        for (var key in extraData) {
                            if (extraData.hasOwnProperty(key)) {
                                formData.append(key, extraData[key]);
                            }
                        }
                    }
                    var loadIdx = layer.load(2, {content:'正在处理，请耐心等待...', time:0});
                    $.ajax({
                        url: submitUrl,
                        type: 'POST',
                        data: formData,
                        processData: false,
                        contentType: false,
                        timeout: 600000,
                        dataType: 'json',
                        success: function(res) {
                            layer.close(loadIdx);
                            if (res.code === 0) {
                                layer.msg(onSuccess ? onSuccess(res) : '操作成功', {icon:1, time:1500}, function() {
                                    layer.closeAll();
                                    loadApps();
                                    if (top.PearAdmin && top.PearAdmin.refreshMenu) top.PearAdmin.refreshMenu();
                                });
                            } else {
                                layer.msg(res.message, {icon:2, time:3000});
                            }
                        },
                        error: function(xhr) {
                            layer.close(loadIdx);
                            if (xhr.status === 401) return;
                            // 尝试解析响应体，后端可能已成功但连接异常断开
                            try {
                                var res = JSON.parse(xhr.responseText);
                                if (res && res.code === 0) {
                                    layer.msg(onSuccess ? onSuccess(res) : '操作成功', {icon:1, time:1500}, function() {
                                        layer.closeAll();
                                        loadApps();
                                        if (top.PearAdmin && top.PearAdmin.refreshMenu) top.PearAdmin.refreshMenu();
                                    });
                                    return;
                                }
                                if (res && res.message) {
                                    layer.msg(res.message, {icon:2, time:3000});
                                    return;
                                }
                            } catch(e) {}
                            layer.msg('操作失败，请稍后重试 (HTTP ' + (xhr.status || 'unknown') + ')', {icon:2, time:3000});
                        }
                    });
                });
            }
        });
    }

    var currentTab = 'installed';
    var allAvailableApps = null;

    function loadApps() {
        var installedDefer = $.get('/api/admin/apps');
        var availableDefer = $.get('/api/admin/apps/available');

        $.when(installedDefer, availableDefer).done(function(installedRes, availableRes) {
            var apps = (installedRes[0] && installedRes[0].code === 0) ? (installedRes[0].data || []) : [];
            var availableData = (availableRes[0] && availableRes[0].code === 0) ? (availableRes[0].data || {}) : {};

            var installedCount = apps.length;
            var enabled = 0, disabled = 0;
            apps.forEach(function(app) {
                if (app.status === 1) enabled++;
                else disabled++;
            });

            var uninstalledCount = availableData.uninstalled || 0;
            var totalCount = installedCount + uninstalledCount;

            $('#totalCount').text(totalCount);
            $('#installedCount').text(installedCount);
            $('#uninstalledCount').text(uninstalledCount);
            $('#enabledCount').text(enabled);
            $('#disabledCount').text(disabled);

            allAvailableApps = availableData;

            // 保存当前版本映射，供升级对话框显示
            window.currentAppVersions = {};
            apps.forEach(function(app) {
                window.currentAppVersions[app.app_id] = app.version;
            });

            if (currentTab === 'installed') {
                renderInstalledApps(apps);
            } else {
                renderUninstalledApps(allAvailableApps.uninstalled_apps || []);
            }

            // 页面加载完成后静默触发懒检查（24h 一次）
            triggerLazyUpdateCheck();
        });
    }

    // 静默触发懒检查（前端 localStorage 24h 节流，后端 Cache 24h 节流）
    function triggerLazyUpdateCheck() {
        var lastCheck = localStorage.getItem('cmspro_app_update_check_apps_page');
        var now = Date.now();
        if (lastCheck && (now - parseInt(lastCheck)) < 24 * 3600 * 1000) {
            return;
        }
        localStorage.setItem('cmspro_app_update_check_apps_page', now.toString());

        $.post('/api/admin/apps/check-updates?lazy=1', function(res) {
            if (res.code === 0 && res.data && res.data.length > 0) {
                // 有更新，重新加载应用列表以显示徽标
                loadApps();
                layer.msg('检测到 ' + res.data.length + ' 个应用有可用更新', {icon: 0, time: 3000});
            }
        }).fail(function() {
            // 静默失败，不打扰用户
        });
    }

    function renderInstalledApps(apps) {
        var enabled = 0, disabled = 0;

        if (apps.length === 0) {
            $('#appList').html('');
            $('#appEmpty').show();
            $('#emptyText').text('暂无已安装的应用');
            $('#emptyHint').text('点击"上传安装"或前往"应用市场"安装应用');
            return;
        }
        $('#appEmpty').hide();

        var html = '';
        apps.forEach(function(app, idx) {
                var statusBadge = '';
                if (app.status === 1) { statusBadge = '<span class="app-card-badge enabled">已启用</span>'; enabled++; }
                else if (app.status === 0) { statusBadge = '<span class="app-card-badge installed">已安装</span>'; disabled++; }
                else if (app.status === 2) { statusBadge = '<span class="app-card-badge disabled">已禁用</span>'; disabled++; }

                var systemTag = app.is_system ? '<span class="app-card-system">系统</span>' : '';
                var updateBadge = app.has_update ? '<span class="app-card-system" style="background:#fff3e0;color:#ff9800;">有更新</span>' : '';

                var actions = '';
                if (app.status === 1) {
                    actions += '<button class="layui-btn layui-btn-sm layui-btn-warm" onclick="toggleApp(\''+app.app_id+'\',\'disable\')">禁用</button>';
                } else if (app.status === 0 || app.status === 2) {
                    actions += '<button class="layui-btn layui-btn-sm" onclick="toggleApp(\''+app.app_id+'\',\'enable\')">启用</button>';
                }
                if (app.has_update) {
                    actions += '<button class="layui-btn layui-btn-sm layui-btn-normal" onclick="upgradeApp(\''+app.app_id+'\',\''+app.name.replace(/'/g, "\\'")+'\')"><i class="fa fa-cloud-download"></i> 在线升级</button>';
                }
                actions += '<button class="layui-btn layui-btn-sm" onclick="manualUpgrade(\''+app.app_id+'\',\''+app.name+'\')"><i class="fa fa-upload"></i> 手动</button>';
                actions += '<button class="layui-btn layui-btn-sm layui-btn-primary" onclick="exportApp(\''+app.app_id+'\',\''+app.name+'\')">导出</button>';
                var hasConfig = app.manifest && app.manifest.config_groups && app.manifest.config_groups.length > 0;
                if (hasConfig) {
                    actions += '<button class="layui-btn layui-btn-sm" style="background:#9575cd;color:#fff;" onclick="openAppSettings(\''+app.app_id+'\',\''+app.name+'\')"><i class="fa fa-cog"></i> 设置</button>';
                }
                if (!app.is_system) {
                    actions += '<button class="layui-btn layui-btn-sm layui-btn-danger" onclick="uninstallApp(\''+app.app_id+'\',\''+app.name+'\')">卸载</button>';
                }

                html += '<div class="app-card">'
                    + systemTag
                    + updateBadge
                    + '<div class="app-card-top">'
                    + '<div class="app-card-icon">' + renderAppIcon(app.icon, app.icon_url) + '</div>'
                    + '<div class="app-card-body">'
                    + '<div class="app-card-head">'
                    + '<div class="app-card-head-row">'
                    + '<span class="app-card-name">'+app.name+'</span>'
                    + '<span class="app-card-backup-btn" onclick="openBackupDialog(\''+app.app_id+'\',\''+app.name+'\')"><i class="fa fa-database"></i> 备份</span>'
                    + '</div>'
                    + '<div class="app-card-head-row">'
                    + '<span class="app-card-version">v'+app.version+'</span>'
                    + statusBadge
                    + (app.author ? '<span style="font-size:13px;color:#999;">by '+app.author+'</span>' : '')
                    + '</div></div>'
                    + '</div></div>'
                    + '<div class="app-card-desc">'+(app.description||'')+'</div>'
                    + '<div class="app-card-actions">'+actions+'</div>'
                    + '</div>';
            });

            $('#appList').html(html);
            $('#enabledCount').text(enabled);
            $('#disabledCount').text(disabled);
    }

    function renderUninstalledApps(apps) {
        if (apps.length === 0) {
            $('#appList').html('');
            $('#appEmpty').show();
            $('#emptyText').text('暂无未安装的应用');
            $('#emptyHint').text('所有可用应用均已安装');
            return;
        }
        $('#appEmpty').hide();

        var html = '';
        apps.forEach(function(app, idx) {
            html += '<div class="app-card">'
                + '<div class="app-card-top">'
                + '<div class="app-card-icon">' + renderAppIcon(app.icon, app.icon_url) + '</div>'
                + '<div class="app-card-body">'
                + '<div class="app-card-head">'
                + '<span class="app-card-name">'+app.name+'</span>'
                + '<div class="app-card-head-row">'
                + '<span class="app-card-version">v'+app.version+'</span>'
                + '<span class="app-card-badge installed">未安装</span>'
                + (app.author ? '<span style="font-size:13px;color:#999;">by '+app.author+'</span>' : '')
                + '</div></div>'
                + '</div></div>'
                + '<div class="app-card-desc">'+(app.description||'')+'</div>'
                + '<div class="app-card-actions">'
                + '<button class="layui-btn layui-btn-sm layui-btn-normal" onclick="installFromMarket(\''+app.app_id+'\',\''+app.name+'\')"><i class="fa fa-download"></i> 安装</button>'
                + '<button class="layui-btn layui-btn-sm layui-btn-primary" onclick="exportApp(\''+app.app_id+'\',\''+app.name+'\')"><i class="fa fa-file-archive-o"></i> 导出</button>'
                + '<button class="layui-btn layui-btn-sm layui-btn-danger" onclick="deleteAppFiles(\''+app.app_id+'\',\''+app.name+'\')"><i class="fa fa-trash"></i> 删除</button>'
                + '</div>'
                + '</div>';
        });

        $('#appList').html(html);
    }

    window.installFromMarket = function(appId, appName) {
        layer.confirm('确定要安装「' + appName + '」应用吗？', {icon:3}, function(idx) {
            layer.close(idx);
            var loadIdx = layer.load(2, {content:'正在安装「'+appName+'」，请耐心等待...', time:0});
            $.ajax({
                url: '/api/admin/apps/' + appId + '/local-install',
                type: 'POST',
                timeout: 600000,
                dataType: 'json',
                success: function(res) {
                    layer.close(loadIdx);
                    if (res.code === 0) {
                        layer.msg('安装成功', {icon:1, time:1500}, function() {
                            loadApps();
                            if (top.PearAdmin && top.PearAdmin.refreshMenu) top.PearAdmin.refreshMenu();
                        });
                    } else {
                        layer.msg(res.message, {icon:2, time:3000});
                    }
                },
                error: function(xhr) {
                    layer.close(loadIdx);
                    if (xhr.status === 401) return;
                    // 尝试解析响应体，后端可能已成功但连接异常断开
                    try {
                        var res = JSON.parse(xhr.responseText);
                        if (res && res.code === 0) {
                            layer.msg('安装成功', {icon:1, time:1500}, function() {
                                loadApps();
                                if (top.PearAdmin && top.PearAdmin.refreshMenu) top.PearAdmin.refreshMenu();
                            });
                            return;
                        }
                        if (res && res.message) {
                            layer.msg(res.message, {icon:2, time:3000});
                            return;
                        }
                    } catch(e) {}
                    layer.msg('安装失败，请稍后重试 (HTTP ' + (xhr.status || 'unknown') + ')', {icon:2, time:3000});
                }
            });
        });
    };

    window.deleteAppFiles = function(appId, appName) {
        var contentHtml = '<div style="padding:20px;">'
            + '<div style="text-align:center;margin-bottom:15px;">'
            + '<i class="fa fa-exclamation-triangle" style="font-size:36px;color:#ff5722;"></i>'
            + '<h3 style="margin:10px 0 5px;">确认删除「<span id="dlgDeleteName"></span>」</h3>'
            + '</div>'
            + '<div style="background:#fff3e0;padding:12px 15px;border-radius:6px;margin-bottom:15px;font-size:13px;color:#e65100;">'
            + '<p>此操作将永久删除应用的所有文件，此操作不可恢复！</p>'
            + '</div>'
            + '<div style="margin-bottom:15px;">'
            + '<label style="font-size:13px;color:#666;">请输入「<span id="dlgDeleteConfirmName"></span>」确认删除：</label>'
            + '<input type="text" id="dlgDeleteInput" class="layui-input" style="margin-top:5px;" placeholder="请输入应用名称">'
            + '</div>'
            + '<div style="text-align:center;">'
            + '<button type="button" class="layui-btn layui-btn-primary" id="dlgBtnCancelDelete">取消</button>'
            + '<button type="button" class="layui-btn layui-btn-danger" id="dlgBtnDoDelete" disabled>确认删除</button>'
            + '</div></div>';

        layer.open({
            type: 1,
            title: '确认删除',
            area: ['420px', '440px'],
            content: contentHtml,
            success: function(layero) {
                layero.find('#dlgDeleteName').text(appName);
                layero.find('#dlgDeleteConfirmName').text(appName);
                var confirmInput = layero.find('#dlgDeleteInput');
                var btnDelete = layero.find('#dlgBtnDoDelete');
                confirmInput.on('input', function() {
                    btnDelete.prop('disabled', $(this).val() !== appName);
                });
                layero.find('#dlgBtnCancelDelete').on('click', function() { layer.closeAll(); });
                btnDelete.on('click', function() {
                    if (confirmInput.val() !== appName) return;
                    var loadIdx = layer.load(2);
                    $.post('/api/admin/apps/' + appId + '/delete', function(res) {
                        layer.close(loadIdx);
                        if (res.code === 0) {
                            layer.msg('删除成功', {icon:1});
                            layer.closeAll();
                            loadApps();
                        } else {
                            layer.msg(res.message, {icon:2});
                        }
                    });
                });
            }
        });
    };

    $('#appTabs').on('click', '.app-tab', function() {
        var tab = $(this).data('tab');
        if (tab === currentTab) return;

        $('.app-tab').removeClass('active');
        $(this).addClass('active');
        currentTab = tab;

        if (tab === 'installed') {
            $.get('/api/admin/apps', function(res) {
                if (res.code !== 0) { layer.msg(res.message, {icon:2}); return; }
                renderInstalledApps(res.data || []);
            });
        } else if (tab === 'uninstalled') {
            $.get('/api/admin/apps/available', function(res) {
                if (res.code !== 0) { layer.msg(res.message, {icon:2}); return; }
                allAvailableApps = res.data || {};
                renderUninstalledApps(allAvailableApps.uninstalled_apps || []);
            });
        }
    });

    loadApps();

    $('#btnUpload').on('click', function() {
        openUploadDialog('上传安装应用', '/api/admin/apps/upload', function(res) { return res.message || '操作成功'; });
    });

    $('#btnCheckUpdates').on('click', function() {
        var loadIdx = layer.load(2);
        $.post('/api/admin/apps/check-updates', function(res) {
            layer.close(loadIdx);
            if (res.code !== 0) { layer.msg(res.message, {icon:2}); return; }
            var updates = res.data || [];
            if (updates.length === 0) {
                layer.msg('所有应用均为最新版本', {icon:1});
            } else {
                var html = '<div style="padding:15px;">';
                updates.forEach(function(u) {
                    var safeName = u.name.replace(/'/g, "\\'");
                    var versionsJson = JSON.stringify(u.available_versions || []).replace(/"/g, '&quot;');
                    html += '<div style="padding:8px 0;border-bottom:1px solid #f0f0f0;">'
                        + '<strong>'+u.name+'</strong> '
                        + '<span style="color:#999;">v'+u.current_version+'</span> → '
                        + '<span style="color:#4caf50;">v'+u.latest_version+'</span> '
                        + '<button class="layui-btn layui-btn-xs" onclick="upgradeApp(\''+u.app_id+'\',\''+safeName+'\','+versionsJson+')">在线升级</button> '
                        + '<button class="layui-btn layui-btn-xs layui-btn-primary" onclick="manualUpgrade(\''+u.app_id+'\',\''+safeName+'\')">手动升级</button>'
                        + '</div>';
                });
                html += '</div>';
                layer.open({ type:1, title:'可用更新', area:['450px','350px'], content:html });
            }
        });
    });

    window.exportApp = function(appId, appName) {
        layer.confirm('确定要导出「' + appName + '」应用包吗？', {icon:3}, function(idx) {
            layer.close(idx);
            var loadIdx = layer.load(2);
            var xhr = new XMLHttpRequest();
            xhr.open('GET', '/api/admin/apps/' + appId + '/export', true);
            xhr.setRequestHeader('X-CSRF-TOKEN', '{{ csrf_token() }}');
            xhr.responseType = 'blob';
            xhr.onload = function() {
                layer.close(loadIdx);
                if (xhr.status === 401) {
                    layer.msg('登录已过期，请重新登录', {icon: 2, time: 1500}, function() {
                        window.location.href = '/admin/login?redirect=' + encodeURIComponent(window.location.pathname + window.location.search);
                    });
                    return;
                }
                if (xhr.status === 200) {
                    var blob = xhr.response;
                    var contentDisposition = xhr.getResponseHeader('Content-Disposition');
                    var fileName = appId + '.zip';
                    if (contentDisposition) {
                        var matches = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
                        if (matches && matches[1]) {
                            fileName = matches[1].replace(/['"]/g, '');
                        }
                    }
                    var url = window.URL.createObjectURL(blob);
                    var a = document.createElement('a');
                    a.href = url;
                    a.download = fileName;
                    document.body.appendChild(a);
                    a.click();
                    document.body.removeChild(a);
                    window.URL.revokeObjectURL(url);
                    layer.msg('导出成功', {icon:1});
                } else {
                    layer.msg('导出失败', {icon:2});
                }
            };
            xhr.onerror = function() {
                layer.close(loadIdx);
                layer.msg('导出失败', {icon:2});
            };
            xhr.send();
        });
    };

    window.toggleApp = function(appId, action) {
        var actionLabel = action === 'enable' ? '启用' : '禁用';
        layer.confirm('确定要'+actionLabel+'此应用吗？', {icon:3}, function(idx) {
            layer.close(idx);
            var loadIdx = layer.load(2);
            $.post('/api/admin/apps/'+appId+'/'+action, function(res) {
                layer.close(loadIdx);
                if (res.code === 0) {
                    layer.msg(actionLabel+'成功', {icon:1});
                    loadApps();
                    // 无刷新更新侧边菜单
                    if (top.PearAdmin && top.PearAdmin.refreshMenu) top.PearAdmin.refreshMenu();
                } else {
                    layer.msg(res.message, {icon:2});
                }
            });
        });
    };

    window.uninstallApp = function(appId, appName) {
        var contentHtml = '<div style="padding:20px;">'
            + '<div style="text-align:center;margin-bottom:15px;">'
            + '<i class="fa fa-exclamation-triangle" style="font-size:36px;color:#ff5722;"></i>'
            + '<h3 style="margin:10px 0 5px;">确认卸载「<span id="dlgUninstallName"></span>」</h3>'
            + '</div>'
            + '<div style="background:#fff3e0;padding:12px 15px;border-radius:6px;margin-bottom:15px;font-size:13px;color:#e65100;">'
            + '<p>此操作将删除以下内容：</p>'
            + '<ul style="padding-left:20px;margin-top:5px;"><li>数据库表</li><li>菜单项</li><li>配置项</li><li>权限项</li><li>静态资源</li></ul>'
            + '<p style="margin-top:8px;font-weight:600;">应用文件将保留，可重新安装或手动删除！</p>'
            + '</div>'
            + '<div style="margin-bottom:15px;">'
            + '<label style="font-size:13px;color:#666;">请输入「<span id="dlgUninstallConfirmName"></span>」确认卸载：</label>'
            + '<input type="text" id="dlgUninstallInput" class="layui-input" style="margin-top:5px;" placeholder="请输入应用名称">'
            + '</div>'
            + '<div style="text-align:center;">'
            + '<button type="button" class="layui-btn layui-btn-primary" id="dlgBtnCancelUninstall">取消</button>'
            + '<button type="button" class="layui-btn layui-btn-danger" id="dlgBtnDoUninstall" disabled>确认卸载</button>'
            + '</div></div>';

        layer.open({
            type: 1,
            title: '确认卸载',
            area: ['420px', '520px'],
            content: contentHtml,
            success: function(layero) {
                layero.find('#dlgUninstallName').text(appName);
                layero.find('#dlgUninstallConfirmName').text(appName);
                var confirmInput = layero.find('#dlgUninstallInput');
                var btnUninstall = layero.find('#dlgBtnDoUninstall');
                confirmInput.on('input', function() {
                    btnUninstall.prop('disabled', $(this).val() !== appName);
                });
                layero.find('#dlgBtnCancelUninstall').on('click', function() { layer.closeAll(); });
                btnUninstall.on('click', function() {
                    if (confirmInput.val() !== appName) return;
                    var loadIdx = layer.load(2, {content:'正在卸载「'+appName+'」，请耐心等待...', time:0});
                    $.ajax({
                        url: '/api/admin/apps/'+appId+'/uninstall',
                        type: 'POST',
                        timeout: 600000,
                        dataType: 'json',
                        success: function(res) {
                            layer.close(loadIdx);
                            if (res.code === 0) {
                                layer.msg('卸载成功', {icon:1, time:1500}, function() {
                                    layer.closeAll();
                                    loadApps();
                                    if (top.PearAdmin && top.PearAdmin.refreshMenu) top.PearAdmin.refreshMenu();
                                });
                            } else {
                                layer.msg(res.message || '卸载失败', {icon:2, time:3000});
                            }
                        },
                        error: function(xhr) {
                            layer.close(loadIdx);
                            if (xhr.status === 401) return;
                            // 尝试解析响应体，后端可能已成功但连接异常断开
                            try {
                                var res = JSON.parse(xhr.responseText);
                                if (res && res.code === 0) {
                                    layer.msg('卸载成功', {icon:1, time:1500}, function() {
                                        layer.closeAll();
                                        loadApps();
                                        if (top.PearAdmin && top.PearAdmin.refreshMenu) top.PearAdmin.refreshMenu();
                                    });
                                    return;
                                }
                                if (res && res.message) {
                                    layer.msg(res.message, {icon:2, time:3000});
                                    return;
                                }
                            } catch(e) {}
                            layer.msg('卸载请求失败，请稍后重试 (HTTP ' + (xhr.status || 'unknown') + ')', {icon:2, time:3000});
                        }
                    });
                });
            }
        });
    };

    window.upgradeApp = function(appId, appName, availableVersions) {
        // 若未传入版本列表，先调用 checkUpdates 获取
        if (!availableVersions || !availableVersions.length) {
            var loadIdx0 = layer.load(2);
            $.post('/api/admin/apps/check-updates', function(res) {
                layer.close(loadIdx0);
                if (res.code !== 0) { layer.msg(res.message, {icon:2}); return; }
                var updateInfo = (res.data || []).find(function(u) { return u.app_id === appId; });
                if (!updateInfo) {
                    layer.msg('没有可用的更新', {icon:1});
                    return;
                }
                window.upgradeApp(appId, appName, updateInfo.available_versions || []);
            }).fail(function() {
                layer.close(loadIdx0);
                layer.msg('获取更新信息失败', {icon:2});
            });
            return;
        }

        var currentVersion = (window.currentAppVersions && window.currentAppVersions[appId]) || '?';

        // 构建版本选择 HTML
        var radioHtml = '<div style="padding:15px;">'
            + '<div style="margin-bottom:10px;color:#666;">当前版本：<strong style="color:#333;">v' + currentVersion + '</strong></div>'
            + '<div style="margin-bottom:12px;padding:8px 12px;background:#fff8e1;border-left:3px solid #ff9800;color:#e65100;font-size:13px;">'
            + '<i class="fa fa-exclamation-triangle"></i> 升级前请先使用「备份」功能备份应用，以防升级失败可恢复'
            + '</div>'
            + '<form class="layui-form" id="upgradeVersionForm">';

        availableVersions.forEach(function(v, idx) {
            var checked = (idx === 0) ? 'checked' : '';
            var label;
            if (idx === 0) {
                label = '（单步升级，推荐）';
            } else if (idx === availableVersions.length - 1) {
                label = '（直接最新）';
            } else {
                label = '（跳跃升级）';
            }
            radioHtml += '<div style="padding:8px 0;border-bottom:1px solid #f0f0f0;">'
                + '<input type="radio" name="upgrade_version" value="' + v.version + '" title="v' + v.version + ' ' + label + '" ' + checked + '>'
                + '<div style="padding-left:24px;color:#999;font-size:12px;margin-top:4px;">' + (v.changelog || '无更新日志') + '</div>'
                + '</div>';
        });

        radioHtml += '</form></div>';

        layer.open({
            type: 1,
            title: '升级「' + appName + '」',
            area: ['500px', '450px'],
            content: radioHtml,
            btn: ['我已备份，确认升级', '先去备份', '取消'],
            success: function() {
                layui.form.render('radio');
            },
            btn2: function() {
                // 打开备份对话框，不关闭当前对话框
                openBackupDialog(appId, appName);
                return false;
            },
            btn1: function(idx) {
                var targetVersion = $('input[name="upgrade_version"]:checked').val();
                if (!targetVersion) {
                    layer.msg('请选择升级版本', {icon:2});
                    return;
                }
                layer.close(idx);
                doUpgrade(appId, targetVersion);
            }
        });
    };

    // 执行升级（下载升级包并安装）
    function doUpgrade(appId, targetVersion) {
        var loadIdx = layer.load(2, {content:'正在下载升级包 v' + targetVersion + '...', time:0});
        $.post('/api/admin/apps/' + appId + '/upgrade?version=' + encodeURIComponent(targetVersion), function(res) {
            layer.close(loadIdx);
            if (res.code === 0) {
                layer.msg('升级成功，当前版本：v' + targetVersion, {icon:1}, function() {
                    loadApps();
                });
            } else {
                layer.msg(res.message, {icon:2});
            }
        }).fail(function() {
            layer.close(loadIdx);
            layer.msg('升级失败，请检查网络连接', {icon:2});
        });
    }

    window.manualUpgrade = function(appId, appName) {
        openUploadDialog('手动升级「' + appName + '」', '/api/admin/apps/upload', function(res) { return res.message || '操作成功'; }, { app_id: appId });
    };

    window.openAppSettings = function(appId, appName) {
        var loadIdx = layer.load(2);
        $.get('/api/admin/apps/' + appId + '/config', function(res) {
            layer.close(loadIdx);
            if (res.code !== 0) { layer.msg(res.message || '获取配置失败', {icon:2}); return; }

            var groups = res.data || [];
            if (groups.length === 0) {
                layer.msg('该应用暂无配置项', {icon:3});
                return;
            }

            var contentHtml = '<div style="padding:20px;">'
                + '<form class="layui-form" lay-filter="appSettingForm_' + appId + '" id="appSettingForm_' + appId + '">';

            for (var gi = 0; gi < groups.length; gi++) {
                var g = groups[gi];
                var items = g.items || [];

                if (gi > 0) {
                    contentHtml += '<div style="margin-top:16px;padding-top:12px;border-top:1px solid #f0f0f0;">';
                }
                contentHtml += '<div style="font-size:14px;font-weight:600;margin-bottom:12px;color:#333;">' + g.title + '</div>';

                if (items.length > 0) {
                    for (var j = 0; j < items.length; j++) {
                        contentHtml += renderConfigItem(items[j]);
                    }
                } else {
                    contentHtml += '<div style="padding:15px;text-align:center;color:#999;font-size:13px;">该分组暂无配置项</div>';
                }

                if (gi > 0) {
                    contentHtml += '</div>';
                }
            }

            contentHtml += '</form>'
                + '<div style="text-align:right;margin-top:18px;padding-top:14px;border-top:1px solid #f0f0f0;">'
                + '<button type="button" class="layui-btn layui-btn-primary" id="dlgBtnCancelSetting">取消</button> '
                + '<button type="button" class="layui-btn" id="dlgBtnSaveSetting" data-app-id="' + appId + '"><i class="fa fa-check"></i> 保存</button>'
                + '</div></div>';

            layer.open({
                type: 1,
                title: '「' + appName + '」应用设置',
                area: ['560px', 'auto'],
                maxHeight: 500,
                content: contentHtml,
                success: function(layero) {
                    form.render();

                    bindImagePreviewsInDialog(layero);

                    layero.find('#dlgBtnCancelSetting').on('click', function() {
                        layer.closeAll();
                    });

                    layero.find('#dlgBtnSaveSetting').on('click', function() {
                        saveAppConfig(appId, layero);
                    });
                }
            });
        }).fail(function() {
            layer.close(loadIdx);
            layer.msg('请求失败', {icon:2});
        });
    };

    function renderConfigItem(item) {
        var val = item.value || '';
        var tips = item.tips ? '<div style="font-size:12px;color:#999;margin-top:4px;line-height:1.5;">' + item.tips + '</div>' : '';
        var html = '<div class="layui-form-item" style="display:flex;align-items:center;margin-bottom:12px;">';
        html += '<label class="layui-form-label" style="width:120px;flex-shrink:0;padding:0 10px 0 0;text-align:right;float:none;">' + item.name + '</label>';
        html += '<div class="layui-input-block" style="margin-left:0!important;flex:1;min-width:0;float:none;">';

        switch (item.type) {
            case 'switch':
                var checked = val === '1' || val === 'true';
                html += '<input type="checkbox" name="' + item.code + '" lay-skin="switch" lay-text="开启|关闭" ' + (checked ? 'checked' : '') + ' data-code="' + item.code + '" data-type="switch">';
                break;
            case 'select':
                var opts = item.options || [];
                html += '<select name="' + item.code + '" data-code="' + item.code + '">';
                for (var i = 0; i < opts.length; i++) {
                    var opt = opts[i];
                    if (typeof opt === 'object') {
                        html += '<option value="' + opt.value + '"' + (opt.value == val ? ' selected' : '') + '>' + opt.label + '</option>';
                    } else {
                        html += '<option value="' + opt + '"' + (opt == val ? ' selected' : '') + '>' + opt + '</option>';
                    }
                }
                html += '</select>';
                break;
            case 'textarea':
                html += '<textarea name="' + item.code + '" class="layui-textarea" data-code="' + item.code + '">' + val + '</textarea>';
                break;
            case 'number':
                html += '<input type="number" name="' + item.code + '" value="' + val + '" class="layui-input" data-code="' + item.code + '">';
                break;
            case 'image':
                html += '<div style="display:flex;align-items:center;gap:6px;">';
                html += '<input type="text" name="' + item.code + '" value="' + val + '" class="layui-input" placeholder="图片URL" data-code="' + item.code + '" data-type="image" style="flex:1;">';
                html += '<button type="button" class="layui-btn layui-btn-sm layui-btn-normal" onclick="chooseAppImage(\''+item.code+'\')">选择</button>';
                html += '</div>';
                html += '<img class="image-preview" id="app_preview_' + item.code + '" src="' + val + '" style="max-width:180px;max-height:60px;margin-top:4px;border:1px solid #e6e6e6;border-radius:4px;display:none;">';
                break;
            default:
                html += '<input type="text" name="' + item.code + '" value="' + val + '" class="layui-input" data-code="' + item.code + '">';
        }

        html += tips;
        html += '</div></div>';
        return html;
    }

    function bindImagePreviewsInDialog(layero) {
        layero.find('input[data-type="image"]').each(function() {
            var input = $(this);
            var code = input.data('code');
            var preview = layero.find('#app_preview_' + code);
            if (input.val()) {
                preview.show();
            }
            input.on('input', function() {
                if ($(this).val()) {
                    preview.attr('src', $(this).val()).show();
                } else {
                    preview.hide();
                }
            });
        });
    }

    function saveAppConfig(appId, layero) {
        var formData = {};
        layero.find('[data-code]').each(function() {
            var code = $(this).data('code');
            var type = $(this).data('type');
            var value;
            if (type === 'switch') {
                value = $(this).prop('checked') ? '1' : '0';
            } else {
                value = $(this).val();
            }
            formData[code] = value;
        });

        if ($.isEmptyObject(formData)) {
            layer.msg('没有需要保存的配置', {icon:3});
            return;
        }

        var loadIdx = layer.load(2);
        $.ajax({
            url: '/api/admin/apps/' + appId + '/config',
            type: 'PUT',
            dataType: 'json',
            contentType: 'application/json',
            data: JSON.stringify(formData),
            success: function(res) {
                layer.close(loadIdx);
                if (res.code === 0) {
                    layer.msg('保存成功', {icon:1}, function() {
                        layer.closeAll();
                    });
                } else {
                    layer.msg(res.message || '保存失败', {icon:2});
                }
            },
            error: function() {
                layer.close(loadIdx);
                layer.msg('请求失败', {icon:2});
            }
        });
    }

    window.chooseAppImage = function(code) {
        layer.open({
            type: 2,
            title: '选择图片',
            area: ['90%', '80%'],
            content: '/admin/attachment?picker=1',
            end: function() {
                var selectedUrl = sessionStorage.getItem('selectedAttachmentUrl');
                if (selectedUrl) {
                    $('input[data-code="' + code + '"]').val(selectedUrl).trigger('input');
                    sessionStorage.removeItem('selectedAttachmentUrl');
                }
            }
        });
    };

    // 应用备份功能
    window.openBackupDialog = function(appId, appName) {
        var loadIdx = layer.load(2);
        $.get('/api/admin/apps/' + appId + '/backups', function(res) {
            layer.close(loadIdx);
            if (res.code !== 0) { layer.msg(res.message || '获取备份列表失败', {icon:2}); return; }

            var backups = res.data || [];
            var listHtml = '';
            if (backups.length === 0) {
                listHtml = '<div style="text-align:center;padding:40px 0;color:#999;"><i class="fa fa-database" style="font-size:36px;display:block;margin-bottom:10px;"></i>暂无备份记录</div>';
            } else {
                listHtml = '<table class="layui-table" style="margin:0;">'
                    + '<colgroup><col width="168"><col width="120"><col width="140"><col></colgroup>'
                    + '<thead><tr><th>备份时间</th><th>版本</th><th>大小</th><th style="text-align:right;">操作</th></tr></thead><tbody>';
                backups.forEach(function(b) {
                    var actions = '';
                    if (b.status === 1) {
                        actions += '<button class="layui-btn layui-btn-xs layui-btn-warm" onclick="downloadAppBackup('+b.id+')"><i class="fa fa-download"></i> 导出</button> ';
                        actions += '<button class="layui-btn layui-btn-xs" onclick="restoreAppBackup('+b.id+',\''+appId+'\',\''+appName+'\')"><i class="fa fa-undo"></i> 恢复</button> ';
                    }
                    actions += '<button class="layui-btn layui-btn-xs layui-btn-danger" onclick="deleteAppBackup('+b.id+',\''+appId+'\',\''+appName+'\')"><i class="fa fa-trash"></i> 删除</button>';
                    listHtml += '<tr>'
                        + '<td>' + (b.create_time || '') + '</td>'
                        + '<td>v' + (b.app_version || '') + '</td>'
                        + '<td>' + formatSize(b.file_size || 0) + '</td>'
                        + '<td style="text-align:right;">' + actions + '</td>'
                        + '</tr>';
                });
                listHtml += '</tbody></table>';
            }

            var contentHtml = '<div style="padding:15px;">'
                + '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">'
                + '<div style="display:flex;gap:8px;align-items:center;">'
                + '<span style="font-size:14px;color:#666;">共 ' + backups.length + ' 条备份记录</span>'
                + '<button class="layui-btn layui-btn-sm layui-btn-warm" id="dlgBtnImportBackup" data-app-id="' + appId + '" data-app-name="' + appName + '" style="margin-left:8px;"><i class="fa fa-upload"></i> 导入恢复</button>'
                + '</div>'
                + '<div style="display:flex;gap:8px;">'
                + '<button class="layui-btn layui-btn-sm layui-btn-normal" id="dlgBtnCreateBackup" data-app-id="' + appId + '"><i class="fa fa-plus"></i> 立即备份</button>'
                + '</div>'
                + '</div>'
                + listHtml
                + '</div>';

            layer.open({
                type: 1,
                title: '「' + appName + '」备份管理',
                area: ['680px', '480px'],
                content: contentHtml,
                success: function(layero) {
                    layero.find('#dlgBtnCreateBackup').on('click', function() {
                        var btn = $(this);
                        var aid = btn.data('app-id');
                        layer.open({
                            type: 1,
                            title: '确认备份「' + appName + '」',
                            area: ['460px', 'auto'],
                            content: '<div style="padding:20px;">'
                                + '<div style="margin-bottom:16px;font-size:13px;color:#666;">备份将包含应用数据表和程序文件</div>'
                                + '<div class="layui-form-item" style="margin-bottom:16px;">'
                                + '<label style="font-size:13px;color:#333;display:block;margin-bottom:6px;">分卷大小（MB）</label>'
                                + '<input type="number" id="dlgVolumeSize" class="layui-input" value="2" min="1" max="100" style="width:120px;">'
                                + '<div style="font-size:12px;color:#999;margin-top:6px;line-height:1.6;">'
                                + '分卷越小单次操作越稳定，但卷数越多；分卷越大效率越高，但服务器性能不足时容易超时。'
                                + '<br>建议值：虚拟主机 1-5MB，云服务器 10-50MB。'
                                + '</div></div>'
                                + '<div style="text-align:right;padding-top:10px;border-top:1px solid #f0f0f0;">'
                                + '<button type="button" class="layui-btn layui-btn-primary" id="dlgBtnCancelBackup">取消</button> '
                                + '<button type="button" class="layui-btn" id="dlgBtnConfirmBackup"><i class="fa fa-database"></i> 确认备份</button>'
                                + '</div></div>',
                            success: function(layero) {
                                layero.find('#dlgBtnCancelBackup').on('click', function() { layer.closeAll(); });
                                layero.find('#dlgBtnConfirmBackup').on('click', function() {
                                    var vs = parseInt(layero.find('#dlgVolumeSize').val()) || 2;
                                    if (vs < 1) vs = 1;
                                    if (vs > 100) vs = 100;
                                    layer.closeAll();
                                    doBackupWithProgress(aid, appName, vs);
                                });
                            }
                        });
                    });

                    // 导入恢复按钮
                    layero.find('#dlgBtnImportBackup').on('click', function() {
                        var btn = $(this);
                        var aid = btn.data('app-id');
                        var aname = btn.data('app-name');
                        openImportDialog(aid, aname);
                    });
                }
            });
        }).fail(function() {
            layer.close(loadIdx);
            layer.msg('获取备份列表失败', {icon:2});
        });
    };

    /**
     * 多步备份 - 带进度展示
     */
    window.doBackupWithProgress = function(aid, appName, volumeSize) {
        var layerIdx = layer.open({
            type: 1,
            title: '正在备份「' + appName + '」',
            area: ['480px', '280px'],
            shadeClose: false,
            content: '<div style="padding:24px;">'
                + '<div style="margin-bottom:12px;"><span id="bkStatus">正在准备...</span></div>'
                + '<div class="layui-progress layui-progress-big" lay-showpercent="true">'
                + '<div class="layui-progress-bar" id="bkProgressBar" lay-percent="0%"></div></div>'
                + '<div style="margin-top:12px;font-size:12px;color:#999;" id="bkDetail"></div>'
                + '</div>'
        });

        var totalTables = 0;
        var sessionId = '';

        // 步骤1：准备
        $.ajax({
            url: '/api/admin/apps/' + aid + '/backups/prepare',
            type: 'POST',
            data: { volume_size: volumeSize },
            timeout: 30000,
            success: function(res) {
                if (res.code !== 0) {
                    layer.close(layerIdx);
                    layer.msg(res.message || '准备失败', {icon:2});
                    return;
                }
                totalTables = res.data.total_tables;
                sessionId = res.data.session_id;
                updateProgress(0, totalTables, '准备完成，共 ' + totalTables + ' 张表', '');
                // 开始逐表备份
                backupNextTable(0, res.data.tables);
            },
            error: function() {
                layer.close(layerIdx);
                layer.msg('备份请求失败', {icon:2});
            }
        });

        function backupNextTable(index, tables) {
            if (index >= tables.length) {
                doFinish();
                return;
            }
            var table = tables[index];
            // 开始备份当前表的第一页
            backupTablePage(index, tables, table, null);
        }

        function backupTablePage(index, tables, table, lastId) {
            var postData = { session_id: sessionId, table: table, volume_size: volumeSize };
            if (lastId !== null) {
                postData.last_id = lastId;
            }

            updateProgress(index, tables.length, '正在备份表 ' + (index+1) + '/' + tables.length,
                lastId === null ? table : (table + ' | 正在分页处理...'));

            $.ajax({
                url: '/api/admin/apps/' + aid + '/backups/table',
                type: 'POST',
                data: postData,
                timeout: 300000, // 5分钟/页
                success: function(res) {
                    if (res.code !== 0) {
                        layer.close(layerIdx);
                        layer.msg(res.message || '表备份失败', {icon:2});
                        return;
                    }

                    var d = res.data;
                    if (d.has_more) {
                        // 该表还有下一页 → 继续分页
                        var processed = d.last_id || 0;
                        var total = d.total_rows || 0;
                        var pct = total > 0 ? Math.round(processed / total * 100) : 0;
                        var detail = '第 ' + d.page + ' 页，已处理 ' + processed + '/' + total + ' 行 (' + pct + '%)';
                        updateProgress(index, tables.length, '正在备份表 ' + (index+1) + '/' + tables.length, detail);
                        backupTablePage(index, tables, table, d.last_id);
                    } else {
                        // 该表已全部完成 → 下一张表
                        var detail = '已完成 ' + (d.total_rows || 0) + ' 行';
                        updateProgress(index + 1, tables.length, '表 ' + table + ' 备份完成', detail);
                        backupNextTable(index + 1, tables);
                    }
                },
                error: function() {
                    layer.close(layerIdx);
                    layer.msg('备份请求失败：表 ' + table, {icon:2});
                }
            });
        }

        function doFinish() {
            updateProgress(totalTables, totalTables, '正在打包...', '');
            $.ajax({
                url: '/api/admin/apps/' + aid + '/backups/finish',
                type: 'POST',
                data: { session_id: sessionId },
                timeout: 120000,
                success: function(res) {
                    layer.close(layerIdx);
                    if (res.code === 0) {
                        layer.msg('备份成功！文件大小：' + formatSize(res.data.file_size), {icon:1}, function() {
                            layer.closeAll();
                            openBackupDialog(aid, appName);
                        });
                    } else {
                        layer.msg(res.message || '打包失败', {icon:2});
                    }
                },
                error: function() {
                    layer.close(layerIdx);
                    layer.msg('打包请求失败', {icon:2});
                }
            });
        }

        function updateProgress(current, total, status, detail) {
            var pct = total > 0 ? Math.round(current / total * 100) : 0;
            if (pct > 100) pct = 100;
            // 更新 layui 进度条
            var bar = $('#bkProgressBar');
            if (bar.length > 0) {
                bar.css('width', pct + '%').attr('lay-percent', pct + '%');
                bar.text(pct + '%');
            }
            $('#bkStatus').text(status);
            if (detail) {
                $('#bkDetail').text(detail);
            }
        }
    };

    window.restoreAppBackup = function(backupId, appId, appName) {
        layer.confirm('确定要从备份恢复「' + appName + '」吗？<br><br><span style="color:#ff5722;font-size:13px;font-weight:600;">警告：恢复操作将覆盖当前应用的数据表和程序文件，此操作不可逆！</span>', {icon:0, title:'确认恢复'}, function(idx) {
            layer.close(idx);
            layer.closeAll();
            doRestoreWithProgress(appId, { backup_id: backupId }, appName);
        });
    };

    window.deleteAppBackup = function(backupId, appId, appName) {
        layer.confirm('确定要删除此备份记录吗？<br><span style="color:#999;font-size:12px;">备份文件将一并删除，此操作不可恢复</span>', {icon:3, title:'确认删除'}, function(idx) {
            layer.close(idx);
            var loadIdx = layer.load(2);
            $.ajax({
                url: '/api/admin/apps/backups/' + backupId,
                type: 'DELETE',
                success: function(res) {
                    layer.close(loadIdx);
                    if (res.code === 0) {
                        layer.msg('删除成功', {icon:1}, function() {
                            layer.closeAll();
                            openBackupDialog(appId, appName);
                        });
                    } else {
                        layer.msg(res.message || '删除失败', {icon:2});
                    }
                },
                error: function() {
                    layer.close(loadIdx);
                    layer.msg('删除请求失败', {icon:2});
                }
            });
        });
    };

    // 下载备份文件（导出到本地）
    window.downloadAppBackup = function(backupId) {
        var loadIdx = layer.load(2, {content:'正在准备下载...', time:0});
        // 先验证备份文件存在
        $.ajax({
            url: '/api/admin/apps/backups/' + backupId + '/download',
            type: 'GET',
            xhrFields: { responseType: 'blob' },
            success: function(data, status, xhr) {
                layer.close(loadIdx);
                if (xhr.status === 200 && data.size > 0) {
                    // 从响应头获取文件名
                    var disposition = xhr.getResponseHeader('Content-Disposition');
                    var fileName = 'backup_' + backupId + '.zip';
                    if (disposition) {
                        var matches = disposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
                        if (matches && matches[1]) fileName = matches[1].replace(/['"]/g, '');
                    }
                    var blob = new Blob([data], { type: 'application/zip' });
                    var url = window.URL.createObjectURL(blob);
                    var a = document.createElement('a');
                    a.href = url;
                    a.download = fileName;
                    document.body.appendChild(a);
                    a.click();
                    document.body.removeChild(a);
                    window.URL.revokeObjectURL(url);
                    layer.msg('下载成功', {icon:1});
                } else {
                    // JSON错误响应
                    var reader = new FileReader();
                    reader.onload = function() {
                        try {
                            var res = JSON.parse(reader.result);
                            layer.msg(res.message || '下载失败', {icon:2});
                        } catch(e) {
                            layer.msg('下载失败', {icon:2});
                        }
                    };
                    reader.readAsText(data);
                }
            },
            error: function() {
                layer.close(loadIdx);
                layer.msg('下载请求失败', {icon:2});
            }
        });
    };

    // 导入恢复弹窗（含上传文件 + 从服务器选择）
    window.openImportDialog = function(appId, appName) {
        // 两个 Tab 的内容区域
        var contentHtml = '<div style="padding:20px;">'
            + '<div style="display:flex;border-bottom:1px solid #e6e6e6;margin-bottom:16px;">'
            + '<button class="import-tab active" data-tab="upload" style="padding:8px 20px;border:none;background:none;cursor:pointer;font-size:14px;font-weight:500;color:#1e9fff;border-bottom:2px solid #1e9fff;">上传文件</button>'
            + '<button class="import-tab" data-tab="server" style="padding:8px 20px;border:none;background:none;cursor:pointer;font-size:14px;font-weight:500;color:#999;border-bottom:2px solid transparent;">从服务器选择</button>'
            + '</div>'

            // Tab 1: 上传文件
            + '<div id="importTabUpload">'
            + '<div style="background:#fff3e0;padding:12px 15px;border-radius:6px;margin-bottom:16px;font-size:13px;color:#e65100;">'
            + '<p style="font-weight:600;margin-bottom:6px;">跨站点导入说明</p>'
            + '<ul style="padding-left:18px;margin:0;">'
            + '<li>请选择从其他站点「导出」的 .zip 备份包</li>'
            + '<li>导入将覆盖当前应用的数据库表和程序文件</li>'
            + '<li>请确保目标应用已安装且版本兼容</li>'
            + '</ul></div>'
            + '<div class="app-upload-zone" id="dlgImportZone" style="border-color:#e6e6e6;">'
            + '<div class="app-upload-icon"><i class="fa fa-upload"></i></div>'
            + '<div class="app-upload-text">拖拽 .zip 备份包到此处，或点击下方按钮选择文件</div>'
            + '<button type="button" class="layui-btn layui-btn-sm layui-btn-warm" id="dlgBtnSelectImportFile"><i class="fa fa-folder-open"></i> 选择备份包</button>'
            + '</div>'
            + '<div class="app-upload-file" id="dlgImportFileInfo" style="display:none;">'
            + '<div class="app-upload-file-info">'
            + '<i class="fa fa-file-archive-o" style="font-size:28px;color:#ff9800;"></i>'
            + '<div class="app-upload-file-detail">'
            + '<div class="app-upload-file-name" id="dlgImportFileName"></div>'
            + '<div class="app-upload-file-size" id="dlgImportFileSize"></div>'
            + '</div>'
            + '<button type="button" class="app-upload-file-remove" id="dlgBtnRemoveImportFile"><i class="fa fa-times"></i></button>'
            + '</div></div>'
            + '<div style="text-align:center;margin-top:15px;">'
            + '<button type="button" class="layui-btn layui-btn-disabled" id="dlgBtnDoImport" disabled><i class="fa fa-cloud-upload"></i> 确认导入恢复</button>'
            + '</div></div>'

            // Tab 2: 从服务器选择
            + '<div id="importTabServer" style="display:none;">'
            + '<div style="background:#e8eaf6;padding:10px 14px;border-radius:6px;margin-bottom:12px;font-size:13px;color:#283593;">'
            + '<i class="fa fa-server"></i> 从服务器本地备份文件恢复，无需上传，不受文件大小限制'
            + '</div>'
            + '<div id="dlgServerFileList"><div style="text-align:center;padding:20px;color:#999;">加载中...</div></div>'
            + '</div>'

            + '</div>';

        layer.open({
            type: 1,
            title: '导入恢复「' + appName + '」',
            area: ['580px', '480px'],
            content: contentHtml,
            success: function(layero) {
                var fileInput = $('<input type="file" accept=".zip" style="position:absolute;top:0;left:0;width:0;height:0;opacity:0;">');
                $('body').append(fileInput);

                var selectedFile = null;
                var importZone = layero.find('#dlgImportZone');
                var fileInfo = layero.find('#dlgImportFileInfo');
                var btnDoImport = layero.find('#dlgBtnDoImport');
                var serverTabLoaded = false;

                // Tab 切换
                layero.on('click', '.import-tab', function() {
                    var tab = $(this).data('tab');
                    layero.find('.import-tab').removeClass('active').css({color:'#999', borderBottom:'2px solid transparent'});
                    $(this).addClass('active').css({color:'#1e9fff', borderBottom:'2px solid #1e9fff'});

                    if (tab === 'upload') {
                        layero.find('#importTabUpload').show();
                        layero.find('#importTabServer').hide();
                    } else {
                        layero.find('#importTabUpload').hide();
                        layero.find('#importTabServer').show();
                        // 懒加载服务器文件列表
                        if (!serverTabLoaded) {
                            serverTabLoaded = true;
                            loadServerFiles(layero);
                        }
                    }
                });

                // 加载服务器本地备份文件列表
                function loadServerFiles(lyr) {
                    var listEl = lyr.find('#dlgServerFileList');
                    listEl.html('<div style="text-align:center;padding:20px;color:#999;"><i class="fa fa-spinner fa-spin"></i> 正在读取...</div>');
                    $.get('/api/admin/apps/' + appId + '/backups/local-files', function(res) {
                        if (res.code !== 0) {
                            listEl.html('<div style="text-align:center;padding:20px;color:#f44336;">' + (res.message || '读取失败') + '</div>');
                            return;
                        }
                        var files = res.data || [];
                        if (files.length === 0) {
                            listEl.html('<div style="text-align:center;padding:40px 0;color:#999;">'
                                + '<i class="fa fa-folder-open" style="font-size:36px;display:block;margin-bottom:10px;"></i>'
                                + '<p>服务器备份目录中暂无 .zip 文件</p>'
                                + '<p style="font-size:12px;margin-top:6px;">请先将备份包上传至：/storage/app_backups/' + appId + '/</p>'
                                + '</div>');
                            return;
                        }
                        var html = '<div style="margin-bottom:8px;font-size:12px;color:#999;">'
                            + '<i class="fa fa-info-circle"></i> 文件位于 /storage/app_backups/' + appId + '/ 目录'
                            + '</div>'
                            + '<table class="layui-table" style="margin:0;">'
                            + '<colgroup><col><col width="90"><col width="140"><col width="80"></colgroup>'
                            + '<thead><tr><th>文件名</th><th>大小</th><th>修改时间</th><th style="text-align:right;">操作</th></tr></thead><tbody>';
                        files.forEach(function(f) {
                            var safeName = f.file_name.replace(/'/g, "\\'");
                            html += '<tr>'
                                + '<td style="word-break:break-all;">' + f.file_name + '</td>'
                                + '<td>' + formatSize(f.file_size) + '</td>'
                                + '<td>' + f.modified_time + '</td>'
                                + '<td style="text-align:right;"><button class="layui-btn layui-btn-xs" onclick="doLocalRestore(\''+appId+'\',\''+safeName+'\',\''+appName+'\')"><i class="fa fa-undo"></i> 恢复</button></td>'
                                + '</tr>';
                        });
                        html += '</tbody></table>';
                        listEl.html(html);
                    }).fail(function() {
                        listEl.html('<div style="text-align:center;padding:20px;color:#f44336;">读取服务器文件失败</div>');
                    });
                }

                // 上传文件相关逻辑
                function setImportFile(file) {
                    if (!file || !file.name.endsWith('.zip')) {
                        layer.msg('请选择 .zip 格式的备份包', {icon:2}); return;
                    }
                    selectedFile = file;
                    layero.find('#dlgImportFileName').text(file.name);
                    layero.find('#dlgImportFileSize').text(formatSize(file.size));
                    importZone.hide();
                    fileInfo.show();
                    btnDoImport.prop('disabled', false).removeClass('layui-btn-disabled');
                }

                function clearImportFile() {
                    selectedFile = null;
                    fileInput.val('');
                    importZone.show();
                    fileInfo.hide();
                    btnDoImport.prop('disabled', true).addClass('layui-btn-disabled');
                }

                layero.find('#dlgBtnSelectImportFile').on('click', function(e) {
                    e.stopPropagation(); fileInput.trigger('click');
                });

                importZone.on('click', function() { fileInput.trigger('click'); });

                fileInput.on('change', function() {
                    if (this.files && this.files.length) setImportFile(this.files[0]);
                });

                importZone.on('dragover', function(e) {
                    e.preventDefault(); e.stopPropagation();
                    $(this).addClass('dragover');
                }).on('dragleave drop', function(e) {
                    e.preventDefault(); e.stopPropagation();
                    $(this).removeClass('dragover');
                }).on('drop', function(e) {
                    var files = e.originalEvent.dataTransfer.files;
                    if (files.length) setImportFile(files[0]);
                });

                layero.find('#dlgBtnRemoveImportFile').on('click', clearImportFile);

                btnDoImport.on('click', function() {
                    if (!selectedFile) return;

                    layer.confirm('确定要从此备份包恢复「' + appName + '」吗？<br><br>'
                        + '<span style="color:#ff5722;font-size:13px;font-weight:600;">警告：当前应用的数据表和程序文件将被覆盖！</span>',
                        {icon:0, title:'确认导入恢复'}, function(idx) {
                            layer.close(idx);
                            layer.closeAll();

                            // 先上传文件到服务器本地目录，再走分步恢复
                            var formData = new FormData();
                            formData.append('package', selectedFile);

                            var uploadLoadIdx = layer.load(2, {content:'正在上传文件...', time:0});
                            $.ajax({
                                url: '/api/admin/apps/' + appId + '/backups/upload-file',
                                type: 'POST',
                                data: formData,
                                processData: false,
                                contentType: false,
                                timeout: 600000,
                                success: function(res) {
                                    layer.close(uploadLoadIdx);
                                    if (res.code === 0) {
                                        // 上传成功，走分步恢复
                                        doRestoreWithProgress(appId, { local_file_name: res.data.file_name }, appName);
                                    } else {
                                        layer.msg(res.message || '上传失败', {icon:2});
                                    }
                                },
                                error: function() {
                                    layer.close(uploadLoadIdx);
                                    layer.msg('上传请求失败', {icon:2});
                                }
                            });
                        });
                });

                // 清理
                layero.on('remove', function() { fileInput.remove(); });
            }
        });
    };

    // 从服务器本地文件恢复（分步进度）
    window.doLocalRestore = function(appId, fileName, appName) {
        layer.confirm('确定要从本地文件「' + fileName + '」恢复「' + appName + '」吗？<br><br>'
            + '<span style="color:#ff5722;font-size:13px;font-weight:600;">警告：当前应用的数据表和程序文件将被覆盖！</span>',
            {icon:0, title:'确认本地恢复'}, function(idx) {
                layer.close(idx);
                layer.closeAll();
                doRestoreWithProgress(appId, { local_file_name: fileName }, appName);
            });
    };

    /**
     * 通用分步恢复（带进度显示）
     */
    window.doRestoreWithProgress = function(appId, prepareData, appName) {
        var layerIdx = layer.open({
            type: 1,
            title: '正在恢复「' + appName + '」',
            area: ['480px', '280px'],
            shadeClose: false,
            content: '<div style="padding:24px;">'
                + '<div style="margin-bottom:12px;"><span id="rsStatus">正在准备...</span></div>'
                + '<div class="layui-progress layui-progress-big" lay-showpercent="true">'
                + '<div class="layui-progress-bar" id="rsProgressBar" lay-percent="0%" style="width:0%;background:#1e9fff;"></div></div>'
                + '<div style="margin-top:12px;font-size:12px;color:#999;" id="rsDetail"></div>'
                + '</div>'
        });

        var totalTables = 0;
        var sessionId = '';

        // 步骤1：准备
        $.ajax({
            url: '/api/admin/apps/' + appId + '/backups/restore-prepare',
            type: 'POST',
            data: prepareData,
            timeout: 300000,
            success: function(res) {
                if (res.code !== 0) {
                    layer.close(layerIdx);
                    layer.msg(res.message || '准备失败', {icon:2});
                    return;
                }
                totalTables = res.data.total_tables;
                sessionId = res.data.session_id;
                updateProgress(0, totalTables, '准备完成，共 ' + totalTables + ' 张表', '');
                restoreNextTable(0, res.data.tables, 0);
            },
            error: function() {
                layer.close(layerIdx);
                layer.msg('恢复准备请求失败', {icon:2});
            }
        });

        // 步骤2：逐表恢复（支持大表分批续传）
        function restoreNextTable(tableIndex, tables, tableOffset) {
            if (tableIndex >= tables.length) {
                doFinish();
                return;
            }
            var table = tables[tableIndex];
            updateProgress(tableIndex, tables.length,
                '正在恢复表 ' + (tableIndex+1) + '/' + tables.length + (tableOffset > 0 ? ' (续传 ' + tableOffset + ')' : ''),
                table);

            $.ajax({
                url: '/api/admin/apps/' + appId + '/backups/restore-table',
                type: 'POST',
                data: { session_id: sessionId, table: table, offset: tableOffset },
                timeout: 300000,
                success: function(res) {
                    if (res.code !== 0) {
                        layer.close(layerIdx);
                        layer.msg(res.message || '表恢复失败', {icon:2});
                        return;
                    }
                    if (res.data.has_more) {
                        // 大表未完成，继续分批执行
                        updateProgress(tableIndex, tables.length,
                            '表 ' + table + ' 恢复中 (' + res.data.offset + '/' + res.data.total + ')',
                            table);
                        restoreNextTable(tableIndex, tables, res.data.offset);
                    } else {
                        // 该表完成，进入下一张表
                        updateProgress(tableIndex + 1, tables.length,
                            '表 ' + table + ' 恢复完成', '');
                        restoreNextTable(tableIndex + 1, tables, 0);
                    }
                },
                error: function() {
                    layer.close(layerIdx);
                    layer.msg('恢复请求失败：表 ' + table, {icon:2});
                }
            });
        }

        // 步骤3：完成
        function doFinish() {
            updateProgress(totalTables, totalTables, '正在恢复程序文件...', '');
            $.ajax({
                url: '/api/admin/apps/' + appId + '/backups/restore-finish',
                type: 'POST',
                data: { session_id: sessionId },
                timeout: 120000,
                success: function(res) {
                    layer.close(layerIdx);
                    if (res.code === 0) {
                        layer.msg('恢复成功', {icon:1}, function() {
                            layer.closeAll();
                            loadApps();
                        });
                    } else {
                        layer.msg(res.message || '恢复完成步骤失败', {icon:2});
                    }
                },
                error: function() {
                    layer.close(layerIdx);
                    layer.msg('恢复完成请求失败', {icon:2});
                }
            });
        }

        function updateProgress(current, total, status, detail) {
            var pct = total > 0 ? Math.round(current / total * 100) : 0;
            if (pct > 100) pct = 100;
            var bar = $('#rsProgressBar');
            if (bar.length > 0) {
                bar.css('width', pct + '%');
                bar.text(pct + '%');
            }
            $('#rsStatus').text(status);
            if (detail) {
                $('#rsDetail').text(detail);
            }
        }
    };
});
</script>
</body>
</html>
