<?php

/**
 * @id 16
 * @name 死链/404巡检工具
 * @version 1.0.0
 * @description 批量检查文章、商品、栏目链接状态，快速找出404页面、异常跳转、访问失败等问题链接，便于站点日常维护排查
 * @price 0
 * @type 工具
 * @width 920
 * @height 560
 * @thumbnail /img/id16.png
 */

$plugin_config = <<<EOT
[config_params]
default_limit =
check_timeout =
[/config_params]
EOT;

$plugin_identifier = '16_orderfix';

function qk16_extract_config_params($fileContent)
{
    $pairs = [];
    if (!preg_match('/\[config_params\](.*?)\[\/config_params\]/s', (string)$fileContent, $m)) {
        return $pairs;
    }
    foreach (preg_split('/\r\n|\r|\n/', trim($m[1])) as $line) {
        $line = trim($line);
        if ($line === '' || strpos($line, '=') === false) {
            continue;
        }
        $pos = strpos($line, '=');
        $pairs[trim(substr($line, 0, $pos))] = trim(substr($line, $pos + 1));
    }
    return $pairs;
}

function qk16_site_url()
{
    $proto = 'http://';
    if (
        (!empty($_SERVER['HTTPS']) && strtolower((string)$_SERVER['HTTPS']) !== 'off')
        || (isset($_SERVER['SERVER_PORT']) && (string)$_SERVER['SERVER_PORT'] === '443')
        || (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && stripos((string)$_SERVER['HTTP_X_FORWARDED_PROTO'], 'https') !== false)
        || (!empty($_SERVER['REQUEST_SCHEME']) && strtolower((string)$_SERVER['REQUEST_SCHEME']) === 'https')
    ) {
        $proto = 'https://';
    }
    $host = '';
    if (!empty($_SERVER['HTTP_X_FORWARDED_HOST'])) {
        $host = trim((string)$_SERVER['HTTP_X_FORWARDED_HOST']);
    }
    if ($host === '' && !empty($_SERVER['HTTP_HOST'])) {
        $host = trim((string)$_SERVER['HTTP_HOST']);
    }
    if ($host === '' && !empty($_SERVER['SERVER_NAME'])) {
        $host = trim((string)$_SERVER['SERVER_NAME']);
        if (!empty($_SERVER['SERVER_PORT']) && !in_array((string)$_SERVER['SERVER_PORT'], ['80', '443'], true) && strpos($host, ':') === false) {
            $host .= ':' . trim((string)$_SERVER['SERVER_PORT']);
        }
    }
    return $host === '' ? '' : rtrim($proto . $host, '/');
}

function qk16_table_exists($conn, $table)
{
    $table = trim((string)$table);
    if ($table === '') {
        return false;
    }
    $safe = mysqli_real_escape_string($conn, $table);
    $rs = @mysqli_query($conn, "SHOW TABLES LIKE '{$safe}'");
    if (!$rs) {
        return false;
    }
    $exists = mysqli_num_rows($rs) > 0;
    mysqli_free_result($rs);
    return $exists;
}

function qk16_detect_columns($conn, $table)
{
    $cols = [];
    if (!qk16_table_exists($conn, $table)) {
        return $cols;
    }
    $rs = @mysqli_query($conn, "SHOW COLUMNS FROM `{$table}`");
    if (!$rs) {
        return $cols;
    }
    while ($row = mysqli_fetch_assoc($rs)) {
        if (isset($row['Field'])) {
            $cols[(string)$row['Field']] = true;
        }
    }
    mysqli_free_result($rs);
    return $cols;
}

function qk16_normalize_path($path)
{
    $path = trim((string)$path);
    if ($path === '') {
        return '';
    }
    if (preg_match('#^https?://#i', $path)) {
        return rtrim($path, '/');
    }
    return strpos($path, '/') === 0 ? $path : '/' . $path;
}

function qk16_build_item_url($siteUrl, $row, $prefix)
{
    $custom = isset($row['custom_url']) ? trim((string)$row['custom_url']) : '';
    if ($custom !== '') {
        return $siteUrl . qk16_normalize_path($custom);
    }
    return $siteUrl . '/' . $prefix . intval($row['id']) . '.html';
}

function qk16_to_timestamp($value)
{
    $value = trim((string)$value);
    if ($value === '') {
        return 0;
    }
    if (is_numeric($value)) {
        return intval($value);
    }
    $ts = strtotime($value);
    return $ts === false ? 0 : intval($ts);
}

function qk16_source_configs()
{
    static $configs = null;
    if ($configs !== null) {
        return $configs;
    }
    $configs = [
        'post' => [
            'table' => 'posts',
            'title_field' => 'title',
            'label' => '文章',
            'url_prefix' => 'post_id',
            'fields' => ['id', 'title', 'custom_url', 'created_at', 'updated_at'],
            'filters' => ['status' => 1],
            'order' => ['updated_at' => 'DESC', 'created_at' => 'DESC', 'id' => 'DESC'],
            'time_fields' => ['updated_at', 'created_at']
        ],
        'goods' => [
            'table' => 'goods',
            'title_field' => 'title',
            'label' => '商品',
            'url_prefix' => 'goods_id',
            'fields' => ['id', 'title', 'custom_url', 'create_time', 'update_time'],
            'filters' => ['status' => 1],
            'order' => ['update_time' => 'DESC', 'create_time' => 'DESC', 'id' => 'DESC'],
            'time_fields' => ['update_time', 'create_time']
        ],
        'category' => [
            'table' => 'categories',
            'title_field' => 'name',
            'label' => '栏目',
            'url_prefix' => 'list_id',
            'fields' => ['id', 'name', 'custom_url', 'created_at', 'updated_at'],
            'filters' => ['status' => 1, 'display' => 1],
            'order' => ['updated_at' => 'DESC', 'created_at' => 'DESC', 'sort' => 'ASC', 'id' => 'DESC'],
            'time_fields' => ['updated_at', 'created_at']
        ]
    ];
    return $configs;
}

