index.html 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>CSGO 商品比价系统</title>
  7. <style>
  8. * {
  9. margin: 0;
  10. padding: 0;
  11. box-sizing: border-box;
  12. }
  13. body {
  14. font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  15. background: #f5f5f5;
  16. padding: 20px;
  17. }
  18. .container {
  19. max-width: 1400px;
  20. margin: 0 auto;
  21. background: white;
  22. border-radius: 12px;
  23. box-shadow: 0 2px 10px rgba(0,0,0,0.1);
  24. overflow: hidden;
  25. }
  26. .header {
  27. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  28. color: white;
  29. padding: 20px 30px;
  30. }
  31. .header h1 {
  32. font-size: 24px;
  33. margin-bottom: 10px;
  34. }
  35. .controls {
  36. padding: 20px 30px;
  37. background: #f8f9fa;
  38. border-bottom: 1px solid #e9ecef;
  39. display: flex;
  40. gap: 15px;
  41. flex-wrap: wrap;
  42. align-items: flex-end;
  43. }
  44. .control-group {
  45. flex: 1;
  46. min-width: 150px;
  47. }
  48. .control-group label {
  49. display: block;
  50. font-size: 12px;
  51. color: #6c757d;
  52. margin-bottom: 5px;
  53. }
  54. .control-group input,
  55. .control-group select {
  56. width: 100%;
  57. padding: 8px 12px;
  58. border: 1px solid #dee2e6;
  59. border-radius: 6px;
  60. font-size: 14px;
  61. }
  62. .exchange-group {
  63. display: flex;
  64. gap: 10px;
  65. }
  66. .exchange-group input {
  67. flex: 1;
  68. }
  69. .exchange-group button {
  70. padding: 8px 15px;
  71. background: #28a745;
  72. color: white;
  73. border: none;
  74. border-radius: 6px;
  75. cursor: pointer;
  76. }
  77. .exchange-group button:hover {
  78. background: #218838;
  79. }
  80. .stats {
  81. padding: 15px 30px;
  82. background: #e9ecef;
  83. font-size: 14px;
  84. color: #495057;
  85. display: flex;
  86. gap: 20px;
  87. flex-wrap: wrap;
  88. }
  89. .stats span {
  90. font-weight: bold;
  91. color: #667eea;
  92. }
  93. table {
  94. width: 100%;
  95. border-collapse: collapse;
  96. }
  97. th, td {
  98. padding: 12px 15px;
  99. text-align: left;
  100. border-bottom: 1px solid #e9ecef;
  101. }
  102. th {
  103. background: #f8f9fa;
  104. font-weight: 600;
  105. color: #495057;
  106. cursor: pointer;
  107. user-select: none;
  108. }
  109. th:hover {
  110. background: #e9ecef;
  111. }
  112. th.active {
  113. color: #667eea;
  114. }
  115. tr:hover {
  116. background: #f8f9fa;
  117. }
  118. .price {
  119. font-weight: 600;
  120. color: #28a745;
  121. }
  122. .price-missing {
  123. color: #dc3545;
  124. font-size: 12px;
  125. }
  126. .item-name {
  127. font-weight: 500;
  128. max-width: 300px;
  129. word-break: break-word;
  130. }
  131. .item-type {
  132. font-size: 12px;
  133. color: #6c757d;
  134. }
  135. .rarity {
  136. display: inline-block;
  137. padding: 2px 8px;
  138. border-radius: 4px;
  139. font-size: 12px;
  140. background: #e9ecef;
  141. }
  142. .pagination {
  143. padding: 20px 30px;
  144. display: flex;
  145. justify-content: center;
  146. gap: 10px;
  147. }
  148. .pagination button {
  149. padding: 8px 15px;
  150. border: 1px solid #dee2e6;
  151. background: white;
  152. border-radius: 6px;
  153. cursor: pointer;
  154. }
  155. .pagination button:hover:not(:disabled) {
  156. background: #667eea;
  157. color: white;
  158. border-color: #667eea;
  159. }
  160. .pagination button:disabled {
  161. opacity: 0.5;
  162. cursor: not-allowed;
  163. }
  164. .pagination .active {
  165. background: #667eea;
  166. color: white;
  167. border-color: #667eea;
  168. }
  169. .loading {
  170. text-align: center;
  171. padding: 50px;
  172. color: #6c757d;
  173. }
  174. .search-box {
  175. flex: 1;
  176. }
  177. .icon-img {
  178. width: 40px;
  179. height: 40px;
  180. object-fit: cover;
  181. border-radius: 4px;
  182. }
  183. @media (max-width: 768px) {
  184. .controls {
  185. flex-direction: column;
  186. }
  187. th, td {
  188. padding: 8px 10px;
  189. font-size: 12px;
  190. }
  191. }
  192. </style>
  193. </head>
  194. <body>
  195. <div class="container">
  196. <div class="header">
  197. <h1>🎮 CSGO 商品比价系统</h1>
  198. <p>对比 Buff、Steam、CSFloat、DMarket 平台价格</p>
  199. </div>
  200. <div class="controls">
  201. <div class="control-group search-box">
  202. <label>🔍 搜索商品 (market_hash_name)</label>
  203. <input type="text" id="searchInput" placeholder="输入商品名称或哈希名称...">
  204. </div>
  205. <div class="control-group">
  206. <label>📊 排序方式</label>
  207. <select id="sortSelect">
  208. <option value="updated_at">最新更新</option>
  209. <option value="buff_price">Buff 价格</option>
  210. <option value="steam_price">Steam 价格</option>
  211. <option value="csfloat_price">CSFloat 价格</option>
  212. <option value="dmarket_price">DMarket 价格</option>
  213. </select>
  214. </div>
  215. <div class="control-group">
  216. <label>⬆️⬇️ 排序顺序</label>
  217. <select id="orderSelect">
  218. <option value="desc">降序</option>
  219. <option value="asc">升序</option>
  220. </select>
  221. </div>
  222. <div class="control-group exchange-group">
  223. <label>💱 美元汇率 (USD → CNY)</label>
  224. <input type="number" id="exchangeRate" step="0.01" value="7.2">
  225. <button id="setRateBtn">设置</button>
  226. </div>
  227. </div>
  228. <div class="stats" id="stats">
  229. <div>加载中...</div>
  230. </div>
  231. <div style="overflow-x: auto;">
  232. <table id="productTable">
  233. <thead>
  234. <tr>
  235. <th>图标</th>
  236. <th>商品名称 / 类型</th>
  237. <th>稀有度</th>
  238. <th>Buff 价格 (CNY)</th>
  239. <th>Steam 价格 (CNY)</th>
  240. <th>CSFloat 价格 (CNY)</th>
  241. <th>DMarket 价格 (CNY)</th>
  242. <th>更新时间</th>
  243. </tr>
  244. </thead>
  245. <tbody id="tableBody">
  246. <tr><td colspan="8" class="loading">加载中...</td></tr>
  247. </tbody>
  248. </table>
  249. </div>
  250. <div class="pagination" id="pagination"></div>
  251. </div>
  252. <script>
  253. let currentPage = 1;
  254. let totalPages = 1;
  255. let currentRate = 7.2;
  256. function loadProducts() {
  257. const search = document.getElementById('searchInput').value;
  258. const sort = document.getElementById('sortSelect').value;
  259. const order = document.getElementById('orderSelect').value;
  260. fetch(`/api/products?page=${currentPage}&search=${encodeURIComponent(search)}&sort=${sort}&order=${order}`)
  261. .then(res => res.json())
  262. .then(data => {
  263. renderTable(data.products);
  264. renderPagination(data);
  265. currentRate = data.exchange_rate;
  266. document.getElementById('exchangeRate').value = currentRate;
  267. })
  268. .catch(err => console.error('加载失败:', err));
  269. }
  270. function renderTable(products) {
  271. const tbody = document.getElementById('tableBody');
  272. if (!products || products.length === 0) {
  273. tbody.innerHTML = '<tr><td colspan="8" class="loading">暂无数据</td></tr>';
  274. return;
  275. }
  276. tbody.innerHTML = products.map(p => `
  277. <tr>
  278. <td>${p.icon_url ? `<img src="${p.icon_url}" class="icon-img" onerror="this.style.display='none'">` : '-'}</td>
  279. <td class="item-name">
  280. ${escapeHtml(p.name)}
  281. <div class="item-type">${p.type || '未知类型'}</div>
  282. <div style="font-size: 11px; color: #999;">${escapeHtml(p.market_hash_name)}</div>
  283. </td>
  284. <td><span class="rarity">${p.rarity || '普通'}</span></td>
  285. <td class="price">${formatPrice(p.buff_price_cny)}</td>
  286. <td class="price">${formatPrice(p.steam_price_cny)}</td>
  287. <td class="price">${formatPrice(p.csfloat_price_cny)}</td>
  288. <td class="price">${formatPrice(p.dmarket_price_cny)}</td>
  289. <td style="font-size: 12px;">${formatDate(p.updated_at)}</td>
  290. </tr>
  291. `).join('');
  292. }
  293. function formatPrice(price) {
  294. if (!price || price === 0) return '<span class="price-missing">无数据</span>';
  295. return `¥${parseFloat(price).toFixed(2)}`;
  296. }
  297. function formatDate(dateStr) {
  298. if (!dateStr) return '-';
  299. const date = new Date(dateStr);
  300. return date.toLocaleString('zh-CN');
  301. }
  302. function renderPagination(data) {
  303. totalPages = data.total_pages;
  304. const paginationDiv = document.getElementById('pagination');
  305. if (totalPages <= 1) {
  306. paginationDiv.innerHTML = '';
  307. return;
  308. }
  309. let html = '';
  310. html += `<button ${currentPage === 1 ? 'disabled' : ''} onclick="goPage(1)">首页</button>`;
  311. html += `<button ${currentPage === 1 ? 'disabled' : ''} onclick="goPage(${currentPage - 1})">上一页</button>`;
  312. let start = Math.max(1, currentPage - 2);
  313. let end = Math.min(totalPages, currentPage + 2);
  314. for (let i = start; i <= end; i++) {
  315. html += `<button class="${i === currentPage ? 'active' : ''}" onclick="goPage(${i})">${i}</button>`;
  316. }
  317. html += `<button ${currentPage === totalPages ? 'disabled' : ''} onclick="goPage(${currentPage + 1})">下一页</button>`;
  318. html += `<button ${currentPage === totalPages ? 'disabled' : ''} onclick="goPage(${totalPages})">末页</button>`;
  319. paginationDiv.innerHTML = html;
  320. }
  321. function goPage(page) {
  322. currentPage = page;
  323. loadProducts();
  324. }
  325. function loadStatistics() {
  326. fetch('/api/statistics')
  327. .then(res => res.json())
  328. .then(data => {
  329. document.getElementById('stats').innerHTML = `
  330. <div>📦 商品总数: <span>${data.total_products}</span></div>
  331. <div>💰 Buff 有价: <span>${data.buff_count || 0}</span></div>
  332. <div>💰 Steam 有价: <span>${data.steam_count || 0}</span></div>
  333. <div>💰 CSFloat 有价: <span>${data.csfloat_count || 0}</span></div>
  334. <div>💰 DMarket 有价: <span>${data.dmarket_count || 0}</span></div>
  335. `;
  336. });
  337. }
  338. function setExchangeRate() {
  339. const rate = parseFloat(document.getElementById('exchangeRate').value);
  340. if (isNaN(rate) || rate <= 0) {
  341. alert('请输入有效的汇率');
  342. return;
  343. }
  344. fetch('/api/set_exchange_rate', {
  345. method: 'POST',
  346. headers: {'Content-Type': 'application/json'},
  347. body: JSON.stringify({rate: rate})
  348. })
  349. .then(res => res.json())
  350. .then(data => {
  351. if (data.success) {
  352. currentRate = data.rate;
  353. loadProducts();
  354. }
  355. });
  356. }
  357. function escapeHtml(str) {
  358. if (!str) return '';
  359. return str.replace(/[&<>]/g, function(m) {
  360. if (m === '&') return '&amp;';
  361. if (m === '<') return '&lt;';
  362. if (m === '>') return '&gt;';
  363. return m;
  364. });
  365. }
  366. // 事件监听
  367. document.getElementById('searchInput').addEventListener('input', () => {
  368. currentPage = 1;
  369. loadProducts();
  370. });
  371. document.getElementById('sortSelect').addEventListener('change', () => {
  372. currentPage = 1;
  373. loadProducts();
  374. });
  375. document.getElementById('orderSelect').addEventListener('change', () => {
  376. currentPage = 1;
  377. loadProducts();
  378. });
  379. document.getElementById('setRateBtn').addEventListener('click', setExchangeRate);
  380. // 初始加载
  381. loadProducts();
  382. loadStatistics();
  383. // 每5分钟刷新统计
  384. setInterval(loadStatistics, 300000);
  385. </script>
  386. </body>
  387. </html>