batch_genealogy.html 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. {% extends "layout.html" %}
  2. {% block title %}批量处理族谱原文 - 家谱管理系统{% endblock %}
  3. {% block extra_css %}
  4. <style>
  5. .batch-container {
  6. max-width: 1200px;
  7. margin: 0 auto;
  8. }
  9. .member-card {
  10. border: 2px solid #e9ecef;
  11. border-radius: 8px;
  12. padding: 15px;
  13. cursor: pointer;
  14. transition: all 0.2s;
  15. }
  16. .member-card:hover {
  17. border-color: #0d6efd;
  18. box-shadow: 0 2px 8px rgba(13, 110, 253, 0.15);
  19. }
  20. .member-card.selected {
  21. border-color: #0d6efd;
  22. background: #e7f3ff;
  23. }
  24. .batch-actions {
  25. position: sticky;
  26. top: 10px;
  27. z-index: 100;
  28. }
  29. .result-modal-body {
  30. max-height: 60vh;
  31. overflow-y: auto;
  32. }
  33. .success-badge {
  34. background-color: #d1fae5;
  35. color: #065f46;
  36. }
  37. .error-badge {
  38. background-color: #fee2e2;
  39. color: #991b1b;
  40. }
  41. </style>
  42. {% endblock %}
  43. {% block content %}
  44. <div class="batch-container">
  45. <div class="d-flex justify-content-between align-items-center mb-4">
  46. <h2><i class="bi bi-file-text"></i> 批量处理族谱原文</h2>
  47. <div class="btn-group batch-actions">
  48. <button id="selectAllBtn" class="btn btn-outline-primary">全选</button>
  49. <button id="clearSelectionBtn" class="btn btn-outline-secondary">清除选择</button>
  50. <button id="processBtn" class="btn btn-primary" disabled>
  51. <i class="bi bi-cpu"></i> 批量生成族谱原文 (0/10)
  52. </button>
  53. </div>
  54. </div>
  55. <!-- 处理结果弹窗 -->
  56. <div class="modal fade" id="resultModal" tabindex="-1" aria-labelledby="resultModalLabel" aria-hidden="true">
  57. <div class="modal-dialog modal-lg">
  58. <div class="modal-content">
  59. <div class="modal-header">
  60. <h5 class="modal-title" id="resultModalLabel">处理结果</h5>
  61. <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
  62. </div>
  63. <div class="modal-body result-modal-body" id="resultContent">
  64. <div class="text-center py-4">
  65. <div class="spinner-border text-primary" role="status">
  66. <span class="visually-hidden">处理中...</span>
  67. </div>
  68. <p class="mt-2">正在处理,请稍候...</p>
  69. </div>
  70. </div>
  71. <div class="modal-footer">
  72. <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
  73. <button type="button" class="btn btn-primary" id="refreshBtn">刷新列表</button>
  74. </div>
  75. </div>
  76. </div>
  77. </div>
  78. <!-- 成员列表 -->
  79. <div class="row g-3" id="membersContainer">
  80. <div class="col-12 text-center py-8">
  81. <div class="spinner-border text-primary" role="status">
  82. <span class="visually-hidden">加载中...</span>
  83. </div>
  84. <p class="mt-2">加载成员列表中...</p>
  85. </div>
  86. </div>
  87. <!-- 分页 -->
  88. <nav class="mt-4" id="paginationContainer">
  89. </nav>
  90. </div>
  91. {% endblock %}
  92. {% block extra_js %}
  93. <script>
  94. let selectedIds = new Set();
  95. let currentPage = 1;
  96. let totalPages = 1;
  97. document.addEventListener('DOMContentLoaded', function() {
  98. loadMembers(currentPage);
  99. document.getElementById('selectAllBtn').addEventListener('click', selectAll);
  100. document.getElementById('clearSelectionBtn').addEventListener('click', clearSelection);
  101. document.getElementById('processBtn').addEventListener('click', processSelected);
  102. document.getElementById('refreshBtn').addEventListener('click', function() {
  103. clearSelection();
  104. loadMembers(1);
  105. const modal = bootstrap.Modal.getInstance(document.getElementById('resultModal'));
  106. if (modal) modal.hide();
  107. });
  108. });
  109. function loadMembers(page) {
  110. fetch(`/manager/api/members/empty_genealogy?page=${page}&per_page=20`)
  111. .then(response => response.json())
  112. .then(data => {
  113. if (data.success) {
  114. renderMembers(data.members);
  115. renderPagination(data.total, page);
  116. currentPage = page;
  117. } else {
  118. document.getElementById('membersContainer').innerHTML =
  119. '<div class="col-12 text-center py-8"><p class="text-danger">加载失败: ' + data.message + '</p></div>';
  120. }
  121. })
  122. .catch(error => {
  123. document.getElementById('membersContainer').innerHTML =
  124. '<div class="col-12 text-center py-8"><p class="text-danger">加载失败: ' + error.message + '</p></div>';
  125. });
  126. }
  127. function renderMembers(members) {
  128. if (members.length === 0) {
  129. document.getElementById('membersContainer').innerHTML =
  130. '<div class="col-12 text-center py-8"><i class="bi bi-check-circle text-success" style="font-size: 48px;"></i><p class="mt-3 text-success">所有成员的族谱原文都已填写完毕!</p></div>';
  131. return;
  132. }
  133. let html = '';
  134. members.forEach(member => {
  135. const isSelected = selectedIds.has(member.id);
  136. html += `
  137. <div class="col-md-6 col-lg-4">
  138. <div class="member-card ${isSelected ? 'selected' : ''}" data-id="${member.id}" onclick="toggleSelect(${member.id})">
  139. <div class="d-flex justify-content-between align-items-start">
  140. <div>
  141. <h5 class="mb-1">${member.name}</h5>
  142. ${member.simplified_name && member.simplified_name !== member.name ? `<p class="text-sm text-muted">(${member.simplified_name})</p>` : ''}
  143. </div>
  144. <div class="form-check" onclick="event.stopPropagation()">
  145. <input type="checkbox" class="form-check-input" ${isSelected ? 'checked' : ''} onchange="toggleSelect(${member.id})" style="transform: scale(1.2);">
  146. </div>
  147. </div>
  148. <hr class="my-2">
  149. <div class="text-sm text-muted">
  150. ${member.name_word_generation ? `<div><i class="bi bi-tree"></i> ${member.name_word_generation}</div>` : ''}
  151. ${member.father_name ? `<div><i class="bi bi-user"></i> 父:${member.father_name}</div>` : ''}
  152. ${member.mother_name ? `<div><i class="bi bi-user"></i> 母:${member.mother_name}</div>` : ''}
  153. </div>
  154. </div>
  155. </div>
  156. `;
  157. });
  158. document.getElementById('membersContainer').innerHTML = html;
  159. }
  160. function renderPagination(total, page) {
  161. const perPage = 20;
  162. totalPages = Math.ceil(total / perPage);
  163. if (totalPages <= 1) {
  164. document.getElementById('paginationContainer').innerHTML = '';
  165. return;
  166. }
  167. let html = `<ul class="pagination justify-content-center">`;
  168. // 上一页
  169. html += `<li class="page-item ${page === 1 ? 'disabled' : ''}">
  170. <a class="page-link" href="#" onclick="loadMembers(${page - 1})">上一页</a>
  171. </li>`;
  172. // 页码
  173. for (let i = 1; i <= totalPages; i++) {
  174. html += `<li class="page-item ${i === page ? 'active' : ''}">
  175. <a class="page-link" href="#" onclick="loadMembers(${i})">${i}</a>
  176. </li>`;
  177. }
  178. // 下一页
  179. html += `<li class="page-item ${page === totalPages ? 'disabled' : ''}">
  180. <a class="page-link" href="#" onclick="loadMembers(${page + 1})">下一页</a>
  181. </li>`;
  182. html += `</ul>`;
  183. document.getElementById('paginationContainer').innerHTML = html;
  184. }
  185. function toggleSelect(id) {
  186. if (selectedIds.has(id)) {
  187. selectedIds.delete(id);
  188. } else {
  189. if (selectedIds.size >= 10) {
  190. alert('最多选择10个成员进行处理');
  191. return;
  192. }
  193. selectedIds.add(id);
  194. }
  195. updateSelection();
  196. }
  197. function selectAll() {
  198. const cards = document.querySelectorAll('.member-card');
  199. const notSelected = [];
  200. cards.forEach(card => {
  201. const id = parseInt(card.dataset.id);
  202. if (!selectedIds.has(id)) {
  203. notSelected.push(id);
  204. }
  205. });
  206. // 最多选择10个,从未选择的中选
  207. notSelected.slice(0, 10 - selectedIds.size).forEach(id => {
  208. selectedIds.add(id);
  209. });
  210. updateSelection();
  211. }
  212. function clearSelection() {
  213. selectedIds.clear();
  214. updateSelection();
  215. }
  216. function updateSelection() {
  217. // 更新卡片样式
  218. document.querySelectorAll('.member-card').forEach(card => {
  219. const id = parseInt(card.dataset.id);
  220. card.classList.toggle('selected', selectedIds.has(id));
  221. const checkbox = card.querySelector('.form-check-input');
  222. if (checkbox) checkbox.checked = selectedIds.has(id);
  223. });
  224. // 更新按钮状态
  225. const btn = document.getElementById('processBtn');
  226. if (selectedIds.size > 0) {
  227. btn.disabled = false;
  228. btn.innerHTML = `<i class="bi bi-cpu"></i> 批量生成族谱原文 (${selectedIds.size}/10)`;
  229. } else {
  230. btn.disabled = true;
  231. btn.innerHTML = '<i class="bi bi-cpu"></i> 批量生成族谱原文 (0/10)';
  232. }
  233. }
  234. function processSelected() {
  235. const ids = Array.from(selectedIds);
  236. console.log('processSelected called, selected ids:', ids);
  237. if (ids.length === 0) {
  238. alert('请先选择成员');
  239. return;
  240. }
  241. document.getElementById('resultContent').innerHTML = `
  242. <div class="text-center py-4">
  243. <div class="spinner-border text-primary" role="status">
  244. <span class="visually-hidden">处理中...</span>
  245. </div>
  246. <p class="mt-2">正在处理 ${ids.length} 个成员,请稍候...</p>
  247. </div>
  248. `;
  249. const modal = new bootstrap.Modal(document.getElementById('resultModal'));
  250. modal.show();
  251. console.log('Modal shown');
  252. fetch('/manager/api/members/batch_process_genealogy', {
  253. method: 'POST',
  254. headers: {
  255. 'Content-Type': 'application/json'
  256. },
  257. body: JSON.stringify({ member_ids: ids })
  258. })
  259. .then(response => response.json())
  260. .then(data => {
  261. if (data.success) {
  262. let html = '<div class="mb-4"><h6 class="text-primary">处理完成</h6></div>';
  263. html += '<div class="list-group">';
  264. data.results.forEach(result => {
  265. html += `
  266. <div class="list-group-item">
  267. <div class="d-flex justify-content-between align-items-start">
  268. <div>
  269. <strong>${result.name}</strong>
  270. ${result.success ? '' : `<span class="text-muted"> (ID: ${result.member_id})</span>`}
  271. </div>
  272. <span class="badge ${result.success ? 'success-badge' : 'error-badge'}">
  273. ${result.success ? '成功' : '失败'}
  274. </span>
  275. </div>
  276. ${result.success ? `
  277. <div class="mt-2 text-sm">
  278. <div class="mb-1">
  279. <span class="text-muted">繁体:</span>
  280. <span>${result.traditional}</span>
  281. </div>
  282. <div>
  283. <span class="text-muted">简体:</span>
  284. <span>${result.simplified}</span>
  285. </div>
  286. </div>
  287. ` : `
  288. <div class="mt-2 text-sm text-danger">
  289. 原因:${result.message}
  290. </div>
  291. `}
  292. </div>
  293. `;
  294. });
  295. html += '</div>';
  296. document.getElementById('resultContent').innerHTML = html;
  297. } else {
  298. document.getElementById('resultContent').innerHTML =
  299. `<div class="text-center py-4"><p class="text-danger">处理失败: ${data.message}</p></div>`;
  300. }
  301. })
  302. .catch(error => {
  303. document.getElementById('resultContent').innerHTML =
  304. `<div class="text-center py-4"><p class="text-danger">请求失败: ${error.message}</p></div>`;
  305. });
  306. }
  307. </script>
  308. {% endblock %}