function qk16_source_config($source)
{
    $configs = qk16_source_configs();
    return isset($configs[$source]) ? $configs[$source] : null;
}

function qk16_active_sources($source)
{
    return $source === 'all' ? array_keys(qk16_source_configs()) : (qk16_source_config($source) ? [$source] : []);
}

function qk16_sql_fields($cols, $fields)
{
    $list = [];
    foreach ($fields as $field) {
        if (($field === 'id' || isset($cols[$field])) && !isset($list[$field])) {
            $list[$field] = '`' . $field . '`';
        }
    }
    return array_values($list);
}

function qk16_sql_where($cols, $filters)
{
    $where = [];
    foreach ($filters as $field => $value) {
        if (isset($cols[$field])) {
            $where[] = '`' . $field . '` = ' . intval($value);
        }
    }
    return $where;
}

function qk16_sql_order($cols, $orders)
{
    $list = [];
    foreach ($orders as $field => $dir) {
        if (isset($cols[$field])) {
            $list[] = '`' . $field . '` ' . (strtoupper((string)$dir) === 'ASC' ? 'ASC' : 'DESC');
        }
    }
    if (empty($list)) {
        $list[] = '`id` DESC';
    }
    return implode(', ', $list);
}

function qk16_map_item_row($source, $config, $siteUrl, $row)
{
    $updatedAt = '';
    foreach ($config['time_fields'] as $field) {
        if (!empty($row[$field])) {
            $updatedAt = (string)$row[$field];
            break;
        }
    }
    return [
        'key' => $source . ':' . intval($row['id']),
        'type' => $source,
        'type_name' => $config['label'],
        'id' => intval($row['id']),
        'title' => (string)($row[$config['title_field']] ?? ''),
        'url' => qk16_build_item_url($siteUrl, $row, $config['url_prefix']),
        'updated_at' => $updatedAt,
        'sort_value' => qk16_to_timestamp($updatedAt),
        'http_status' => '',
        'check_status' => '待巡检',
        'check_detail' => '',
        'final_url' => ''
    ];
}

function qk16_fetch_source_items($conn, $siteUrl, $source, $limit, $offset = 0, $countOnly = false)
{
    $config = qk16_source_config($source);
    if (!$config || !qk16_table_exists($conn, $config['table'])) {
        return $countOnly ? 0 : [];
    }
    $cols = qk16_detect_columns($conn, $config['table']);
    $where = qk16_sql_where($cols, $config['filters']);
    if ($countOnly) {
        $sql = 'SELECT COUNT(*) AS c FROM `' . $config['table'] . '`';
        if (!empty($where)) {
            $sql .= ' WHERE ' . implode(' AND ', $where);
        }
        $rs = @mysqli_query($conn, $sql);
        if (!$rs) {
            return 0;
        }
        $row = mysqli_fetch_assoc($rs);
        mysqli_free_result($rs);
        return intval($row['c'] ?? 0);
    }
    $fields = qk16_sql_fields($cols, $config['fields']);
    if (empty($fields)) {
        return [];
    }
    $sql = 'SELECT ' . implode(',', $fields) . ' FROM `' . $config['table'] . '`';
    if (!empty($where)) {
        $sql .= ' WHERE ' . implode(' AND ', $where);
    }
    $sql .= ' ORDER BY ' . qk16_sql_order($cols, $config['order']) . ' LIMIT ' . intval($offset) . ',' . intval($limit);
    $rs = @mysqli_query($conn, $sql);
    if (!$rs) {
        return [];
    }
    $rows = [];
    while ($row = mysqli_fetch_assoc($rs)) {
        $rows[] = qk16_map_item_row($source, $config, $siteUrl, $row);
    }
    mysqli_free_result($rs);
    return $rows;
}

function qk16_fetch_items_paged($conn, $siteUrl, $source, $page, $pageSize)
{
    $page = max(1, intval($page));
    $pageSize = intval($pageSize);
    if ($pageSize <= 0) {
        $pageSize = 20;
    }
    if ($pageSize > 100) {
        $pageSize = 100;
    }
    $offset = max(0, ($page - 1) * $pageSize);
    $sources = qk16_active_sources($source);
    if (count($sources) === 1) {
        $sourceKey = $sources[0];
        $total = qk16_fetch_source_items($conn, $siteUrl, $sourceKey, 0, 0, true);
        $items = $total > 0 ? qk16_fetch_source_items($conn, $siteUrl, $sourceKey, $pageSize, $offset) : [];
        return ['total' => $total, 'items' => $items];
    }
    $total = 0;
    foreach ($sources as $sourceKey) {
        $total += qk16_fetch_source_items($conn, $siteUrl, $sourceKey, 0, 0, true);
    }
    if ($total <= 0) {
        return ['total' => 0, 'items' => []];
    }
    $need = min(max($pageSize, $offset + $pageSize), min($total, 5000));
    $items = [];
    foreach ($sources as $sourceKey) {
        $items = array_merge($items, qk16_fetch_source_items($conn, $siteUrl, $sourceKey, $need, 0));
    }
    usort($items, function ($a, $b) {
        return intval($b['sort_value'] ?? 0) <=> intval($a['sort_value'] ?? 0);
    });
    return ['total' => $total, 'items' => array_slice($items, $offset, $pageSize)];
}

