|
|
@@ -0,0 +1,452 @@
|
|
|
+<!DOCTYPE html>
|
|
|
+<html lang="zh-CN">
|
|
|
+<head>
|
|
|
+ <meta charset="UTF-8">
|
|
|
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
+ <title>CSGO 商品比价系统</title>
|
|
|
+ <style>
|
|
|
+ * {
|
|
|
+ margin: 0;
|
|
|
+ padding: 0;
|
|
|
+ box-sizing: border-box;
|
|
|
+ }
|
|
|
+
|
|
|
+ body {
|
|
|
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
|
+ background: #f5f5f5;
|
|
|
+ padding: 20px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .container {
|
|
|
+ max-width: 1400px;
|
|
|
+ margin: 0 auto;
|
|
|
+ background: white;
|
|
|
+ border-radius: 12px;
|
|
|
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
|
+ overflow: hidden;
|
|
|
+ }
|
|
|
+
|
|
|
+ .header {
|
|
|
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
+ color: white;
|
|
|
+ padding: 20px 30px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .header h1 {
|
|
|
+ font-size: 24px;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .controls {
|
|
|
+ padding: 20px 30px;
|
|
|
+ background: #f8f9fa;
|
|
|
+ border-bottom: 1px solid #e9ecef;
|
|
|
+ display: flex;
|
|
|
+ gap: 15px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ align-items: flex-end;
|
|
|
+ }
|
|
|
+
|
|
|
+ .control-group {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 150px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .control-group label {
|
|
|
+ display: block;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #6c757d;
|
|
|
+ margin-bottom: 5px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .control-group input,
|
|
|
+ .control-group select {
|
|
|
+ width: 100%;
|
|
|
+ padding: 8px 12px;
|
|
|
+ border: 1px solid #dee2e6;
|
|
|
+ border-radius: 6px;
|
|
|
+ font-size: 14px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .exchange-group {
|
|
|
+ display: flex;
|
|
|
+ gap: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .exchange-group input {
|
|
|
+ flex: 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ .exchange-group button {
|
|
|
+ padding: 8px 15px;
|
|
|
+ background: #28a745;
|
|
|
+ color: white;
|
|
|
+ border: none;
|
|
|
+ border-radius: 6px;
|
|
|
+ cursor: pointer;
|
|
|
+ }
|
|
|
+
|
|
|
+ .exchange-group button:hover {
|
|
|
+ background: #218838;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stats {
|
|
|
+ padding: 15px 30px;
|
|
|
+ background: #e9ecef;
|
|
|
+ font-size: 14px;
|
|
|
+ color: #495057;
|
|
|
+ display: flex;
|
|
|
+ gap: 20px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stats span {
|
|
|
+ font-weight: bold;
|
|
|
+ color: #667eea;
|
|
|
+ }
|
|
|
+
|
|
|
+ table {
|
|
|
+ width: 100%;
|
|
|
+ border-collapse: collapse;
|
|
|
+ }
|
|
|
+
|
|
|
+ th, td {
|
|
|
+ padding: 12px 15px;
|
|
|
+ text-align: left;
|
|
|
+ border-bottom: 1px solid #e9ecef;
|
|
|
+ }
|
|
|
+
|
|
|
+ th {
|
|
|
+ background: #f8f9fa;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #495057;
|
|
|
+ cursor: pointer;
|
|
|
+ user-select: none;
|
|
|
+ }
|
|
|
+
|
|
|
+ th:hover {
|
|
|
+ background: #e9ecef;
|
|
|
+ }
|
|
|
+
|
|
|
+ th.active {
|
|
|
+ color: #667eea;
|
|
|
+ }
|
|
|
+
|
|
|
+ tr:hover {
|
|
|
+ background: #f8f9fa;
|
|
|
+ }
|
|
|
+
|
|
|
+ .price {
|
|
|
+ font-weight: 600;
|
|
|
+ color: #28a745;
|
|
|
+ }
|
|
|
+
|
|
|
+ .price-missing {
|
|
|
+ color: #dc3545;
|
|
|
+ font-size: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .item-name {
|
|
|
+ font-weight: 500;
|
|
|
+ max-width: 300px;
|
|
|
+ word-break: break-word;
|
|
|
+ }
|
|
|
+
|
|
|
+ .item-type {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #6c757d;
|
|
|
+ }
|
|
|
+
|
|
|
+ .rarity {
|
|
|
+ display: inline-block;
|
|
|
+ padding: 2px 8px;
|
|
|
+ border-radius: 4px;
|
|
|
+ font-size: 12px;
|
|
|
+ background: #e9ecef;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pagination {
|
|
|
+ padding: 20px 30px;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pagination button {
|
|
|
+ padding: 8px 15px;
|
|
|
+ border: 1px solid #dee2e6;
|
|
|
+ background: white;
|
|
|
+ border-radius: 6px;
|
|
|
+ cursor: pointer;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pagination button:hover:not(:disabled) {
|
|
|
+ background: #667eea;
|
|
|
+ color: white;
|
|
|
+ border-color: #667eea;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pagination button:disabled {
|
|
|
+ opacity: 0.5;
|
|
|
+ cursor: not-allowed;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pagination .active {
|
|
|
+ background: #667eea;
|
|
|
+ color: white;
|
|
|
+ border-color: #667eea;
|
|
|
+ }
|
|
|
+
|
|
|
+ .loading {
|
|
|
+ text-align: center;
|
|
|
+ padding: 50px;
|
|
|
+ color: #6c757d;
|
|
|
+ }
|
|
|
+
|
|
|
+ .search-box {
|
|
|
+ flex: 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ .icon-img {
|
|
|
+ width: 40px;
|
|
|
+ height: 40px;
|
|
|
+ object-fit: cover;
|
|
|
+ border-radius: 4px;
|
|
|
+ }
|
|
|
+
|
|
|
+ @media (max-width: 768px) {
|
|
|
+ .controls {
|
|
|
+ flex-direction: column;
|
|
|
+ }
|
|
|
+
|
|
|
+ th, td {
|
|
|
+ padding: 8px 10px;
|
|
|
+ font-size: 12px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ </style>
|
|
|
+</head>
|
|
|
+<body>
|
|
|
+ <div class="container">
|
|
|
+ <div class="header">
|
|
|
+ <h1>🎮 CSGO 商品比价系统</h1>
|
|
|
+ <p>对比 Buff、Steam、CSFloat、DMarket 平台价格</p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="controls">
|
|
|
+ <div class="control-group search-box">
|
|
|
+ <label>🔍 搜索商品 (market_hash_name)</label>
|
|
|
+ <input type="text" id="searchInput" placeholder="输入商品名称或哈希名称...">
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="control-group">
|
|
|
+ <label>📊 排序方式</label>
|
|
|
+ <select id="sortSelect">
|
|
|
+ <option value="updated_at">最新更新</option>
|
|
|
+ <option value="buff_price">Buff 价格</option>
|
|
|
+ <option value="steam_price">Steam 价格</option>
|
|
|
+ <option value="csfloat_price">CSFloat 价格</option>
|
|
|
+ <option value="dmarket_price">DMarket 价格</option>
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="control-group">
|
|
|
+ <label>⬆️⬇️ 排序顺序</label>
|
|
|
+ <select id="orderSelect">
|
|
|
+ <option value="desc">降序</option>
|
|
|
+ <option value="asc">升序</option>
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="control-group exchange-group">
|
|
|
+ <label>💱 美元汇率 (USD → CNY)</label>
|
|
|
+ <input type="number" id="exchangeRate" step="0.01" value="7.2">
|
|
|
+ <button id="setRateBtn">设置</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="stats" id="stats">
|
|
|
+ <div>加载中...</div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div style="overflow-x: auto;">
|
|
|
+ <table id="productTable">
|
|
|
+ <thead>
|
|
|
+ <tr>
|
|
|
+ <th>图标</th>
|
|
|
+ <th>商品名称 / 类型</th>
|
|
|
+ <th>稀有度</th>
|
|
|
+ <th>Buff 价格 (CNY)</th>
|
|
|
+ <th>Steam 价格 (CNY)</th>
|
|
|
+ <th>CSFloat 价格 (CNY)</th>
|
|
|
+ <th>DMarket 价格 (CNY)</th>
|
|
|
+ <th>更新时间</th>
|
|
|
+ </tr>
|
|
|
+ </thead>
|
|
|
+ <tbody id="tableBody">
|
|
|
+ <tr><td colspan="8" class="loading">加载中...</td></tr>
|
|
|
+ </tbody>
|
|
|
+ </table>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="pagination" id="pagination"></div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <script>
|
|
|
+ let currentPage = 1;
|
|
|
+ let totalPages = 1;
|
|
|
+ let currentRate = 7.2;
|
|
|
+
|
|
|
+ function loadProducts() {
|
|
|
+ const search = document.getElementById('searchInput').value;
|
|
|
+ const sort = document.getElementById('sortSelect').value;
|
|
|
+ const order = document.getElementById('orderSelect').value;
|
|
|
+
|
|
|
+ fetch(`/api/products?page=${currentPage}&search=${encodeURIComponent(search)}&sort=${sort}&order=${order}`)
|
|
|
+ .then(res => res.json())
|
|
|
+ .then(data => {
|
|
|
+ renderTable(data.products);
|
|
|
+ renderPagination(data);
|
|
|
+ currentRate = data.exchange_rate;
|
|
|
+ document.getElementById('exchangeRate').value = currentRate;
|
|
|
+ })
|
|
|
+ .catch(err => console.error('加载失败:', err));
|
|
|
+ }
|
|
|
+
|
|
|
+ function renderTable(products) {
|
|
|
+ const tbody = document.getElementById('tableBody');
|
|
|
+
|
|
|
+ if (!products || products.length === 0) {
|
|
|
+ tbody.innerHTML = '<tr><td colspan="8" class="loading">暂无数据</td></tr>';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ tbody.innerHTML = products.map(p => `
|
|
|
+ <tr>
|
|
|
+ <td>${p.icon_url ? `<img src="${p.icon_url}" class="icon-img" onerror="this.style.display='none'">` : '-'}</td>
|
|
|
+ <td class="item-name">
|
|
|
+ ${escapeHtml(p.name)}
|
|
|
+ <div class="item-type">${p.type || '未知类型'}</div>
|
|
|
+ <div style="font-size: 11px; color: #999;">${escapeHtml(p.market_hash_name)}</div>
|
|
|
+ </td>
|
|
|
+ <td><span class="rarity">${p.rarity || '普通'}</span></td>
|
|
|
+ <td class="price">${formatPrice(p.buff_price_cny)}</td>
|
|
|
+ <td class="price">${formatPrice(p.steam_price_cny)}</td>
|
|
|
+ <td class="price">${formatPrice(p.csfloat_price_cny)}</td>
|
|
|
+ <td class="price">${formatPrice(p.dmarket_price_cny)}</td>
|
|
|
+ <td style="font-size: 12px;">${formatDate(p.updated_at)}</td>
|
|
|
+ </tr>
|
|
|
+ `).join('');
|
|
|
+ }
|
|
|
+
|
|
|
+ function formatPrice(price) {
|
|
|
+ if (!price || price === 0) return '<span class="price-missing">无数据</span>';
|
|
|
+ return `¥${parseFloat(price).toFixed(2)}`;
|
|
|
+ }
|
|
|
+
|
|
|
+ function formatDate(dateStr) {
|
|
|
+ if (!dateStr) return '-';
|
|
|
+ const date = new Date(dateStr);
|
|
|
+ return date.toLocaleString('zh-CN');
|
|
|
+ }
|
|
|
+
|
|
|
+ function renderPagination(data) {
|
|
|
+ totalPages = data.total_pages;
|
|
|
+ const paginationDiv = document.getElementById('pagination');
|
|
|
+
|
|
|
+ if (totalPages <= 1) {
|
|
|
+ paginationDiv.innerHTML = '';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ let html = '';
|
|
|
+ html += `<button ${currentPage === 1 ? 'disabled' : ''} onclick="goPage(1)">首页</button>`;
|
|
|
+ html += `<button ${currentPage === 1 ? 'disabled' : ''} onclick="goPage(${currentPage - 1})">上一页</button>`;
|
|
|
+
|
|
|
+ let start = Math.max(1, currentPage - 2);
|
|
|
+ let end = Math.min(totalPages, currentPage + 2);
|
|
|
+
|
|
|
+ for (let i = start; i <= end; i++) {
|
|
|
+ html += `<button class="${i === currentPage ? 'active' : ''}" onclick="goPage(${i})">${i}</button>`;
|
|
|
+ }
|
|
|
+
|
|
|
+ html += `<button ${currentPage === totalPages ? 'disabled' : ''} onclick="goPage(${currentPage + 1})">下一页</button>`;
|
|
|
+ html += `<button ${currentPage === totalPages ? 'disabled' : ''} onclick="goPage(${totalPages})">末页</button>`;
|
|
|
+
|
|
|
+ paginationDiv.innerHTML = html;
|
|
|
+ }
|
|
|
+
|
|
|
+ function goPage(page) {
|
|
|
+ currentPage = page;
|
|
|
+ loadProducts();
|
|
|
+ }
|
|
|
+
|
|
|
+ function loadStatistics() {
|
|
|
+ fetch('/api/statistics')
|
|
|
+ .then(res => res.json())
|
|
|
+ .then(data => {
|
|
|
+ document.getElementById('stats').innerHTML = `
|
|
|
+ <div>📦 商品总数: <span>${data.total_products}</span></div>
|
|
|
+ <div>💰 Buff 有价: <span>${data.buff_count || 0}</span></div>
|
|
|
+ <div>💰 Steam 有价: <span>${data.steam_count || 0}</span></div>
|
|
|
+ <div>💰 CSFloat 有价: <span>${data.csfloat_count || 0}</span></div>
|
|
|
+ <div>💰 DMarket 有价: <span>${data.dmarket_count || 0}</span></div>
|
|
|
+ `;
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ function setExchangeRate() {
|
|
|
+ const rate = parseFloat(document.getElementById('exchangeRate').value);
|
|
|
+ if (isNaN(rate) || rate <= 0) {
|
|
|
+ alert('请输入有效的汇率');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ fetch('/api/set_exchange_rate', {
|
|
|
+ method: 'POST',
|
|
|
+ headers: {'Content-Type': 'application/json'},
|
|
|
+ body: JSON.stringify({rate: rate})
|
|
|
+ })
|
|
|
+ .then(res => res.json())
|
|
|
+ .then(data => {
|
|
|
+ if (data.success) {
|
|
|
+ currentRate = data.rate;
|
|
|
+ loadProducts();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ function escapeHtml(str) {
|
|
|
+ if (!str) return '';
|
|
|
+ return str.replace(/[&<>]/g, function(m) {
|
|
|
+ if (m === '&') return '&';
|
|
|
+ if (m === '<') return '<';
|
|
|
+ if (m === '>') return '>';
|
|
|
+ return m;
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 事件监听
|
|
|
+ document.getElementById('searchInput').addEventListener('input', () => {
|
|
|
+ currentPage = 1;
|
|
|
+ loadProducts();
|
|
|
+ });
|
|
|
+ document.getElementById('sortSelect').addEventListener('change', () => {
|
|
|
+ currentPage = 1;
|
|
|
+ loadProducts();
|
|
|
+ });
|
|
|
+ document.getElementById('orderSelect').addEventListener('change', () => {
|
|
|
+ currentPage = 1;
|
|
|
+ loadProducts();
|
|
|
+ });
|
|
|
+ document.getElementById('setRateBtn').addEventListener('click', setExchangeRate);
|
|
|
+
|
|
|
+ // 初始加载
|
|
|
+ loadProducts();
|
|
|
+ loadStatistics();
|
|
|
+
|
|
|
+ // 每5分钟刷新统计
|
|
|
+ setInterval(loadStatistics, 300000);
|
|
|
+ </script>
|
|
|
+</body>
|
|
|
+</html>
|