pdf_management.html 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476
  1. {% extends "layout.html" %}
  2. {% block title %}家谱管理 - 家谱管理系统{% endblock %}
  3. {% block extra_css %}
  4. <style>
  5. .pdf-card {
  6. cursor: pointer;
  7. transition: all 0.3s ease;
  8. border: 2px solid transparent;
  9. border-radius: 12px;
  10. overflow: hidden;
  11. background-color: #ffffff;
  12. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
  13. min-height: 280px;
  14. display: flex;
  15. flex-direction: column;
  16. }
  17. .pdf-card:hover {
  18. border-color: #0d6efd;
  19. box-shadow: 0 6px 20px rgba(13, 110, 253, 0.2);
  20. transform: translateY(-3px);
  21. }
  22. .pdf-card.active {
  23. border-color: #0d6efd;
  24. background-color: #f0f6ff;
  25. box-shadow: 0 4px 16px rgba(13, 110, 253, 0.18);
  26. }
  27. .pdf-card .card-body {
  28. padding: 16px;
  29. flex: 1;
  30. display: flex;
  31. flex-direction: column;
  32. }
  33. .pdf-card-icon {
  34. font-size: 1.8rem;
  35. color: #dc3545;
  36. margin-top: 2px;
  37. }
  38. .pdf-card-title {
  39. font-size: 0.9rem;
  40. font-weight: 600;
  41. line-height: 1.3;
  42. margin-bottom: 10px;
  43. color: #333333;
  44. flex-shrink: 0;
  45. }
  46. .pdf-card-meta {
  47. font-size: 0.75rem;
  48. color: #6c757d;
  49. line-height: 1.4;
  50. margin-bottom: 12px;
  51. flex-grow: 1;
  52. }
  53. .pdf-card-meta-item {
  54. display: flex;
  55. align-items: center;
  56. margin-bottom: 4px;
  57. }
  58. .pdf-card-meta-item i {
  59. font-size: 0.65rem;
  60. margin-right: 6px;
  61. width: 14px;
  62. text-align: center;
  63. color: #adb5bd;
  64. }
  65. .pdf-viewer-wrapper {
  66. background: #f8f9fa;
  67. border-radius: 12px;
  68. overflow: hidden;
  69. min-height: 80vh;
  70. position: relative;
  71. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  72. }
  73. .pdf-viewer-wrapper iframe,
  74. .pdf-viewer-wrapper embed {
  75. width: 100%;
  76. min-height: 80vh;
  77. border: none;
  78. }
  79. .pdf-list-scroll {
  80. max-height: 600px;
  81. overflow-y: auto;
  82. padding-right: 8px;
  83. }
  84. .pdf-list-scroll::-webkit-scrollbar {
  85. width: 6px;
  86. }
  87. .pdf-list-scroll::-webkit-scrollbar-track {
  88. background: #f1f1f1;
  89. border-radius: 3px;
  90. }
  91. .pdf-list-scroll::-webkit-scrollbar-thumb {
  92. background: #c1c1c1;
  93. border-radius: 3px;
  94. }
  95. .pdf-list-scroll::-webkit-scrollbar-thumb:hover {
  96. background: #a1a1a1;
  97. }
  98. .pdf-detail-meta {
  99. display: flex;
  100. flex-wrap: wrap;
  101. gap: 20px;
  102. margin-top: 12px;
  103. padding-top: 16px;
  104. border-top: 1px solid #e9ecef;
  105. }
  106. .pdf-detail-meta-item {
  107. display: flex;
  108. align-items: center;
  109. font-size: 0.9rem;
  110. }
  111. .pdf-detail-meta-item i {
  112. color: #6c757d;
  113. margin-right: 10px;
  114. font-size: 1rem;
  115. }
  116. .card {
  117. border-radius: 12px;
  118. overflow: hidden;
  119. }
  120. .card-header {
  121. border-bottom: none;
  122. background-color: #ffffff;
  123. }
  124. .card-body {
  125. padding: 24px;
  126. }
  127. .btn {
  128. border-radius: 8px;
  129. font-weight: 500;
  130. }
  131. .btn-sm {
  132. padding: 6px 12px;
  133. font-size: 0.875rem;
  134. }
  135. .badge {
  136. font-weight: 500;
  137. font-size: 0.75rem;
  138. }
  139. .loading-overlay {
  140. position: absolute;
  141. top: 0;
  142. left: 0;
  143. width: 100%;
  144. height: 100%;
  145. background-color: rgba(255, 255, 255, 0.95);
  146. display: flex;
  147. flex-direction: column;
  148. justify-content: center;
  149. align-items: center;
  150. z-index: 1000;
  151. transition: opacity 0.3s ease;
  152. }
  153. .loading-spinner {
  154. width: 50px;
  155. height: 50px;
  156. border: 5px solid #f3f3f3;
  157. border-top: 5px solid #0d6efd;
  158. border-radius: 50%;
  159. animation: spin 1s linear infinite;
  160. margin-bottom: 16px;
  161. }
  162. .loading-text {
  163. font-size: 1rem;
  164. color: #6c757d;
  165. text-align: center;
  166. }
  167. @keyframes spin {
  168. 0% { transform: rotate(0deg); }
  169. 100% { transform: rotate(360deg); }
  170. }
  171. .loading-progress {
  172. width: 80%;
  173. max-width: 400px;
  174. height: 8px;
  175. background-color: #e9ecef;
  176. border-radius: 4px;
  177. overflow: hidden;
  178. margin: 16px 0;
  179. }
  180. .loading-progress-bar {
  181. height: 100%;
  182. background-color: #0d6efd;
  183. border-radius: 4px;
  184. width: 0%;
  185. transition: width 0.3s ease;
  186. }
  187. </style>
  188. {% endblock %}
  189. {% block content %}
  190. <div class="d-flex justify-content-between align-items-center mb-4">
  191. <h2><i class="bi bi-book"></i> 家谱管理</h2>
  192. <a href="{{ url_for('upload_pdf') }}" class="btn btn-primary">
  193. <i class="bi bi-cloud-upload me-2"></i> 上传家谱
  194. </a>
  195. </div>
  196. {% if pdfs %}
  197. <div class="card shadow-sm mb-4">
  198. <div class="card-header bg-white py-2">
  199. <span class="fw-bold small text-muted">已上传家谱文件({{ pdfs|length }} 个)</span>
  200. </div>
  201. <div class="card-body p-2 pdf-list-scroll">
  202. <div class="row g-4">
  203. {% for pdf in pdfs %}
  204. <div class="col-md-4 col-sm-6">
  205. <div class="card pdf-card {{ 'active' if selected_pdf and selected_pdf.id == pdf.id }}"
  206. onclick="window.location.href='{{ url_for('pdf_management', view=pdf.id) }}'>
  207. <div class="position-relative">
  208. <div class="position-absolute top-2 right-2 z-10">
  209. {% if pdf.parse_status == 2 %}
  210. <span class="badge bg-success rounded-full p-2">
  211. <i class="bi bi-check-circle"></i> 解析成功
  212. </span>
  213. {% elif pdf.parse_status == 1 %}
  214. <span class="badge bg-warning text-dark rounded-full p-2">
  215. <i class="bi bi-hourglass-split"></i> 解析中
  216. </span>
  217. {% elif pdf.parse_status == 3 %}
  218. <span class="badge bg-danger rounded-full p-2">
  219. <i class="bi bi-x-circle"></i> 解析失败
  220. </span>
  221. {% else %}
  222. <span class="badge bg-secondary rounded-full p-2">
  223. <i class="bi bi-robot"></i> 未解析
  224. </span>
  225. {% endif %}
  226. </div>
  227. <div class="card-body">
  228. <div class="d-flex align-items-start">
  229. <i class="bi bi-file-earmark-pdf pdf-card-icon me-3 flex-shrink-0"></i>
  230. <div class="flex-grow-1">
  231. <div class="pdf-card-title text-truncate" title="{{ pdf.file_name }}">{{ pdf.file_name }}</div>
  232. <div class="pdf-card-meta">
  233. {% if pdf.version_name %}
  234. <div class="pdf-card-meta-item">
  235. <i class="bi bi-tag"></i>
  236. <span>{{ pdf.version_name }}</span>
  237. </div>
  238. {% endif %}
  239. {% if pdf.version_source %}
  240. <div class="pdf-card-meta-item">
  241. <i class="bi bi-building"></i>
  242. <span>{{ pdf.version_source }}</span>
  243. </div>
  244. {% endif %}
  245. {% if pdf.file_provider %}
  246. <div class="pdf-card-meta-item">
  247. <i class="bi bi-person"></i>
  248. <span>{{ pdf.file_provider }}</span>
  249. </div>
  250. {% endif %}
  251. <div class="pdf-card-meta-item">
  252. <i class="bi bi-calendar"></i>
  253. <span>{{ pdf.upload_time.strftime('%Y-%m-%d') if pdf.upload_time else '未知' }}</span>
  254. </div>
  255. </div>
  256. <div class="mt-3 d-grid gap-2">
  257. <button onclick="previewPDF({{ pdf.id }})" class="btn btn-sm btn-outline-info w-100" id="previewBtn{{ pdf.id }}">
  258. <i class="bi bi-eye"></i> 预览
  259. </button>
  260. {% if pdf.parse_status == 2 %}
  261. <button onclick="parsePDF({{ pdf.id }})" class="btn btn-sm btn-outline-warning w-100" id="parseBtn{{ pdf.id }}">
  262. <i class="bi bi-arrow-repeat"></i> 重新解析
  263. </button>
  264. {% elif pdf.parse_status != 1 %}
  265. <button onclick="parsePDF({{ pdf.id }})" class="btn btn-sm btn-outline-primary w-100" id="parseBtn{{ pdf.id }}">
  266. <i class="bi bi-robot"></i> AI解析
  267. </button>
  268. {% else %}
  269. <button class="btn btn-sm btn-outline-primary w-100 disabled">
  270. <span class="spinner-border spinner-border-sm"></span> 解析中...
  271. </button>
  272. {% endif %}
  273. </div>
  274. </div>
  275. </div>
  276. </div>
  277. </div>
  278. </div>
  279. {% endfor %}
  280. </div>
  281. </div>
  282. </div>
  283. {% endif %}
  284. {% if selected_pdf %}
  285. <div class="card shadow-sm">
  286. <div class="card-header bg-white py-3">
  287. <div class="d-flex align-items-center mb-2">
  288. <i class="bi bi-file-earmark-pdf text-danger me-3 fs-5"></i>
  289. <h5 class="mb-0">{{ selected_pdf.file_name }}</h5>
  290. </div>
  291. <div class="pdf-detail-meta">
  292. {% if selected_pdf.version_name %}
  293. <div class="pdf-detail-meta-item">
  294. <i class="bi bi-tag"></i>
  295. <span><strong>版本名称:</strong>{{ selected_pdf.version_name }}</span>
  296. </div>
  297. {% endif %}
  298. {% if selected_pdf.version_source %}
  299. <div class="pdf-detail-meta-item">
  300. <i class="bi bi-building"></i>
  301. <span><strong>版本来源:</strong>{{ selected_pdf.version_source }}</span>
  302. </div>
  303. {% endif %}
  304. {% if selected_pdf.file_provider %}
  305. <div class="pdf-detail-meta-item">
  306. <i class="bi bi-person"></i>
  307. <span><strong>文件提供人:</strong>{{ selected_pdf.file_provider }}</span>
  308. </div>
  309. {% endif %}
  310. <div class="pdf-detail-meta-item">
  311. <i class="bi bi-calendar"></i>
  312. <span><strong>上传时间:</strong>{{ selected_pdf.upload_time.strftime('%Y-%m-%d %H:%M') if selected_pdf.upload_time else '未知' }}</span>
  313. </div>
  314. </div>
  315. <div class="d-flex gap-2 mt-3 justify-content-end">
  316. <form action="{{ url_for('delete_pdf', pdf_id=selected_pdf.id) }}" method="POST" class="d-inline"
  317. onsubmit="return confirm('确定要删除此PDF文件吗?此操作无法撤销。');">
  318. <button type="submit" class="btn btn-sm btn-outline-danger">
  319. <i class="bi bi-trash"></i> 删除
  320. </button>
  321. </form>
  322. </div>
  323. </div>
  324. <div class="card-body p-0">
  325. <div class="pdf-viewer-wrapper">
  326. <div id="loadingOverlay" class="loading-overlay">
  327. <div class="loading-spinner"></div>
  328. <div class="loading-text">正在加载PDF文件,请稍候...</div>
  329. <div class="loading-progress">
  330. <div class="loading-progress-bar" id="loadingProgressBar"></div>
  331. </div>
  332. </div>
  333. <iframe id="pdfViewer" src="{{ selected_pdf.oss_url }}#toolbar=0" type="application/pdf" onload="hideLoading()"></iframe>
  334. </div>
  335. </div>
  336. </div>
  337. {% elif not pdfs %}
  338. <div class="card shadow-sm">
  339. <div class="card-body text-center py-5 text-muted">
  340. <i class="bi bi-file-earmark-pdf fs-1 d-block mb-3 text-secondary"></i>
  341. <h5 class="text-secondary">暂无PDF家谱文件</h5>
  342. <p class="mb-3">在扫描件管理中上传PDF文件后,会自动添加到此处。</p>
  343. </div>
  344. </div>
  345. {% else %}
  346. <div class="card shadow-sm">
  347. <div class="card-body text-center py-5 text-muted">
  348. <i class="bi bi-hand-index-thumb fs-1 d-block mb-3"></i>
  349. <h5 class="text-secondary">请从上方选择一个PDF文件进行查看</h5>
  350. </div>
  351. </div>
  352. {% endif %}
  353. {% endblock %}
  354. {% block extra_js %}
  355. <script>
  356. // 显示加载动画
  357. function showLoading() {
  358. document.getElementById('loadingOverlay').style.display = 'flex';
  359. }
  360. // 隐藏加载动画
  361. function hideLoading() {
  362. const overlay = document.getElementById('loadingOverlay');
  363. overlay.style.opacity = '0';
  364. setTimeout(() => {
  365. overlay.style.display = 'none';
  366. }, 300);
  367. }
  368. // 模拟加载进度
  369. function simulateLoadingProgress() {
  370. const progressBar = document.getElementById('loadingProgressBar');
  371. if (progressBar) {
  372. let progress = 0;
  373. const interval = setInterval(() => {
  374. progress += 5;
  375. if (progress > 90) {
  376. clearInterval(interval);
  377. } else {
  378. progressBar.style.width = progress + '%';
  379. }
  380. }, 200);
  381. }
  382. }
  383. // 页面加载完成后开始模拟加载进度
  384. document.addEventListener('DOMContentLoaded', function() {
  385. // 检查是否有选中的PDF
  386. if (document.getElementById('pdfViewer')) {
  387. simulateLoadingProgress();
  388. }
  389. });
  390. // 监听PDF卡片点击事件,显示加载动画
  391. document.querySelectorAll('.pdf-card').forEach(card => {
  392. card.addEventListener('click', function() {
  393. // 显示加载动画
  394. const overlay = document.createElement('div');
  395. overlay.id = 'loadingOverlay';
  396. overlay.className = 'loading-overlay';
  397. overlay.innerHTML = `
  398. <div class="loading-spinner"></div>
  399. <div class="loading-text">正在加载PDF文件,请稍候...</div>
  400. <div class="loading-progress">
  401. <div class="loading-progress-bar" id="loadingProgressBar"></div>
  402. </div>
  403. `;
  404. document.body.appendChild(overlay);
  405. // 开始模拟加载进度
  406. let progress = 0;
  407. const progressBar = overlay.querySelector('#loadingProgressBar');
  408. const interval = setInterval(() => {
  409. progress += 5;
  410. if (progress > 90) {
  411. clearInterval(interval);
  412. } else {
  413. progressBar.style.width = progress + '%';
  414. }
  415. }, 200);
  416. });
  417. });
  418. // PDF预览函数
  419. function previewPDF(pdfId) {
  420. window.location.href = '{{ url_for('pdf_management') }}?view=' + pdfId + '&preview=true';
  421. }
  422. // PDF解析函数
  423. function parsePDF(pdfId) {
  424. if (!confirm('确定要开始AI解析吗?这将把PDF拆分为单页并生成扫描件数据,可能需要一些时间。')) {
  425. return;
  426. }
  427. const btn = document.getElementById('parseBtn' + pdfId);
  428. const originalHtml = btn.innerHTML;
  429. btn.disabled = true;
  430. btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> 解析中...';
  431. fetch('/manager/parse_pdf/' + pdfId, {
  432. method: 'POST',
  433. headers: {
  434. 'Content-Type': 'application/json'
  435. }
  436. })
  437. .then(response => response.json())
  438. .then(data => {
  439. if (data.success) {
  440. alert('PDF解析已开始,将在后台执行。解析完成后状态会自动更新。');
  441. // 重新加载页面以更新状态
  442. setTimeout(() => {
  443. window.location.reload();
  444. }, 1000);
  445. } else {
  446. alert('启动解析失败: ' + data.message);
  447. btn.innerHTML = originalHtml;
  448. btn.disabled = false;
  449. }
  450. })
  451. .catch(error => {
  452. console.error('Error:', error);
  453. alert('请求失败,请重试');
  454. btn.innerHTML = originalHtml;
  455. btn.disabled = false;
  456. });
  457. }
  458. </script>
  459. {% endblock %}