function qk16_status_text($status, $error = '')
{
    $status = intval($status);
    if ($status >= 200 && $status < 300) {
        return ['status' => '正常', 'detail' => '页面可正常访问'];
    }
    if ($status >= 300 && $status < 400) {
        return ['status' => '跳转', 'detail' => '页面发生跳转'];
    }
    if ($status === 404) {
        return ['status' => '404', 'detail' => '页面不存在'];
    }
    if ($status >= 400 && $status < 500) {
        return ['status' => '异常', 'detail' => '客户端错误'];
    }
    if ($status >= 500) {
        return ['status' => '异常', 'detail' => '服务器错误'];
    }
    return ['status' => '异常', 'detail' => $error !== '' ? $error : '请求失败'];
}

function qk16_http_probe($url, $timeout = 10)
{
    $url = trim((string)$url);
    if ($url === '') {
        return ['status' => 0, 'effective_url' => '', 'error' => 'empty_url'];
    }
    $timeout = max(1, intval($timeout));
    $headers = [
        'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
        'Accept-Language: zh-CN,zh;q=0.9',
        'Cache-Control: no-cache',
        'Pragma: no-cache'
    ];
    $ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36';
    if (function_exists('curl_init')) {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_NOBODY, true);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout);
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
        curl_setopt($ch, CURLOPT_ENCODING, '');
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        curl_setopt($ch, CURLOPT_USERAGENT, $ua);
        curl_exec($ch);
        $status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $effectiveUrl = (string)curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
        $error = trim((string)curl_error($ch));
        if ($status === 0 || $status === 405) {
            curl_setopt($ch, CURLOPT_NOBODY, false);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
            curl_exec($ch);
            $status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
            $effectiveUrl = (string)curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
            $error = trim((string)curl_error($ch));
        }
        curl_close($ch);
        return [
            'status' => $status,
            'effective_url' => $effectiveUrl === '' ? $url : $effectiveUrl,
            'error' => $error
        ];
    }
    $headersRaw = @get_headers($url, 1);
    $status = 0;
    if (is_array($headersRaw) && !empty($headersRaw[0]) && preg_match('#\s(\d{3})\s#', (string)$headersRaw[0], $m)) {
        $status = intval($m[1]);
    }
    return ['status' => $status, 'effective_url' => $url, 'error' => $status > 0 ? '' : 'request_failed'];
}

function qk16_http_probe_multi($requestMap, $timeout = 10, $concurrency = 6)
{
    $results = [];
    if (!is_array($requestMap) || empty($requestMap)) {
        return $results;
    }
    $timeout = max(1, intval($timeout));
    $concurrency = max(1, intval($concurrency));
    $queue = [];
    foreach ($requestMap as $key => $url) {
        $queue[] = ['key' => (string)$key, 'url' => trim((string)$url)];
    }
    if (!function_exists('curl_multi_init') || !function_exists('curl_init')) {
        foreach ($queue as $item) {
            $results[$item['key']] = qk16_http_probe($item['url'], $timeout);
        }
        return $results;
    }
    $headers = [
        'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
        'Accept-Language: zh-CN,zh;q=0.9',
        'Cache-Control: no-cache',
        'Pragma: no-cache'
    ];
    $ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36';
    $mh = curl_multi_init();
    $handles = [];
    $index = 0;
    $total = count($queue);
    while ($index < $total || !empty($handles)) {
        while ($index < $total && count($handles) < $concurrency) {
            $item = $queue[$index++];
            $ch = curl_init();
            curl_setopt($ch, CURLOPT_URL, $item['url']);
            curl_setopt($ch, CURLOPT_NOBODY, true);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
            curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout);
            curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
            curl_setopt($ch, CURLOPT_ENCODING, '');
            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
            curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
            curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
            curl_setopt($ch, CURLOPT_USERAGENT, $ua);
            curl_multi_add_handle($mh, $ch);
            $handles[(int)$ch] = ['handle' => $ch, 'key' => $item['key'], 'url' => $item['url']];
        }
        do {
            $status = curl_multi_exec($mh, $running);
        } while ($status === CURLM_CALL_MULTI_PERFORM);
        while ($info = curl_multi_info_read($mh)) {
            $ch = $info['handle'];
            $meta = $handles[(int)$ch] ?? null;
            if ($meta) {
                $statusCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
                $effectiveUrl = (string)curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
                $error = trim((string)curl_error($ch));
                if ($statusCode === 0 || $statusCode === 405) {
                    curl_setopt($ch, CURLOPT_NOBODY, false);
                    curl_multi_remove_handle($mh, $ch);
                    curl_multi_add_handle($mh, $ch);
                    do {
                        $status = curl_multi_exec($mh, $running);
                    } while ($status === CURLM_CALL_MULTI_PERFORM);
                    $statusCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
                    $effectiveUrl = (string)curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
                    $error = trim((string)curl_error($ch));
                }
                $results[$meta['key']] = [
                    'status' => $statusCode,
                    'effective_url' => $effectiveUrl === '' ? $meta['url'] : $effectiveUrl,
                    'error' => $error
                ];
                curl_multi_remove_handle($mh, $ch);
                curl_close($ch);
                unset($handles[(int)$ch]);
            }
        }
        if ($running > 0) {
            curl_multi_select($mh, 1.0);
        }
    }
    curl_multi_close($mh);
    foreach ($queue as $item) {
        if (!isset($results[$item['key']])) {
            $results[$item['key']] = qk16_http_probe($item['url'], $timeout);
        }
    }
    return $results;
}

