contract_view.html 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  1. {% extends "base.html" %}
  2. {% block title %}逐鹿人才-证件合同管理系统-查看合同 - {{ contract.name }}{% endblock %}
  3. {% block content %}
  4. <div class="container mt-5">
  5. <!-- 页面标题与操作按钮 -->
  6. <div class="d-flex justify-content-between align-items-center mb-4 flex-wrap">
  7. <h2 class="text-dark mb-2"><i class="bi bi-file-text"></i> 查看合同 - {{ contract.name }}</h2>
  8. <div class="btn-toolbar mb-2" role="toolbar">
  9. <div class="btn-group me-2 mb-2" role="group">
  10. {% if current_user.can_edit %}
  11. <a href="{{ url_for('edit_contract', contract_id=contract.id) }}" class="btn btn-outline-secondary btn-sm">
  12. <i class="bi bi-pencil-square"></i> 编辑
  13. </a>
  14. {% endif %}
  15. {% if current_user.can_create and contract.is_active %}
  16. <a href="{{ url_for('renew_contract', contract_id=contract.id) }}" class="btn btn-outline-info btn-sm">
  17. <i class="bi bi-arrow-clockwise"></i> 续签
  18. </a>
  19. {% endif %}
  20. </div>
  21. <div class="btn-group me-2 mb-2" role="group">
  22. {% if current_user.can_delete %}
  23. <form action="{{ url_for('delete_contract', contract_id=contract.id) }}" method="POST" class="d-inline">
  24. <button type="submit" class="btn btn-warning btn-sm"
  25. onclick="return confirm('确定要终止此合同吗?')">
  26. <i class="bi bi-x-circle"></i> 终止
  27. </button>
  28. </form>
  29. <form action="{{ url_for('delete_contract_permanent', contract_id=contract.id) }}" method="POST" class="d-inline">
  30. <button type="submit" class="btn btn-danger btn-sm"
  31. onclick="return confirm('⚠️ 该操作不可恢复!确定要彻底删除此合同吗?')">
  32. <i class="bi bi-trash-fill"></i> 删除
  33. </button>
  34. </form>
  35. {% endif %}
  36. </div>
  37. <div class="btn-group mb-2" role="group">
  38. <a href="{{ url_for('contract_versions', contract_id=contract.id) }}" class="btn btn-outline-secondary btn-sm">
  39. <i class="bi bi-clock-history"></i> 历史版本
  40. </a>
  41. <a href="{{ url_for('contract_list') }}" class="btn btn-outline-primary btn-sm">
  42. <i class="bi bi-list-ul"></i> 返回列表
  43. </a>
  44. </div>
  45. </div>
  46. </div>
  47. <!-- 合同基本信息 -->
  48. <div class="card shadow-sm mb-4">
  49. <div class="card-header bg-light"><h5 class="mb-0"><i class="bi bi-info-circle"></i> 合同基本信息</h5></div>
  50. <div class="card-body">
  51. <div class="row">
  52. <div class="col-md-6">
  53. <table class="table table-borderless mb-0">
  54. <tr><th>合同编号</th><td>{{ contract.contract_number }}</td></tr>
  55. <tr><th>合同名称</th><td>{{ contract.name }}</td></tr>
  56. <tr><th>合同类型</th><td>{{ contract.type.name }}</td></tr>
  57. <tr><th>我方主体</th><td>{{ contract.company_entity.name }}</td></tr>
  58. <tr><th>我方签约人</th><td>{{ contract.signer.name }}</td></tr>
  59. <tr><th>签约方式</th><td>{{ contract.signing_method }}</td></tr>
  60. <tr><th>存储盒名称</th><td>{{ contract.storage_box }}</td></tr>
  61. </table>
  62. </div>
  63. <div class="col-md-6">
  64. <table class="table table-borderless mb-0">
  65. <tr><th>开始日期</th><td>{{ contract.start_date.strftime('%Y-%m-%d') }}</td></tr>
  66. <tr><th>结束日期</th><td>{{ contract.end_date.strftime('%Y-%m-%d') }}</td></tr>
  67. <tr><th>合同收回时间</th>
  68. <td>{% if contract.collected_date %}{{ contract.collected_date.strftime('%Y-%m-%d') }}{% else %}-{% endif %}</td>
  69. </tr>
  70. <tr><th>状态</th>
  71. <td>{% if contract.is_active %}
  72. <span class="badge bg-success">有效</span>
  73. {% else %}
  74. <span class="badge bg-secondary">终止</span>
  75. {% endif %}
  76. </td>
  77. </tr>
  78. <tr><th>创建人</th><td>{{ contract.creator.name }}</td></tr>
  79. <tr><th>创建时间</th><td>{{ contract.created_at.strftime('%Y-%m-%d %H:%M') }}</td></tr>
  80. </table>
  81. </div>
  82. </div>
  83. </div>
  84. </div>
  85. <!-- 对方主体 -->
  86. <div class="card shadow-sm mb-4">
  87. <div class="card-header bg-light"><h5 class="mb-0"><i class="bi bi-people"></i> 对方主体</h5></div>
  88. <div class="card-body">
  89. <ul class="list-group list-group-flush">
  90. {% for cp in counterparties %}
  91. <li class="list-group-item">{{ cp.name }}</li>
  92. {% endfor %}
  93. </ul>
  94. </div>
  95. </div>
  96. <!-- 备注 -->
  97. {% if contract.notes %}
  98. <div class="card shadow-sm mb-4">
  99. <div class="card-header bg-light"><h5 class="mb-0"><i class="bi bi-journal-text"></i> 备注</h5></div>
  100. <div class="card-body"><p>{{ contract.notes }}</p></div>
  101. </div>
  102. {% endif %}
  103. <!-- 附件 -->
  104. {% if attachments %}
  105. <div class="card shadow-sm mb-4">
  106. <div class="card-header bg-light d-flex justify-content-between align-items-center">
  107. <h5 class="mb-0"><i class="bi bi-paperclip"></i> 附件</h5>
  108. <div class="scroll-hint text-muted d-none d-md-block">
  109. <i class="bi bi-arrow-left-right"></i> 左右滑动查看所有附件
  110. </div>
  111. </div>
  112. <div class="card-body">
  113. <div class="attachments-container">
  114. <div class="attachments-scroll">
  115. {% set preview_attachments = [] %}
  116. {% for attachment in attachments %}
  117. {% set ext = attachment.filepath.split('.')[-1].lower() %}
  118. {% if ext in ['pdf','jpg','jpeg','png','gif','bmp','tiff','webp'] %}
  119. {% set _ = preview_attachments.append(attachment) %}
  120. {% endif %}
  121. {% endfor %}
  122. {% for attachment in attachments %}
  123. {% set ext = attachment.filepath.split('.')[-1].lower() %}
  124. <div class="attachment-item">
  125. {% if ext in ['pdf','jpg','jpeg','png','gif','bmp','tiff','webp'] %}
  126. {% set preview_index = preview_attachments.index(attachment) %}
  127. <a href="#" data-bs-toggle="modal" data-bs-target="#attachmentCarouselModal"
  128. data-preview-index="{{ preview_index }}" class="d-flex align-items-center">
  129. {% if ext == 'pdf' %}
  130. <i class="bi bi-file-earmark-pdf-fill text-danger me-2" style="font-size:1.8rem;"></i>
  131. {% else %}
  132. <img src="{{ url_for('view_attachment', attachment_id=attachment.id) }}"
  133. class="img-thumbnail me-2" style="width:40px; height:40px; object-fit:cover;">
  134. {% endif %}
  135. <span class="attachment-name">{{ attachment.filename }}</span>
  136. </a>
  137. {% else %}
  138. <a href="{{ url_for('download_file', attachment_id=attachment.id) }}" target="_blank" class="d-flex align-items-center">
  139. <i class="bi bi-file-earmark-text-fill text-primary me-2" style="font-size:1.8rem;"></i>
  140. <span class="attachment-name">{{ attachment.filename }}</span>
  141. </a>
  142. {% endif %}
  143. </div>
  144. {% endfor %}
  145. </div>
  146. </div>
  147. </div>
  148. </div>
  149. <!-- 轮播模态,只显示 PDF 和图片 -->
  150. <div class="modal fade" id="attachmentCarouselModal" tabindex="-1">
  151. <div class="modal-dialog modal-xl modal-dialog-centered">
  152. <div class="modal-content">
  153. <div class="modal-header d-flex justify-content-between align-items-center">
  154. <h5 class="modal-title" id="carouselAttachmentTitle"></h5>
  155. <div class="d-flex gap-2">
  156. <a id="carouselDownloadBtn" href="#" class="btn btn-primary btn-sm">
  157. <i class="bi bi-download"></i> 下载
  158. </a>
  159. <button id="carouselPrintBtn" type="button" class="btn btn-info btn-sm">
  160. <i class="bi bi-printer"></i> 打印
  161. </button>
  162. <button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">
  163. <i class="bi bi-x-lg"></i> 关闭
  164. </button>
  165. </div>
  166. </div>
  167. <div class="modal-body text-center">
  168. <!-- 关闭自动轮播:移除 data-bs-ride -->
  169. <div id="attachmentCarousel" class="carousel slide">
  170. <div class="carousel-inner">
  171. {% for attachment in preview_attachments %}
  172. {% set ext = attachment.filepath.split('.')[-1].lower() %}
  173. <div class="carousel-item {% if loop.first %}active{% endif %}"
  174. data-filename="{{ attachment.filename }}"
  175. data-attachment-id="{{ attachment.id }}">
  176. {% if ext == 'pdf' %}
  177. <iframe src="{{ url_for('view_attachment', attachment_id=attachment.id) }}"
  178. width="100%" height="600px"></iframe>
  179. {% else %}
  180. <img src="{{ url_for('view_attachment', attachment_id=attachment.id) }}"
  181. class="img-fluid rounded" style="max-height: 600px;">
  182. {% endif %}
  183. </div>
  184. {% endfor %}
  185. </div>
  186. <button class="carousel-control-prev" type="button" data-bs-target="#attachmentCarousel" data-bs-slide="prev">
  187. <span class="carousel-control-prev-icon"></span>
  188. </button>
  189. <button class="carousel-control-next" type="button" data-bs-target="#attachmentCarousel" data-bs-slide="next">
  190. <span class="carousel-control-next-icon"></span>
  191. </button>
  192. </div>
  193. <!-- 缩略图导航 - 使用与附件列表相同的样式 -->
  194. <div class="card shadow-sm mt-3">
  195. <div class="card-header bg-light py-2">
  196. <h6 class="mb-0">附件列表</h6>
  197. </div>
  198. <div class="card-body p-2">
  199. <div class="attachments-container" style="max-height: 150px; overflow-y: auto;">
  200. <div class="attachments-scroll">
  201. {% for attachment in preview_attachments %}
  202. {% set ext = attachment.filepath.split('.')[-1].lower() %}
  203. <div class="attachment-item" style="cursor: pointer;" data-bs-target="#attachmentCarousel" data-bs-slide-to="{{ loop.index0 }}">
  204. {% if ext == 'pdf' %}
  205. <div class="d-flex align-items-center">
  206. <i class="bi bi-file-earmark-pdf-fill text-danger me-2" style="font-size:1.5rem;"></i>
  207. <span class="attachment-name">{{ attachment.filename }}</span>
  208. </div>
  209. {% else %}
  210. <div class="d-flex align-items-center">
  211. <img src="{{ url_for('view_attachment', attachment_id=attachment.id) }}"
  212. class="img-thumbnail me-2" style="width:35px; height:35px; object-fit:cover;">
  213. <span class="attachment-name">{{ attachment.filename }}</span>
  214. </div>
  215. {% endif %}
  216. </div>
  217. {% endfor %}
  218. </div>
  219. </div>
  220. </div>
  221. </div>
  222. </div>
  223. </div>
  224. </div>
  225. </div>
  226. <script>
  227. var carouselModal = document.getElementById('attachmentCarouselModal');
  228. var carousel = document.getElementById('attachmentCarousel');
  229. var carouselTitle = document.getElementById('carouselAttachmentTitle');
  230. var downloadBtn = document.getElementById('carouselDownloadBtn');
  231. var printBtn = document.getElementById('carouselPrintBtn');
  232. carouselModal.addEventListener('show.bs.modal', function (event) {
  233. var trigger = event.relatedTarget;
  234. var previewIndex = parseInt(trigger.getAttribute('data-preview-index'));
  235. var carouselInstance = bootstrap.Carousel.getInstance(carousel) || new bootstrap.Carousel(carousel);
  236. carouselInstance.to(previewIndex);
  237. var items = carousel.querySelectorAll('.carousel-item');
  238. var currentItem = items[previewIndex];
  239. var filename = currentItem.getAttribute('data-filename');
  240. var attachmentId = currentItem.getAttribute('data-attachment-id');
  241. carouselTitle.textContent = filename;
  242. downloadBtn.href = "{{ url_for('download_file', attachment_id=0) }}".replace('0', attachmentId);
  243. });
  244. carousel.addEventListener('slid.bs.carousel', function () {
  245. var activeItem = carousel.querySelector('.carousel-item.active');
  246. var filename = activeItem.getAttribute('data-filename');
  247. var attachmentId = activeItem.getAttribute('data-attachment-id');
  248. carouselTitle.textContent = filename;
  249. downloadBtn.href = "{{ url_for('download_file', attachment_id=0) }}".replace('0', attachmentId);
  250. });
  251. // 新增打印按钮功能
  252. printBtn.addEventListener('click', function() {
  253. var activeItem = carousel.querySelector('.carousel-item.active');
  254. var iframe = activeItem.querySelector('iframe');
  255. var img = activeItem.querySelector('img');
  256. var attachmentId = activeItem.getAttribute('data-attachment-id');
  257. if (iframe) {
  258. // PDF打印处理
  259. try {
  260. iframe.contentWindow.focus();
  261. iframe.contentWindow.print();
  262. } catch (e) {
  263. console.error("PDF打印失败:", e);
  264. // 备选方案:打开新窗口打印
  265. var printWindow = window.open(
  266. "{{ url_for('view_attachment', attachment_id=0) }}".replace('0', attachmentId),
  267. '_blank'
  268. );
  269. printWindow.onload = function() {
  270. printWindow.print();
  271. };
  272. }
  273. } else if (img) {
  274. // 图片打印处理
  275. var printWindow = window.open('', '_blank');
  276. printWindow.document.write(`
  277. <html>
  278. <head>
  279. <title>打印图片</title>
  280. <style>
  281. body { margin: 0; display: flex; justify-content: center; align-items: center; min-height: 100vh; }
  282. img { max-width: 100%; max-height: 100vh; object-fit: contain; }
  283. </style>
  284. </head>
  285. <body>
  286. <img src="${img.src}">
  287. </body>
  288. </html>
  289. `);
  290. printWindow.document.close();
  291. printWindow.onload = function() {
  292. printWindow.print();
  293. };
  294. }
  295. });
  296. </script>
  297. {% endif %}
  298. </div>
  299. <style>
  300. .text-truncate {
  301. overflow: hidden;
  302. white-space: nowrap;
  303. text-overflow: ellipsis;
  304. }
  305. .card-header {
  306. font-weight: 500;
  307. }
  308. .table th {
  309. width: 35%;
  310. }
  311. /* 附件容器样式 */
  312. .attachments-container {
  313. overflow-x: auto;
  314. padding-bottom: 10px;
  315. }
  316. .attachments-scroll {
  317. display: flex;
  318. flex-wrap: nowrap;
  319. gap: 15px;
  320. padding: 5px 0;
  321. }
  322. .attachment-item {
  323. flex: 0 0 auto;
  324. min-width: 200px;
  325. padding: 10px 15px;
  326. background-color: #f8f9fa;
  327. border-radius: 8px;
  328. border: 1px solid #e9ecef;
  329. transition: all 0.2s;
  330. }
  331. .attachment-item:hover {
  332. background-color: #e9ecef;
  333. transform: translateY(-2px);
  334. box-shadow: 0 4px 8px rgba(0,0,0,0.1);
  335. }
  336. .attachment-name {
  337. font-size: 0.9rem;
  338. word-break: break-word;
  339. text-align: left;
  340. max-width: 180px;
  341. }
  342. .scroll-hint {
  343. font-size: 0.85rem;
  344. opacity: 0.7;
  345. }
  346. /* 滚动条样式 */
  347. .attachments-container::-webkit-scrollbar {
  348. height: 8px;
  349. }
  350. .attachments-container::-webkit-scrollbar-track {
  351. background: #f1f1f1;
  352. border-radius: 10px;
  353. }
  354. .attachments-container::-webkit-scrollbar-thumb {
  355. background: #c1c1c1;
  356. border-radius: 10px;
  357. }
  358. .attachments-container::-webkit-scrollbar-thumb:hover {
  359. background: #a8a8a8;
  360. }
  361. /* 响应式调整 */
  362. @media (max-width: 768px) {
  363. .attachment-item {
  364. min-width: 180px;
  365. }
  366. .attachment-name {
  367. max-width: 150px;
  368. }
  369. /* 小屏幕下按钮垂直排列 */
  370. .modal-header .d-flex {
  371. flex-direction: column;
  372. align-items: flex-end;
  373. gap: 5px;
  374. }
  375. }
  376. </style>
  377. {% endblock %}