index.html 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  1. {% extends 'base.html' %}
  2. {% block title %}期货基础信息 - 期货数据管理系统{% endblock %}
  3. {% block content %}
  4. <div class="container-fluid">
  5. <div class="card">
  6. <div class="card-header">
  7. <!-- 标题和操作按钮 -->
  8. <div class="row mb-0 align-items-center"> <!-- mb-3 removed, header adds padding -->
  9. <div class="col-md-6">
  10. <h2>期货基础信息</h2>
  11. </div>
  12. <div class="col-md-6 text-end">
  13. <button id="updateDataBtn" type="button" class="btn btn-primary btn-sm me-1">
  14. <i class="fas fa-sync-alt"></i> 更新
  15. </button>
  16. <button id="syncMainContractsBtn" type="button" class="btn btn-warning btn-sm me-1">
  17. <i class="fas fa-link"></i> 同步
  18. </button>
  19. <button id="sortMainContract" class="btn btn-info btn-sm me-1">
  20. <i class="fas fa-sort"></i> 不一致
  21. </button>
  22. <a href="{{ url_for('future_info.import_view') }}" class="btn btn-success btn-sm me-1">
  23. <i class="fas fa-file-import"></i> 导入
  24. </a>
  25. <a href="{{ url_for('future_info.add') }}" class="btn btn-primary btn-sm">
  26. <i class="fas fa-plus"></i> 添加
  27. </a>
  28. </div>
  29. </div>
  30. </div>
  31. <div class="card-body">
  32. <!-- 筛选/搜索区域 -->
  33. <form id="searchForm" class="mb-3">
  34. <div class="row g-2 align-items-center justify-content-end">
  35. <div class="col-md-2">
  36. <select class="form-select form-select-sm" name="market_filter" id="marketFilter">
  37. <option value="">所有市场</option>
  38. <option value="0">国内</option>
  39. <option value="1">国外</option>
  40. </select>
  41. </div>
  42. <div class="col-md-3">
  43. <input type="text" class="form-control form-control-sm" placeholder="按长期趋势筛选..." name="trend_filter" id="trendFilter">
  44. </div>
  45. <div class="col-md-4">
  46. <div class="input-group input-group-sm">
  47. <input type="text" class="form-control" placeholder="搜索合约字母或名称..." name="search" id="searchInput">
  48. <button class="btn btn-outline-secondary btn-sm" type="submit">搜索</button>
  49. </div>
  50. </div>
  51. </div>
  52. </form>
  53. <!-- 数据表格 -->
  54. <div class="table-responsive">
  55. <table class="table table-striped table-hover">
  56. <thead>
  57. <tr>
  58. <th>合约字母</th>
  59. <th>名称</th>
  60. <th>市场</th>
  61. <!-- <th>交易所</th> -->
  62. <!-- <th>合约乘数</th> -->
  63. <th>做多保证金率</th>
  64. <th>做空保证金率</th>
  65. <th>同花主力合约</th>
  66. <th>当前主力合约</th>
  67. <th>长期趋势</th>
  68. <th>操作</th>
  69. </tr>
  70. </thead>
  71. <tbody id="futuresTableBody">
  72. <!-- 数据将通过JavaScript动态加载 -->
  73. </tbody>
  74. </table>
  75. </div>
  76. <!-- 分页控件和每页数量选择器 -->
  77. <div class="d-flex justify-content-center align-items-center mt-3">
  78. <nav aria-label="Page navigation" class="me-3">
  79. <ul class="pagination mb-0" id="pagination">
  80. <!-- 分页按钮将通过JavaScript动态加载 -->
  81. </ul>
  82. </nav>
  83. <div class="d-flex align-items-center" id="itemsPerPageContainer" style="display: none;">
  84. <label for="itemsPerPageSelect" class="col-form-label me-2 mb-0">每页:</label>
  85. <select class="form-select form-select-sm" id="itemsPerPageSelect" style="width: auto;">
  86. <option value="10" selected>10</option>
  87. <option value="20">20</option>
  88. <option value="50">50</option>
  89. </select>
  90. </div>
  91. </div>
  92. </div>
  93. </div>
  94. </div>
  95. <!-- Toast容器 -->
  96. <div id="toast-container" class="toast-container position-fixed bottom-0 end-0 p-3"></div>
  97. <!-- 引入 jQuery -->
  98. <script src="https://cdn.staticfile.org/jquery/3.6.0/jquery.min.js"></script>
  99. <!-- 引入 Bootstrap JS (如果尚未在base.html中引入) -->
  100. <!-- <script src="https://cdn.staticfile.org/twitter-bootstrap/5.1.1/js/bootstrap.bundle.min.js"></script> -->
  101. <script>
  102. $(document).ready(function() {
  103. let currentPage = 1;
  104. let currentItemsPerPage = parseInt($('#itemsPerPageSelect').val()) || 10;
  105. let currentSearch = '';
  106. let currentMarketFilter = '';
  107. let currentTrendFilter = '';
  108. let isSorting = false; // 标记是否处于排序状态
  109. // --- 数据加载与渲染 ---
  110. function loadFutures(page = 1, filters = {}) {
  111. currentPage = page;
  112. filters.page = page;
  113. filters.limit = currentItemsPerPage;
  114. filters.search = currentSearch;
  115. filters.market = currentMarketFilter; // 添加市场筛选参数
  116. filters.long_term_trend = currentTrendFilter; // 添加趋势筛选参数
  117. // 添加加载提示
  118. $('#futuresTableBody').html('<tr><td colspan="9" class="text-center">数据加载中...</td></tr>');
  119. $('#pagination').empty();
  120. $('#itemsPerPageContainer').hide();
  121. $.get("{{ url_for('future_info.get_future_info_list') }}", filters, function(response) {
  122. if (response.code === 0) {
  123. renderTable(response.data);
  124. renderPagination(response.count, currentPage, currentItemsPerPage);
  125. if (isSorting) {
  126. applySortingAndHighlighting(); // 如果处于排序状态,重新应用排序和高亮
  127. }
  128. } else {
  129. $('#futuresTableBody').html('<tr><td colspan="9" class="text-center text-danger">加载失败:' + response.msg + '</td></tr>');
  130. }
  131. }).fail(function(jqXHR, textStatus, errorThrown) {
  132. $('#futuresTableBody').html('<tr><td colspan="9" class="text-center text-danger">加载失败:' + textStatus + '</td></tr>');
  133. });
  134. }
  135. function renderTable(data) {
  136. let html = '';
  137. if (data && data.length > 0) {
  138. data.forEach(function(item) {
  139. const longMarginRate = (item.long_margin_rate !== null && item.long_margin_rate !== undefined) ? (item.long_margin_rate * 100).toFixed(2) + '%' : '';
  140. const shortMarginRate = (item.short_margin_rate !== null && item.short_margin_rate !== undefined) ? (item.short_margin_rate * 100).toFixed(2) + '%' : '';
  141. const marketDisplay = item.market === 0 ? '国内' : (item.market === 1 ? '国外' : '未知'); // 转换市场显示
  142. html += `
  143. <tr data-id="${item.id}" data-th-main="${item.th_main_contract || ''}" data-current-main="${item.current_main_contract || ''}">
  144. <td>${item.contract_letter || ''}</td>
  145. <td>${item.name || ''}</td>
  146. <td>${marketDisplay}</td>
  147. <td>${longMarginRate}</td>
  148. <td>${shortMarginRate}</td>
  149. <td>${item.th_main_contract || ''}</td>
  150. <td>${item.current_main_contract || ''}</td>
  151. <td>${item.long_term_trend || ''}</td>
  152. <td>
  153. <div class="btn-group">
  154. <button type="button" class="btn btn-secondary btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
  155. 操作
  156. </button>
  157. <ul class="dropdown-menu">
  158. <li><a class="dropdown-item" href="/api/future_info/detail/${item.id}">查看</a></li>
  159. <li><a class="dropdown-item" href="/api/future_info/edit/${item.id}">编辑</a></li>
  160. <li><hr class="dropdown-divider"></li>
  161. <li><button class="dropdown-item text-danger delete-future" data-id="${item.id}">删除</button></li>
  162. </ul>
  163. </div>
  164. </td>
  165. </tr>
  166. `;
  167. });
  168. } else {
  169. html = '<tr><td colspan="9" class="text-center">暂无期货信息</td></tr>';
  170. }
  171. $('#futuresTableBody').html(html);
  172. }
  173. function renderPagination(totalItems, page, itemsPerPage) {
  174. const totalPages = Math.ceil(totalItems / itemsPerPage);
  175. let paginationHtml = '';
  176. if (totalPages <= 1) {
  177. $('#pagination').empty();
  178. $('#itemsPerPageContainer').hide();
  179. return;
  180. }
  181. $('#itemsPerPageContainer').show();
  182. // 上一页按钮
  183. paginationHtml += `<li class="page-item ${page === 1 ? 'disabled' : ''}">
  184. <a class="page-link" href="#" data-page="${page - 1}" aria-label="Previous">
  185. <span aria-hidden="true">&laquo;</span>
  186. </a>
  187. </li>`;
  188. // 页码按钮 (只显示部分页码)
  189. const maxPagesToShow = 5;
  190. let startPage = Math.max(1, page - Math.floor(maxPagesToShow / 2));
  191. let endPage = Math.min(totalPages, startPage + maxPagesToShow - 1);
  192. if (endPage - startPage + 1 < maxPagesToShow) {
  193. startPage = Math.max(1, endPage - maxPagesToShow + 1);
  194. }
  195. if (startPage > 1) {
  196. paginationHtml += `<li class="page-item"><a class="page-link" href="#" data-page="1">1</a></li>`;
  197. if (startPage > 2) {
  198. paginationHtml += `<li class="page-item disabled"><span class="page-link">...</span></li>`;
  199. }
  200. }
  201. for (let i = startPage; i <= endPage; i++) {
  202. paginationHtml += `<li class="page-item ${i === page ? 'active' : ''}">
  203. <a class="page-link" href="#" data-page="${i}">${i}</a>
  204. </li>`;
  205. }
  206. if (endPage < totalPages) {
  207. if (endPage < totalPages - 1) {
  208. paginationHtml += `<li class="page-item disabled"><span class="page-link">...</span></li>`;
  209. }
  210. paginationHtml += `<li class="page-item"><a class="page-link" href="#" data-page="${totalPages}">${totalPages}</a></li>`;
  211. }
  212. // 下一页按钮
  213. paginationHtml += `<li class="page-item ${page === totalPages ? 'disabled' : ''}">
  214. <a class="page-link" href="#" data-page="${page + 1}" aria-label="Next">
  215. <span aria-hidden="true">&raquo;</span>
  216. </a>
  217. </li>`;
  218. $('#pagination').html(paginationHtml);
  219. }
  220. // --- 事件绑定 ---
  221. // 分页点击
  222. $('#pagination').on('click', 'a.page-link', function(e) {
  223. e.preventDefault();
  224. const page = $(this).data('page');
  225. if (page && page !== currentPage) {
  226. loadFutures(page);
  227. }
  228. });
  229. // 每页数量变化
  230. $('#itemsPerPageSelect').on('change', function() {
  231. currentItemsPerPage = parseInt($(this).val());
  232. loadFutures(1); // 回到第一页
  233. });
  234. // 搜索表单提交 (包括筛选)
  235. $('#searchForm').on('submit', function(e) {
  236. e.preventDefault();
  237. currentSearch = $('#searchInput').val();
  238. currentMarketFilter = $('#marketFilter').val(); // 获取市场筛选值
  239. currentTrendFilter = $('#trendFilter').val(); // 获取趋势筛选值
  240. isSorting = false; // 清除排序状态
  241. loadFutures(1); // 搜索后回到第一页
  242. });
  243. // 删除按钮 (使用事件委托)
  244. $('#futuresTableBody').on('click', '.delete-future', function() {
  245. const futureId = $(this).data('id');
  246. if (confirm('确定要删除此期货信息吗?')) {
  247. $.ajax({
  248. url: `/api/future_info/delete/${futureId}`,
  249. type: 'DELETE',
  250. success: function(response) {
  251. if (response.code === 0) {
  252. showToast('success', '删除成功', '期货信息已删除。');
  253. loadFutures(currentPage); // 重新加载当前页
  254. } else {
  255. showToast('error', '删除失败', response.msg);
  256. }
  257. },
  258. error: function() {
  259. showToast('error', '请求失败', '删除请求发送失败。');
  260. }
  261. });
  262. }
  263. });
  264. // 手动更新数据按钮
  265. const updateDataBtn = $('#updateDataBtn');
  266. let pollInterval;
  267. let pollCount = 0;
  268. const maxPollCount = 60; // 最多轮询60次
  269. updateDataBtn.on('click', function() {
  270. const $btn = $(this);
  271. const icon = $btn.find('i');
  272. $btn.prop('disabled', true);
  273. icon.addClass('fa-spin');
  274. $btn.html(`<i class="fas fa-sync-alt fa-spin"></i> 正在启动更新...`);
  275. $.ajax({
  276. url: "{{ url_for('future_info.update_future_data') }}",
  277. type: 'POST',
  278. contentType: 'application/json',
  279. data: JSON.stringify({ update_mode: 'both' }),
  280. success: function(response) {
  281. if (response.code === 0) {
  282. showToast('info', '更新已启动', response.msg);
  283. checkUpdateStatus();
  284. } else {
  285. showToast('error', '启动更新失败', response.msg);
  286. restoreUpdateBtn();
  287. }
  288. },
  289. error: function() {
  290. showToast('error', '请求失败', '启动更新请求失败');
  291. restoreUpdateBtn();
  292. }
  293. });
  294. });
  295. function checkUpdateStatus() {
  296. pollCount = 0;
  297. if (pollInterval) {
  298. clearInterval(pollInterval);
  299. }
  300. updateDataBtn.html(`<i class="fas fa-sync-alt fa-spin"></i> 数据更新中...`);
  301. pollInterval = setInterval(function() {
  302. pollCount++;
  303. if (pollCount > maxPollCount) {
  304. clearInterval(pollInterval);
  305. showToast('warning', '更新超时', '数据更新超时,请稍后手动刷新页面。');
  306. restoreUpdateBtn();
  307. return;
  308. }
  309. $.get("{{ url_for('future_info.get_update_status') }}", function(response) {
  310. if (response.code === 0 && response.data) {
  311. if (response.data.complete === true) {
  312. clearInterval(pollInterval);
  313. showToast('success', '更新完成', '数据更新成功!页面将自动刷新。');
  314. setTimeout(() => { location.reload(); }, 2000);
  315. } else if (response.data.complete === false) {
  316. clearInterval(pollInterval);
  317. showToast('error', '更新失败', `数据更新失败: ${response.data.error || '未知错误'}`);
  318. restoreUpdateBtn();
  319. }
  320. }
  321. }).fail(function() {
  322. console.error("检查更新状态请求失败");
  323. });
  324. }, 5000);
  325. }
  326. function restoreUpdateBtn() {
  327. updateDataBtn.prop('disabled', false);
  328. updateDataBtn.html(`<i class="fas fa-sync-alt"></i> 手动更新数据`);
  329. }
  330. // 同步主力合约功能
  331. $('#syncMainContractsBtn').on('click', function() {
  332. const $btn = $(this);
  333. const icon = $btn.find('i');
  334. if (!confirm('确定要同步主力合约吗?这将把"同花主力合约"更新为"当前主力合约"。')) {
  335. return;
  336. }
  337. $btn.prop('disabled', true);
  338. icon.addClass('fa-spin');
  339. $btn.html(`<i class="fas fa-link fa-spin"></i> 同步中...`);
  340. $.ajax({
  341. url: "{{ url_for('future_info.sync_main_contracts') }}",
  342. type: 'POST',
  343. contentType: 'application/json',
  344. success: function(response) {
  345. if (response.code === 0) {
  346. showToast('success', '同步成功', response.msg);
  347. loadFutures(currentPage); // 重新加载当前页数据
  348. } else {
  349. showToast('error', '同步失败', response.msg);
  350. }
  351. },
  352. error: function() {
  353. showToast('error', '请求失败', '同步请求发送失败。');
  354. },
  355. complete: function() {
  356. $btn.prop('disabled', false);
  357. $btn.html(`<i class="fas fa-link"></i> 同步`);
  358. }
  359. });
  360. });
  361. // 主力合约排序功能
  362. $('#sortMainContract').on('click', function() {
  363. isSorting = true; // 标记为排序状态
  364. applySortingAndHighlighting();
  365. });
  366. function applySortingAndHighlighting() {
  367. const rows = $('#futuresTableBody tr').get(); // 获取所有行DOM元素
  368. rows.sort((a, b) => {
  369. const aThMain = $(a).data('th-main') || '';
  370. const aCurrentMain = $(a).data('current-main') || '';
  371. const bThMain = $(b).data('th-main') || '';
  372. const bCurrentMain = $(b).data('current-main') || '';
  373. const aNotMatch = aThMain !== aCurrentMain;
  374. const bNotMatch = bThMain !== bCurrentMain;
  375. if (aNotMatch === bNotMatch) return 0;
  376. return aNotMatch ? -1 : 1;
  377. });
  378. const tbody = $('#futuresTableBody');
  379. tbody.empty(); // 清空tbody
  380. $.each(rows, function(index, row) {
  381. const $row = $(row);
  382. const thMain = $row.data('th-main') || '';
  383. const currentMain = $row.data('current-main') || '';
  384. if (thMain !== currentMain) {
  385. $row.addClass('table-warning');
  386. } else {
  387. $row.removeClass('table-warning');
  388. }
  389. tbody.append($row); // 重新添加排序和高亮后的行
  390. });
  391. }
  392. // --- Toast 显示 ---
  393. function showToast(type, title, message) {
  394. const bgClass = type === 'success' ? 'bg-success' : (type === 'error' ? 'bg-danger' : (type === 'warning' ? 'bg-warning' : 'bg-info'));
  395. const toastHtml = `
  396. <div class="toast align-items-center text-white ${bgClass} border-0" role="alert" aria-live="assertive" aria-atomic="true" data-bs-autohide="true" data-bs-delay="5000">
  397. <div class="d-flex">
  398. <div class="toast-body">
  399. <strong>${title}:</strong> ${message}
  400. </div>
  401. <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
  402. </div>
  403. </div>
  404. `;
  405. const $toastElement = $(toastHtml);
  406. $('#toast-container').append($toastElement);
  407. const toast = new bootstrap.Toast($toastElement[0]);
  408. toast.show();
  409. // Optional: Remove the toast from DOM after it's hidden
  410. $toastElement.on('hidden.bs.toast', function () {
  411. $(this).remove();
  412. });
  413. }
  414. // --- 初始加载 ---
  415. loadFutures();
  416. });
  417. </script>
  418. {% endblock %}