function qk16_check_urls_batch($items, $timeout)
{
    $results = [];
    $requestMap = [];
    foreach ($items as $item) {
        $key = trim((string)($item['key'] ?? ''));
        $url = trim((string)($item['url'] ?? ''));
        if ($key === '' || $url === '') {
            continue;
        }
        $requestMap[$key] = $url;
    }
    $responses = qk16_http_probe_multi($requestMap, $timeout, 6);
    foreach ($items as $item) {
        $key = trim((string)($item['key'] ?? ''));
        $url = trim((string)($item['url'] ?? ''));
        $resp = $responses[$key] ?? ['status' => 0, 'effective_url' => $url, 'error' => 'request_failed'];
        $label = qk16_status_text($resp['status'], (string)($resp['error'] ?? ''));
        $results[$key] = [
            'key' => $key,
            'type_name' => (string)($item['type_name'] ?? ''),
            'id' => (string)($item['id'] ?? ''),
            'title' => (string)($item['title'] ?? ''),
            'url' => $url,
            'http_status' => intval($resp['status']) > 0 ? (string)intval($resp['status']) : '-',
            'check_status' => $label['status'],
            'check_detail' => $label['detail'],
            'final_url' => trim((string)($resp['effective_url'] ?? $url)),
            'updated_at' => (string)($item['updated_at'] ?? '')
        ];
    }
    return $results;
}

if (
    $_SERVER['REQUEST_METHOD'] === 'POST'
    && isset($_POST['action'])
    && isset($_POST['plugin'])
    && (string)$_POST['plugin'] === $plugin_identifier
    && (string)$_POST['action'] !== 'render_tool_html'
) {
    header('Content-Type: application/json; charset=utf-8');
    $root = realpath(dirname(__DIR__, 2));
    $common = $root . '/common.php';
    if (file_exists($common)) {
        require_once $common;
    }
    $fileContent = @file_get_contents(__FILE__);
    $pairs = $fileContent !== false ? qk16_extract_config_params($fileContent) : [];
    $siteUrl = qk16_site_url();
    $action = (string)$_POST['action'];
    if ($action === 'fetch_items') {
        if (!function_exists('get_db_connection')) {
            echo json_encode(['success' => false, 'message' => '缺少系统函数'], JSON_UNESCAPED_UNICODE);
            exit;
        }
        $page = max(1, intval($_POST['page'] ?? 1));
        $pageSize = intval($_POST['page_size'] ?? ($_POST['limit'] ?? ($pairs['default_limit'] ?? 30)));
        if ($pageSize <= 0) {
            $pageSize = 30;
        }
        if ($pageSize > 100) {
            $pageSize = 100;
        }
        $source = trim((string)($_POST['source'] ?? 'all'));
        if (!in_array($source, ['all', 'post', 'goods', 'category'], true)) {
            $source = 'all';
        }
        $conn = get_db_connection();
        if (!$conn || isset($conn->connect_error)) {
            echo json_encode(['success' => false, 'message' => '数据库连接失败'], JSON_UNESCAPED_UNICODE);
            exit;
        }
        $paged = qk16_fetch_items_paged($conn, $siteUrl, $source, $page, $pageSize);
        @mysqli_close($conn);
        $total = intval($paged['total'] ?? 0);
        $items = is_array($paged['items'] ?? null) ? $paged['items'] : [];
        if ($total <= 0) {
            echo json_encode(['success' => false, 'message' => '缺少巡检数据'], JSON_UNESCAPED_UNICODE);
            exit;
        }
        $totalPages = max(1, (int)ceil($total / $pageSize));
        if ($page > $totalPages) {
            $page = $totalPages;
        }
        echo json_encode([
            'success' => true,
            'items' => $items,
            'total' => $total,
            'page' => $page,
            'page_size' => $pageSize,
            'total_pages' => $totalPages
        ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        exit;
    }
    if ($action === 'check_urls') {
        $items = json_decode((string)($_POST['items'] ?? '[]'), true);
        if (!is_array($items) || empty($items)) {
            echo json_encode(['success' => false, 'message' => '请先勾选要检查的URL'], JSON_UNESCAPED_UNICODE);
            exit;
        }
        $timeout = intval($_POST['timeout'] ?? ($pairs['check_timeout'] ?? 10));
        if ($timeout <= 0) {
            $timeout = 10;
        }
        if ($timeout > 20) {
            $timeout = 20;
        }
        $items = array_slice($items, 0, 50);
        echo json_encode([
            'success' => true,
            'results' => array_values(qk16_check_urls_batch($items, $timeout))
        ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        exit;
    }
    echo json_encode(['success' => false, 'message' => '未知操作'], JSON_UNESCAPED_UNICODE);
    exit;
}

$fileContent = @file_get_contents(__FILE__);
$pairs = $fileContent !== false ? qk16_extract_config_params($fileContent) : [];
$defaultLimit = intval($pairs['default_limit'] ?? 30);
if ($defaultLimit <= 0) {
    $defaultLimit = 30;
}
$defaultTimeout = intval($pairs['check_timeout'] ?? 10);
if ($defaultTimeout <= 0) {
    $defaultTimeout = 10;
}
?>

<div class="qk16-wrap">
    <div class="qk16-card">
        <div class="qk16-toolbar">
            <div class="qk16-inline">
                <select class="qk16-input" id="qk16Source">
                    <option value="" selected>请选择范围</option>
                    <option value="all">全部</option>
                    <option value="post">文章</option>
                    <option value="goods">商品</option>
                    <option value="category">栏目</option>
                </select>
            </div>
            <div class="qk16-inline">
                <input class="qk16-input qk16-timeout" id="qk16Timeout" value="<?php echo htmlspecialchars((string)$defaultTimeout, ENT_QUOTES); ?>">
            </div>
            <div class="qk16-actions">
                <button class="qk16-btn qk16-primary" id="qk16CheckSelected">检查选中</button>
                <button class="qk16-btn" id="qk16SaveCfg">保存默认</button>
                <div class="qk16-tip" id="qk16Status">请先选择范围，再勾选需要检查的URL。</div>
            </div>
        </div>
    </div>
    <div class="qk16-card">
        <div class="qk16-table-wrap">
            <table class="qk16-table">
                <thead>
                    <tr>
                        <th class="qk16-col-check"><input type="checkbox" id="qk16CheckAll"></th>
                        <th>类型</th>
                        <th>ID</th>
                        <th>标题</th>
                        <th>URL</th>
                        <th>HTTP</th>
                        <th>检查结果</th>
                        <th>最终地址</th>
                    </tr>
                </thead>
                <tbody id="qk16Tbody"></tbody>
            </table>
        </div>
        <div class="qk16-pagination" id="qk16Pagination"></div>
    </div>
</div>

<style>
    .qk16-wrap {
        font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial;
        color: #0f172a;
        --qk16-border: #e5e7eb;
        --qk16-soft-border: #eef2f7;
        --qk16-muted: #64748b;
        --qk16-card: #ffffff;
        --qk16-primary: #FF9800;
        --qk16-primary-hover: #dd8d16ff;
        --qk16-shadow: 0 10px 30px rgba(15, 23, 42, .06);
    }
    .qk16-card {
        background: var(--qk16-card);
        border: 1px solid var(--qk16-border);
        border-radius: 12px;
        padding: 12px 14px;
        margin-bottom: 12px;
        box-shadow: var(--qk16-shadow);
        box-sizing: border-box;
    }
    .qk16-toolbar {
        display: flex;
        align-items: center;
        gap: 10px;
        flex-wrap: nowrap;
    }
    .qk16-inline {
        display: inline-flex;
        align-items: center;
        gap: 6px;
    }
    .qk16-input {
        height: 32px;
        border: 1px solid var(--qk16-border);
        border-radius: 8px;
        padding: 0 10px;
        font-size: 12px;
        box-sizing: border-box;
        outline: none;
        background: #fff;
        color: #0f172a;
        width: 100%;
    }
    #qk16Source {
        width: 110px;
    }
    .qk16-timeout {
        width: 88px;
    }
    .qk16-input:focus {
        border-color: rgba(37, 99, 235, .65);
        box-shadow: 0 0 0 4px rgba(37, 99, 235, .12);
    }
    .qk16-actions {
        display: flex;
        align-items: center;
        gap: 8px;
        flex: 1;
        min-width: 0;
    }
    .qk16-btn {
        min-width: 96px;
        height: 32px;
        border: 1px solid var(--qk16-border);
        background: #fff;
        color: #111827;
        border-radius: 8px;
        padding: 0 14px;
        font-size: 12px;
        cursor: pointer;
    }
    .qk16-btn:hover {
        background: #f8fafc;
    }
    .qk16-primary {
        background: var(--qk16-primary);
        border-color: var(--qk16-primary);
        color: #fff;
    }
    .qk16-primary:hover {
        background: var(--qk16-primary-hover);
        border-color: var(--qk16-primary-hover);
    }
    .qk16-tip {
        font-size: 12px;
        color: var(--qk16-muted);
        padding: 0 4px;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
        min-width: 180px;
    }
    .qk16-table-wrap {
        overflow: auto;
        border: 1px solid var(--qk16-border);
        border-radius: 12px;
        background: #fff;
        max-height: 350px;
    }
    .qk16-table {
        width: 100%;
        border-collapse: collapse;
        table-layout: fixed;
    }
    .qk16-table th,
    .qk16-table td {
        border-bottom: 1px solid var(--qk16-soft-border);
        padding: 9px 8px;
        font-size: 12px;
        text-align: left;
        vertical-align: top;
        word-break: break-all;
    }
    .qk16-table th {
        color: var(--qk16-muted);
        background: #f8fafc;
        position: sticky;
        top: 0;
        z-index: 1;
    }
    .qk16-col-check {
        width: 32px;
        text-align: center;
    }
    .qk16-table tbody tr:hover td {
        background: #f8fafc;
    }
    .qk16-table input[type="checkbox"] {
        width: 14px;
        height: 14px;
        accent-color: var(--qk16-primary);
        cursor: pointer;
    }
    .qk16-table th:nth-child(2),
    .qk16-table td:nth-child(2) {
        width: 52px;
    }
    .qk16-table th:nth-child(3),
    .qk16-table td:nth-child(3) {
        width: 52px;
    }
    .qk16-table th:nth-child(6),
    .qk16-table td:nth-child(6) {
        width: 58px;
    }
    .qk16-table th:nth-child(7),
    .qk16-table td:nth-child(7) {
        width: 86px;
    }
    .qk16-badge {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        height: 22px;
        border-radius: 999px;
        padding: 0 10px;
        font-size: 12px;
        font-weight: 600;
        white-space: nowrap;
    }
    .qk16-badge-pending {
        background: #f1f5f9;
        color: #475569;
    }
    .qk16-badge-ok {
        background: #dcfce7;
        color: #166534;
    }
    .qk16-badge-redirect {
        background: #fef3c7;
        color: #92400e;
    }
    .qk16-badge-dead {
        background: #fee2e2;
        color: #991b1b;
    }
    .qk16-link {
        color: #0f172a;
        text-decoration: none;
    }
    .qk16-link:hover {
        text-decoration: underline;
    }
    .qk16-pagination {
        display: flex;
        align-items: center;
        justify-content: center;
        gap: 6px;
        padding-top: 10px;
        flex-wrap: wrap;
    }
    .qk16-page-info {
        font-size: 12px;
        color: var(--qk16-muted);
        white-space: nowrap;
    }
    .qk16-page-btn {
        min-width: 38px;
        height: 28px;
        border: 1px solid var(--qk16-border);
        background: #fff;
        color: #111827;
        border-radius: 8px;
        font-size: 12px;
        cursor: pointer;
        padding: 0 10px;
    }
    .qk16-page-btn:hover {
        background: #f1f5f9;
    }
    .qk16-page-btn.qk16-active {
        background: var(--qk16-primary);
        border-color: var(--qk16-primary);
        color: #fff;
    }
    .qk16-page-btn:disabled {
        color: #9ca3af;
        background: #f8fafc;
        cursor: not-allowed;
    }
    .qk16-page-select,
    .qk16-page-size {
        height: 28px;
        border: 1px solid var(--qk16-border);
        border-radius: 8px;
        font-size: 12px;
        padding: 0 24px 0 8px;
        background: #fff;
        color: #111827;
        box-sizing: border-box;
        width: auto;
        appearance: none;
        -moz-appearance: none;
        -webkit-appearance: none;
        background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='none' stroke='%23666' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
        background-repeat: no-repeat;
        background-position: right 6px center;
        background-size: 16px;
    }
    .qk16-btn-loading {
        position: relative;
        pointer-events: none;
        opacity: .88;
    }
    .qk16-btn-loading::before {
        content: '';
        width: 12px;
        height: 12px;
        border: 2px solid rgba(255, 255, 255, .45);
        border-top-color: #fff;
        border-radius: 50%;
        display: inline-block;
        margin-right: 6px;
        vertical-align: -2px;
        animation: qk16spin .8s linear infinite;
    }
    @keyframes qk16spin {
        from {
            transform: rotate(0deg);
        }
        to {
            transform: rotate(360deg);
        }
    }
</style>

<script>
    (function() {
        const plugin = '<?php echo $plugin_identifier; ?>';
        const state = {
            pageRows: [],
            cache: {},
            selected: {},
            source: '',
            currentPage: 1,
            pageSize: <?php echo intval($defaultLimit); ?>,
            total: 0,
            totalPages: 1
        };

        function $(id) {
            return document.getElementById(id);
        }

        function toast(message, type) {
            try {
                if (window.parent && typeof window.parent.showToast === 'function') {
                    window.parent.showToast(message, type === 'success' ? 'success' : 'error', 2400);
                    return;
                }
            } catch (e) {}
            alert(message);
        }

        function setStatus(text) {
            $('qk16Status').textContent = text;
        }

        function escapeHtml(text) {
            return String(text || '')
                .replace(/&/g, '&amp;')
                .replace(/</g, '&lt;')
                .replace(/>/g, '&gt;')
                .replace(/"/g, '&quot;')
                .replace(/'/g, '&#39;');
        }

        function postAction(action, extra) {
            const body = new URLSearchParams(Object.assign({ plugin, action }, extra || {}));
            return fetch('/admin/Plugin.php', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded',
                    'X-Requested-With': 'XMLHttpRequest'
                },
                body
            }).then(function(r) {
                return r.json();
            });
        }

        function normalizePageSize() {
            const el = $('qk16Limit');
            if (!el) {
                state.pageSize = Math.max(1, Math.min(100, parseInt(state.pageSize || '30', 10) || 30));
                return state.pageSize;
            }
            state.pageSize = Math.max(1, Math.min(100, parseInt(el.value || '30', 10) || 30));
            el.value = String(state.pageSize);
            return state.pageSize;
        }

        function normalizeTimeout() {
            const el = $('qk16Timeout');
            const timeout = Math.max(1, Math.min(20, parseInt(el.value || '10', 10) || 10));
            el.value = String(timeout);
            return timeout;
        }

        function urlText(url) {
            const s = String(url || '');
            if (!s) {
                return '';
            }
            try {
                const u = new URL(s);
                return (u.pathname || '/') + (u.search || '') + (u.hash || '');
            } catch (e) {
                const noProto = s.replace(/^https?:\/\/[^/]+/i, '');
                return noProto || s;
            }
        }

        function badgeClass(status) {
            if (status === '正常') return 'qk16-badge qk16-badge-ok';
            if (status === '跳转') return 'qk16-badge qk16-badge-redirect';
            if (status === '404' || status === '异常') return 'qk16-badge qk16-badge-dead';
            return 'qk16-badge qk16-badge-pending';
        }

        function setCheckButtonLoading(loading) {
            const btn = $('qk16CheckSelected');
            if (!btn) return;
            btn.disabled = !!loading;
            btn.classList.toggle('qk16-btn-loading', !!loading);
            btn.textContent = loading ? '检查中...' : '检查选中';
        }

        function rowPayload(row) {
            return {
                key: row.key,
                type_name: row.type_name,
                id: row.id,
                title: row.title,
                url: row.url,
                updated_at: row.updated_at
            };
        }

        function mergeRow(row, redraw) {
            if (!row || !row.key) {
                return null;
            }
            const old = state.cache[row.key] || {};
            const merged = Object.assign({}, old, row);
            merged.check_status = merged.check_status || '待巡检';
            merged.check_detail = merged.check_detail || '';
            merged.http_status = merged.http_status || '';
            merged.final_url = merged.final_url || '';
            state.cache[row.key] = merged;
            if (redraw) {
                state.pageRows = state.pageRows.map(function(item) {
                    return item.key === row.key ? merged : item;
                });
                renderRows();
            }
            return merged;
        }

        function resetList(source) {
            state.source = source || '';
            state.selected = {};
            state.cache = {};
            state.pageRows = [];
            state.currentPage = 1;
            state.total = 0;
            state.totalPages = 1;
            renderRows();
        }

        function renderPagination() {
            const wrap = $('qk16Pagination');
            if (!wrap) return;
            const total = state.total || 0;
            const totalPages = state.totalPages || 1;
            const currentPage = state.currentPage || 1;
            let pageButtons = '';
            for (let i = 1; i <= Math.min(5, totalPages); i++) {
                pageButtons += '<button class="qk16-page-btn ' + (i === currentPage ? 'qk16-active' : '') + '" data-page="' + i + '">' + i + '</button>';
            }
            let pageOptions = '';
            for (let p = 1; p <= totalPages; p++) {
                pageOptions += '<option value="' + p + '"' + (p === currentPage ? ' selected' : '') + '>' + p + '</option>';
            }
            const pageSize = normalizePageSize();
            const sizeOptions = [10, 20, 30, 50, 100].map(function(n) {
                return '<option value="' + n + '"' + (n === pageSize ? ' selected' : '') + '>' + n + '</option>';
            }).join('');
            wrap.innerHTML =
                '<button class="qk16-page-btn" id="qk16PrevPage" ' + (currentPage <= 1 ? 'disabled' : '') + '>上一页</button>' +
                pageButtons +
                '<select class="qk16-page-select" id="qk16PageSelect" ' + (totalPages <= 1 ? 'disabled' : '') + '>' + pageOptions + '</select>' +
                '<button class="qk16-page-btn" id="qk16NextPage" ' + (currentPage >= totalPages ? 'disabled' : '') + '>下一页</button>' +
                '<select class="qk16-page-size" id="qk16Limit">' + sizeOptions + '</select>' +
                '<span class="qk16-page-info">共 ' + total + ' 条，第 ' + currentPage + '/' + totalPages + ' 页</span>';
            const prev = $('qk16PrevPage');
            const next = $('qk16NextPage');
            if (prev) prev.addEventListener('click', function() { if (state.currentPage > 1) fetchPage(state.currentPage - 1); });
            if (next) next.addEventListener('click', function() { if (state.currentPage < state.totalPages) fetchPage(state.currentPage + 1); });
            Array.prototype.slice.call(wrap.querySelectorAll('button[data-page]')).forEach(function(btn) {
                btn.addEventListener('click', function() {
                    fetchPage(parseInt(btn.getAttribute('data-page') || '1', 10) || 1);
                });
            });
            const sel = $('qk16PageSelect');
            if (sel) sel.addEventListener('change', function() { fetchPage(parseInt(sel.value || '1', 10) || 1); });
            const sizeSel = $('qk16Limit');
            if (sizeSel) sizeSel.addEventListener('change', function() { normalizePageSize(); fetchPage(1, true); });
        }

        function renderRows() {
            const tbody = $('qk16Tbody');
            if (!state.pageRows.length) {
                tbody.innerHTML = '';
                $('qk16CheckAll').checked = false;
                renderPagination();
                return;
            }
            tbody.innerHTML = state.pageRows.map(function(row) {
                const checked = state.selected[row.key] ? 'checked' : '';
                const finalText = row.final_url ? '<a class="qk16-link" href="' + escapeHtml(row.final_url) + '" target="_blank">' + escapeHtml(urlText(row.final_url)) + '</a>' : '-';
                return '' +
                    '<tr data-key="' + escapeHtml(row.key) + '">' +
                    '<td class="qk16-col-check"><input type="checkbox" class="qk16-row-check" data-key="' + escapeHtml(row.key) + '" ' + checked + '></td>' +
                    '<td>' + escapeHtml(row.type_name || '') + '</td>' +
                    '<td>' + escapeHtml(row.id || '-') + '</td>' +
                    '<td>' + escapeHtml(row.title || '') + '</td>' +
                    '<td><a class="qk16-link" href="' + escapeHtml(row.url || '#') + '" target="_blank">' + escapeHtml(urlText(row.url || '')) + '</a></td>' +
                    '<td>' + escapeHtml(row.http_status || '-') + '</td>' +
                    '<td><span class="' + badgeClass(row.check_status || '') + '" title="' + escapeHtml(row.check_detail || '') + '">' + escapeHtml(row.check_status || '待巡检') + '</span></td>' +
                    '<td>' + finalText + '</td>' +
                    '</tr>';
            }).join('');
            syncCheckAll();
            renderPagination();
        }

        function syncCheckAll() {
            if (!state.pageRows.length) {
                $('qk16CheckAll').checked = false;
                return;
            }
            $('qk16CheckAll').checked = state.pageRows.every(function(row) {
                return !!state.selected[row.key];
            });
        }

        function loadItems() {
            const source = ($('qk16Source').value || '').trim();
            if (!source) {
                resetList('');
                setStatus('请先选择范围，再勾选需要检查的URL。');
                return;
            }
            normalizePageSize();
            resetList(source);
            fetchPage(1, true);
        }

        function fetchPage(page, resetStatusText) {
            const targetPage = Math.max(1, parseInt(page || '1', 10) || 1);
            setStatus(resetStatusText ? '正在加载巡检列表...' : '正在加载第 ' + targetPage + ' 页...');
            postAction('fetch_items', {
                source: state.source,
                page: String(targetPage),
                page_size: String(normalizePageSize())
            }).then(function(data) {
                if (!data || !data.success) {
                    toast(data && data.message ? data.message : '加载失败', 'error');
                    setStatus('加载失败');
                    return;
                }
                state.total = parseInt(data.total || '0', 10) || 0;
                state.currentPage = parseInt(data.page || String(targetPage), 10) || targetPage;
                state.pageSize = parseInt(data.page_size || String(state.pageSize), 10) || state.pageSize;
                state.totalPages = parseInt(data.total_pages || '1', 10) || 1;
                state.pageRows = (Array.isArray(data.items) ? data.items : []).map(function(row) {
                    return mergeRow(row, false);
                });
                renderRows();
                setStatus('已加载 ' + state.total + ' 条URL，第 ' + state.currentPage + '/' + state.totalPages + ' 页，可翻页后勾选检查。');
            }).catch(function() {
                toast('加载失败', 'error');
                setStatus('加载失败');
            });
        }

        function selectedRows() {
            return Object.keys(state.selected).filter(function(key) {
                return !!state.selected[key];
            }).map(function(key) {
                return state.cache[key];
            }).filter(function(row) {
                return !!row;
            });
        }

        function applyCheckResult(row) {
            mergeRow(row, true);
        }

        function checkSingleRow(row) {
            return postAction('check_urls', {
                timeout: String(normalizeTimeout()),
                items: JSON.stringify([rowPayload(row)])
            }).then(function(data) {
                if (!data || !data.success || !Array.isArray(data.results) || !data.results[0]) {
                    throw new Error(data && data.message ? data.message : '检查失败');
                }
                return data.results[0];
            });
        }

        function checkSelected() {
            let rows = selectedRows();
            if (!rows.length) {
                toast('请先勾选要检查的URL', 'error');
                return;
            }
            if (rows.length > 50) {
                rows = rows.slice(0, 50);
                toast('一次最多检查50条，已只取前50条', 'error');
            }
            setCheckButtonLoading(true);
            let completed = 0;
            let failed = 0;
            let active = 0;
            let index = 0;
            const total = rows.length;
            const concurrency = Math.min(4, total);
            setStatus('正在检查 0/' + total + ' 条URL...');
            function finishOne() {
                completed++;
                setStatus('正在检查 ' + completed + '/' + total + ' 条URL...');
                if (completed >= total) {
                    setCheckButtonLoading(false);
                    if (failed > 0) {
                        setStatus('检查完成，成功 ' + (total - failed) + ' 条，失败 ' + failed + ' 条。');
                        toast('检查完成，部分失败', 'error');
                    } else {
                        setStatus('检查完成，共处理 ' + total + ' 条URL。');
                        toast('检查完成', 'success');
                    }
                    return;
                }
                runNext();
            }
            function runNext() {
                while (active < concurrency && index < total) {
                    const row = rows[index++];
                    active++;
                    checkSingleRow(row).then(function(result) {
                        applyCheckResult(result);
                    }).catch(function() {
                        failed++;
                    }).finally(function() {
                        active--;
                        finishOne();
                    });
                }
            }
            runNext();
        }

        $('qk16SaveCfg').addEventListener('click', function() {
            const settings = {
                default_limit: String(normalizePageSize()),
                check_timeout: String(normalizeTimeout())
            };
            fetch('/admin/Plugin.php', {
                method: 'POST',
                headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                body: 'action=save_plugin_settings&plugin=' + encodeURIComponent(plugin) + '&settings=' + encodeURIComponent(JSON.stringify(settings))
            }).then(function(r) {
                return r.json();
            }).then(function(d) {
                if (d && d.success) {
                    toast('已保存默认', 'success');
                } else {
                    toast(d && d.message ? d.message : '保存失败', 'error');
                }
            }).catch(function() {
                toast('保存失败', 'error');
            });
        });

        document.addEventListener('change', function(e) {
            if (e.target && e.target.classList.contains('qk16-row-check')) {
                state.selected[e.target.getAttribute('data-key') || ''] = !!e.target.checked;
                syncCheckAll();
            }
        });

        $('qk16CheckAll').addEventListener('change', function() {
            const checked = !!this.checked;
            state.pageRows.forEach(function(row) {
                state.selected[row.key] = checked;
            });
            renderRows();
        });

        renderRows();
        $('qk16Source').addEventListener('change', loadItems);
        $('qk16CheckSelected').addEventListener('click', checkSelected);
    })();
</script>
