member_detail.html 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721
  1. {% extends "layout.html" %}
  2. {% block title %}成员详情 - {{ member.name }}{% endblock %}
  3. {% block extra_css %}
  4. <style>
  5. .info-label { color: #6c757d; font-weight: normal; width: 120px; display: inline-block; }
  6. .info-value { color: #212529; font-weight: 500; }
  7. .detail-section { background: white; border-radius: 8px; padding: 25px; margin-bottom: 20px; box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); }
  8. .section-title { font-size: 1.1rem; font-weight: bold; border-bottom: 2px solid #f8f9fa; padding-bottom: 10px; margin-bottom: 20px; color: #0d6efd; }
  9. .relation-card { border-left: 4px solid #198754; background: #f8fff9; padding: 10px 15px; margin-bottom: 10px; border-radius: 4px; }
  10. /* Image Preview in Sidebar */
  11. .source-image-preview {
  12. cursor: pointer;
  13. position: relative;
  14. overflow: hidden;
  15. border: 1px solid #dee2e6;
  16. border-radius: 4px;
  17. transition: transform 0.2s;
  18. }
  19. .source-image-preview:hover {
  20. transform: scale(1.02);
  21. box-shadow: 0 4px 8px rgba(0,0,0,0.1);
  22. }
  23. .source-image-preview img {
  24. width: 100%;
  25. height: auto;
  26. display: block;
  27. }
  28. .preview-overlay {
  29. position: absolute;
  30. bottom: 0;
  31. left: 0;
  32. right: 0;
  33. background: rgba(0,0,0,0.6);
  34. color: white;
  35. padding: 5px;
  36. text-align: center;
  37. font-size: 0.8rem;
  38. opacity: 0;
  39. transition: opacity 0.2s;
  40. }
  41. .source-image-preview:hover .preview-overlay {
  42. opacity: 1;
  43. }
  44. #detailImageTabNav .nav-link { cursor: pointer; font-size: 0.85rem; padding: 0.3rem 0.6rem; }
  45. .image-empty-hint { color: #6c757d; font-size: 0.85rem; padding: 20px; text-align: center; }
  46. .detail-preview-empty {
  47. min-height: 160px;
  48. display: flex;
  49. align-items: center;
  50. justify-content: center;
  51. background: #f8f9fa;
  52. border: 1px dashed #dee2e6;
  53. border-radius: 4px;
  54. color: #6c757d;
  55. font-size: 0.9rem;
  56. }
  57. /* Modal Image Viewer */
  58. .modal-image-container {
  59. height: 80vh;
  60. display: flex;
  61. flex-direction: column;
  62. }
  63. .image-toolbar {
  64. background: #e9ecef;
  65. padding: 5px 10px;
  66. border-bottom: 1px solid #dee2e6;
  67. display: flex;
  68. gap: 10px;
  69. align-items: center;
  70. flex-wrap: wrap;
  71. }
  72. .image-viewer {
  73. flex: 1;
  74. border: 1px solid #ccc;
  75. background: #f0f0f0;
  76. overflow: hidden;
  77. text-align: center;
  78. position: relative;
  79. cursor: grab;
  80. user-select: none;
  81. }
  82. .image-viewer:active {
  83. cursor: grabbing;
  84. }
  85. .image-wrapper {
  86. display: inline-block;
  87. transition: transform 0.2s ease-out;
  88. transform-origin: center center;
  89. position: absolute;
  90. top: 50%;
  91. left: 50%;
  92. transform: translate(-50%, -50%);
  93. }
  94. .image-wrapper img {
  95. max-width: 100%;
  96. max-height: 100%; /* Fit within viewer initially */
  97. display: block;
  98. pointer-events: none;
  99. user-select: none;
  100. transition: filter 0.2s;
  101. box-shadow: 0 0 20px rgba(0,0,0,0.1);
  102. }
  103. /* 放大镜样式 - Optional if we want it in modal too */
  104. .magnifier-glass {
  105. position: absolute;
  106. border: 3px solid #000;
  107. border-radius: 50%;
  108. cursor: none;
  109. width: 150px;
  110. height: 150px;
  111. box-shadow: 0 0 10px rgba(0,0,0,0.5);
  112. display: none;
  113. z-index: 1000;
  114. background-repeat: no-repeat;
  115. background-color: white;
  116. pointer-events: none;
  117. }
  118. </style>
  119. {% endblock %}
  120. {% block content %}
  121. <div class="container py-4">
  122. <div class="d-flex justify-content-between align-items-center mb-4">
  123. <h2><i class="bi bi-person-badge"></i> {{ member.name }} 的个人详情</h2>
  124. <div class="btn-group">
  125. <a href="{{ url_for('edit_member', member_id=member.id) }}" class="btn btn-primary">
  126. <i class="bi bi-pencil"></i> 编辑信息
  127. </a>
  128. <a href="{{ url_for('members') }}" class="btn btn-outline-secondary">返回列表</a>
  129. </div>
  130. </div>
  131. <div class="row">
  132. <!-- 基本与核心信息 -->
  133. <div class="col-md-8">
  134. <div class="detail-section">
  135. <div class="section-title">基本信息</div>
  136. {% if adopt_info %}
  137. <div class="alert alert-warning d-flex align-items-center mb-3 py-2 px-3" role="alert" style="border-left: 4px solid #fd7e14; background:#fff8f0;">
  138. <i class="bi bi-house-door-fill me-2 text-warning"></i>
  139. <span class="fw-bold">{{ adopt_info }}</span>
  140. </div>
  141. {% endif %}
  142. <div class="row g-3">
  143. <div class="col-md-6">
  144. <span class="info-label">姓名(繁体):</span>
  145. <span class="info-value">{{ member.name }}</span>
  146. </div>
  147. <div class="col-md-6">
  148. <span class="info-label">姓名(简体):</span>
  149. <span class="info-value">{{ member.simplified_name or '-' }}</span>
  150. </div>
  151. <div class="col-md-12">
  152. <span class="info-label d-block mb-1">族谱原文(繁体):</span>
  153. <span class="info-value">{{ member.genealogy_original_traditional if member.genealogy_original_traditional and member.genealogy_original_traditional != 'None' else '-' }}</span>
  154. </div>
  155. <div class="col-md-12">
  156. <span class="info-label d-block mb-1">族谱原文(简体):</span>
  157. <span class="info-value">{{ member.genealogy_original_simplified if member.genealogy_original_simplified and member.genealogy_original_simplified != 'None' else '-' }}</span>
  158. </div>
  159. <div class="col-md-6">
  160. <span class="info-label">性别:</span>
  161. <span class="info-value">{{ '男' if member.sex == 1 else '女' }}</span>
  162. </div>
  163. <div class="col-md-6">
  164. <span class="info-label">曾用名:</span>
  165. <span class="info-value">{{ member.former_name or '-' }}</span>
  166. </div>
  167. <div class="col-md-6">
  168. <span class="info-label">幼名/乳名:</span>
  169. <span class="info-value">{{ member.childhood_name or '-' }}</span>
  170. </div>
  171. <div class="col-md-6">
  172. <span class="info-label">出生日期:</span>
  173. <span class="info-value">{{ member.birthday_str or '未知' }}</span>
  174. </div>
  175. <div class="col-md-6">
  176. <span class="info-label">民族:</span>
  177. <span class="info-value">{{ member.nation or '-' }}</span>
  178. </div>
  179. <div class="col-md-6">
  180. <span class="info-label">状态:</span>
  181. <span class="info-value">
  182. {% if member.is_pass_away == 1 %}
  183. <span class="text-danger">已故</span>
  184. {% elif member.is_pass_away == 2 %}
  185. <span class="text-warning">未知</span>
  186. {% else %}
  187. <span class="text-success">健在</span>
  188. {% endif %}
  189. </span>
  190. </div>
  191. <div class="col-md-6">
  192. <span class="info-label">婚姻状况:</span>
  193. <span class="info-value">
  194. {% set marital_map = {0: '未知', 1: '未婚', 2: '已婚', 3: '离异/丧偶'} %}
  195. {{ marital_map.get(member.marital_status, '未知') }}
  196. </span>
  197. </div>
  198. </div>
  199. <div class="section-title mt-4">谱系资料</div>
  200. <div class="row g-3">
  201. <div class="col-md-6">
  202. <span class="info-label">字辈:</span>
  203. <span class="info-value">{{ member.name_word or '-' }}</span>
  204. </div>
  205. <div class="col-md-6">
  206. <span class="info-label">堂内排行:</span>
  207. <span class="info-value">{{ member.family_rank or '-' }}</span>
  208. </div>
  209. <div class="col-md-6">
  210. <span class="info-label">世系世代:</span>
  211. <span class="info-value">
  212. {% if member.name_word_generation %}
  213. {% set generations = member.name_word_generation.split(';') %}
  214. <span class="d-inline-flex flex-wrap gap-1 align-items-center">
  215. {% for gen in generations %}
  216. {% if gen.strip() %}
  217. <span class="badge rounded-pill px-3 py-1" style="background: linear-gradient(135deg, #e8f0fe, #d2e3fc); color: #1a56db; font-weight: 500; font-size: 0.8rem; letter-spacing: 0.5px; border: 1px solid rgba(26,86,219,0.15);">
  218. <i class="bi bi-diagram-3 me-1" style="font-size: 0.7rem;"></i>{{ gen.strip() }}
  219. </span>
  220. {% endif %}
  221. {% endfor %}
  222. </span>
  223. {% else %}
  224. -
  225. {% endif %}
  226. </span>
  227. </div>
  228. <div class="col-md-6">
  229. <span class="info-label">名号/封号:</span>
  230. <span class="info-value">{{ member.name_title or '-' }}</span>
  231. </div>
  232. <div class="col-md-6">
  233. <span class="info-label">分房/堂号:</span>
  234. <span class="info-value">{{ member.branch_family_hall or '-' }}</span>
  235. </div>
  236. <div class="col-md-12">
  237. <span class="info-label">聚居地:</span>
  238. <span class="info-value">{{ member.cluster_place or '-' }}</span>
  239. </div>
  240. </div>
  241. <div class="section-title mt-4">联系与地址</div>
  242. <div class="row g-3">
  243. <div class="col-md-6">
  244. <span class="info-label">手机号:</span>
  245. <span class="info-value">{{ member.phone or '-' }}</span>
  246. </div>
  247. <div class="col-md-6">
  248. <span class="info-label">微信号:</span>
  249. <span class="info-value">{{ member.wechat_account or '-' }}</span>
  250. </div>
  251. <div class="col-md-12">
  252. <span class="info-label">现居住址:</span>
  253. <span class="info-value">{{ member.residential_address or '-' }}</span>
  254. </div>
  255. </div>
  256. </div>
  257. <div class="detail-section">
  258. <div class="section-title">个人履历与成就</div>
  259. <div class="mb-4">
  260. <label class="info-label d-block mb-2">标签:</label>
  261. <div class="p-3 bg-light rounded">{{ member.tags or '暂无' }}</div>
  262. </div>
  263. <div class="mb-4">
  264. <label class="info-label d-block mb-2">人员备注:</label>
  265. <div class="p-3 bg-light rounded">{{ member.notes or '暂无' }}</div>
  266. </div>
  267. <div class="mb-4">
  268. <label class="info-label d-block mb-2">疑似错误标注:</label>
  269. <div class="p-3 bg-warning bg-opacity-10 border border-warning rounded text-warning">
  270. {{ member.suspected_error or '暂无' }}
  271. </div>
  272. </div>
  273. <div class="mb-4">
  274. <label class="info-label d-block mb-2">职业背景:</label>
  275. <div class="p-3 bg-light rounded">{{ member.occupation or '暂无信息' }}</div>
  276. </div>
  277. <div class="mb-4">
  278. <label class="info-label d-block mb-2">教育经历:</label>
  279. <div class="p-3 bg-light rounded">{{ member.educational or '暂无信息' }}</div>
  280. </div>
  281. <div>
  282. <label class="info-label d-block mb-2">个人成就:</label>
  283. <div class="p-3 bg-light rounded">{{ member.personal_achievements or '暂无信息' }}</div>
  284. </div>
  285. </div>
  286. </div>
  287. <!-- 关系信息与原图 -->
  288. <div class="col-md-4">
  289. {% if member.source_image_url or member.reference_image_url %}
  290. <div class="detail-section">
  291. <div class="section-title">家谱图片</div>
  292. <ul class="nav nav-tabs mb-3" id="detailImageTabNav">
  293. <li class="nav-item">
  294. <button type="button" class="nav-link" id="detail-tab-scan" onclick="switchDetailImageTab('scan')">
  295. <i class="bi bi-file-earmark-image"></i> 查看扫描件
  296. </button>
  297. </li>
  298. <li class="nav-item">
  299. <button type="button" class="nav-link" id="detail-tab-reference" onclick="switchDetailImageTab('reference')">
  300. <i class="bi bi-file-earmark-plus"></i> 查看参考件
  301. </button>
  302. </li>
  303. </ul>
  304. <div class="source-image-preview" onclick="openImageViewer()">
  305. <img id="detailPreviewImage" src="" alt="家谱图片" style="display: none;">
  306. <div id="detailPreviewEmpty" class="detail-preview-empty" style="display: none;">
  307. <span><i class="bi bi-image"></i> 暂无扫描件</span>
  308. </div>
  309. <div class="preview-overlay">
  310. <i class="bi bi-arrows-fullscreen"></i> <span id="detailPreviewHint">点击查看大图</span>
  311. </div>
  312. </div>
  313. <div class="mt-3 small text-muted bg-light p-2 rounded" id="detailScanMeta">
  314. <div class="mb-1"><i class="bi bi-journal-text me-1"></i><strong>版本名称:</strong> {{ member.genealogy_version or '未提供' }}</div>
  315. <div class="mb-1"><i class="bi bi-archive me-1"></i><strong>版本来源:</strong> {{ member.genealogy_source or '未提供' }}</div>
  316. <div><i class="bi bi-person me-1"></i><strong>文件提供人:</strong> {{ member.upload_person or '未提供' }}</div>
  317. </div>
  318. <div class="mt-3 small text-muted bg-light p-2 rounded" id="detailReferenceMeta" style="display: none;">
  319. <div class="mb-1"><i class="bi bi-file-earmark me-1"></i><strong>文件名:</strong> {{ member.reference_file_name or '未提供' }}</div>
  320. <div><i class="bi bi-clock me-1"></i><strong>上传时间:</strong> {{ member.reference_upload_time or '未提供' }}</div>
  321. </div>
  322. </div>
  323. {% endif %}
  324. <div class="detail-section">
  325. <div class="section-title">家族关系</div>
  326. <h6 class="fw-bold mb-3"><i class="bi bi-arrow-up-circle me-1"></i> 尊辈/关联人</h6>
  327. {% set child_order_labels = {1: '长子', 2: '次子', 3: '三子', 4: '四子', 5: '五子', 6: '六子', 7: '七子', 8: '八子', 9: '九子', 10: '十子'} %}
  328. {% for p in parents %}
  329. <div class="relation-card">
  330. <div class="d-flex justify-content-between align-items-center">
  331. <a href="{{ url_for('member_detail', member_id=p.id) }}" class="text-decoration-none fw-bold">
  332. {{ p.name }}
  333. </a>
  334. <div class="d-flex gap-2">
  335. <span class="badge bg-success small">
  336. {% set rel_map = {1: '父亲', 2: '母亲', 10: '配偶', 11: '兄弟', 12: '姐妹'} %}
  337. {{ rel_map.get(p.relation_type, '关联人') }}
  338. </span>
  339. {% if p.relation_type == 1 and p.child_order %}
  340. <span class="badge bg-info small">
  341. {{ child_order_labels.get(p.child_order, '第' ~ p.child_order ~ '子') }}
  342. </span>
  343. {% endif %}
  344. {% if p.sub_relation_type %}
  345. <span class="badge bg-warning small">
  346. {% set sub_rel_map = {0: '亲生', 1: '养父', 2: '出继', 3: '入继', 10: '妾', 11: '外室'} %}
  347. {{ sub_rel_map.get(p.sub_relation_type, '') }}
  348. </span>
  349. {% endif %}
  350. </div>
  351. </div>
  352. </div>
  353. {% endfor %}
  354. {% if not parents %}
  355. <p class="text-muted small">暂无上层关系记录</p>
  356. {% endif %}
  357. <h6 class="fw-bold mt-4 mb-3"><i class="bi bi-arrow-down-circle me-1"></i> 子嗣/晚辈</h6>
  358. {% for c in children %}
  359. <div class="relation-card" style="border-left-color: #0d6efd; background: #f0f7ff;">
  360. <div class="d-flex justify-content-between align-items-center">
  361. <a href="{{ url_for('member_detail', member_id=c.id) }}" class="text-decoration-none fw-bold">
  362. {{ c.name }}
  363. </a>
  364. <div class="d-flex gap-2">
  365. <span class="badge bg-primary small">
  366. {% set rel_map = {1: '子女', 2: '子女', 10: '配偶', 11: '兄弟', 12: '姐妹'} %}
  367. {{ rel_map.get(c.relation_type, '后辈') }}
  368. </span>
  369. {% if c.child_order %}
  370. <span class="badge bg-info small">
  371. {{ child_order_labels.get(c.child_order, '第' ~ c.child_order ~ '子') }}
  372. </span>
  373. {% endif %}
  374. {% if c.sub_relation_type %}
  375. <span class="badge bg-warning small">
  376. {% set sub_rel_map = {0: '亲生', 1: '养父', 2: '出继', 3: '入继', 10: '妾', 11: '外室'} %}
  377. {{ sub_rel_map.get(c.sub_relation_type, '') }}
  378. </span>
  379. {% endif %}
  380. </div>
  381. </div>
  382. </div>
  383. {% endfor %}
  384. {% if not children %}
  385. <p class="text-muted small">暂无下层关系记录</p>
  386. {% endif %}
  387. </div>
  388. <div class="detail-section text-center">
  389. <div class="section-title">系统操作</div>
  390. <div class="small text-muted mb-3">
  391. 记录创建于:{{ member.create_time }}<br>
  392. 最后修改:{{ member.modified_time }}
  393. </div>
  394. <button class="btn btn-sm btn-outline-danger w-100" onclick="confirmDelete()">
  395. <i class="bi bi-trash"></i> 删除此成员
  396. </button>
  397. <form id="deleteForm" action="{{ url_for('delete_member', member_id=member.id) }}" method="POST" style="display: none;"></form>
  398. </div>
  399. </div>
  400. </div>
  401. </div>
  402. <!-- Image Viewer Modal -->
  403. <div class="modal fade" id="imageModal" tabindex="-1" aria-hidden="true">
  404. <div class="modal-dialog modal-fullscreen">
  405. <div class="modal-content bg-light">
  406. <div class="modal-header py-2">
  407. <h5 class="modal-title fs-6"><i class="bi bi-image"></i> <span id="modalImageTitle">家谱图片查看</span></h5>
  408. <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
  409. </div>
  410. <div class="modal-body p-0 modal-image-container">
  411. <ul class="nav nav-tabs px-3 pt-2 bg-white border-bottom" id="modalImageTabNav">
  412. <li class="nav-item">
  413. <button type="button" class="nav-link" id="modal-tab-scan" onclick="switchModalImageTab('scan')">
  414. <i class="bi bi-file-earmark-image"></i> 查看扫描件
  415. </button>
  416. </li>
  417. <li class="nav-item">
  418. <button type="button" class="nav-link" id="modal-tab-reference" onclick="switchModalImageTab('reference')">
  419. <i class="bi bi-file-earmark-plus"></i> 查看参考件
  420. </button>
  421. </li>
  422. </ul>
  423. <div class="image-toolbar">
  424. <div class="btn-group btn-group-sm">
  425. <button type="button" class="btn btn-outline-secondary" onclick="rotateImage(-90)" title="左旋90°"><i class="bi bi-arrow-counterclockwise"></i></button>
  426. <button type="button" class="btn btn-outline-secondary" onclick="rotateImage(90)" title="右旋90°"><i class="bi bi-arrow-clockwise"></i></button>
  427. </div>
  428. <div class="d-flex align-items-center gap-2 mx-3 border-start border-end px-3">
  429. <i class="bi bi-brightness-high" title="亮度"></i>
  430. <input type="range" class="form-range" min="50" max="150" value="100" id="brightnessRange" oninput="updateImageFilter()" style="width: 80px;">
  431. <i class="bi bi-circle-half ms-2" title="对比度"></i>
  432. <input type="range" class="form-range" min="50" max="200" value="100" id="contrastRange" oninput="updateImageFilter()" style="width: 80px;">
  433. <button class="btn btn-link btn-sm text-decoration-none py-0" onclick="resetFilters()">重置</button>
  434. </div>
  435. <div class="form-check form-switch ms-auto mb-0" title="开启后鼠标悬停图片可局部放大">
  436. <input class="form-check-input" type="checkbox" id="magnifierSwitch">
  437. <label class="form-check-label small" for="magnifierSwitch">🔍 放大镜</label>
  438. </div>
  439. </div>
  440. <div class="image-viewer shadow-inner" id="viewer">
  441. <div id="magnifier" class="magnifier-glass"></div>
  442. <div id="imageWrapper" class="image-wrapper">
  443. <img id="refImage" src="" alt="家谱图片" draggable="false" style="display: none;">
  444. <div id="modalEmptyState" class="image-empty-hint" style="display: none;">暂无图片</div>
  445. </div>
  446. </div>
  447. </div>
  448. </div>
  449. </div>
  450. </div>
  451. {% endblock %}
  452. {% block extra_js %}
  453. <script>
  454. const scanImageUrl = {{ member.source_image_url | tojson if member.source_image_url else 'null' }};
  455. const referenceImageUrl = {{ member.reference_image_url | tojson if member.reference_image_url else 'null' }};
  456. const scanPage = {{ member.source_page | tojson if member.source_page else 'null' }};
  457. let detailActiveTab = scanImageUrl ? 'scan' : 'reference';
  458. let modalActiveTab = detailActiveTab;
  459. function switchDetailImageTab(tab) {
  460. detailActiveTab = tab;
  461. document.getElementById('detail-tab-scan').classList.toggle('active', tab === 'scan');
  462. document.getElementById('detail-tab-reference').classList.toggle('active', tab === 'reference');
  463. document.getElementById('detailScanMeta').style.display = tab === 'scan' && scanImageUrl ? 'block' : 'none';
  464. document.getElementById('detailReferenceMeta').style.display = tab === 'reference' && referenceImageUrl ? 'block' : 'none';
  465. const preview = document.getElementById('detailPreviewImage');
  466. const previewEmpty = document.getElementById('detailPreviewEmpty');
  467. const hint = document.getElementById('detailPreviewHint');
  468. if (tab === 'scan' && scanImageUrl) {
  469. preview.src = scanImageUrl;
  470. preview.style.display = 'block';
  471. previewEmpty.style.display = 'none';
  472. hint.textContent = scanPage ? `点击查看大图 (第${scanPage}页)` : '点击查看大图';
  473. } else if (tab === 'reference' && referenceImageUrl) {
  474. preview.src = referenceImageUrl;
  475. preview.style.display = 'block';
  476. previewEmpty.style.display = 'none';
  477. hint.textContent = '点击查看参考件大图';
  478. } else if (tab === 'scan') {
  479. preview.style.display = 'none';
  480. preview.removeAttribute('src');
  481. previewEmpty.style.display = 'flex';
  482. previewEmpty.innerHTML = '<span><i class="bi bi-image"></i> 暂无扫描件</span>';
  483. hint.textContent = '暂无扫描件';
  484. } else {
  485. preview.style.display = 'none';
  486. preview.removeAttribute('src');
  487. previewEmpty.style.display = 'flex';
  488. previewEmpty.innerHTML = '<span><i class="bi bi-file-earmark-plus"></i> 暂无参考件</span>';
  489. hint.textContent = '暂无参考件';
  490. }
  491. }
  492. function switchModalImageTab(tab) {
  493. modalActiveTab = tab;
  494. document.getElementById('modal-tab-scan').classList.toggle('active', tab === 'scan');
  495. document.getElementById('modal-tab-reference').classList.toggle('active', tab === 'reference');
  496. updateModalImageDisplay();
  497. resetFilters();
  498. }
  499. function updateModalImageDisplay() {
  500. const img = document.getElementById('refImage');
  501. const emptyState = document.getElementById('modalEmptyState');
  502. const title = document.getElementById('modalImageTitle');
  503. const url = modalActiveTab === 'scan' ? scanImageUrl : referenceImageUrl;
  504. if (url) {
  505. img.src = url;
  506. img.style.display = 'block';
  507. emptyState.style.display = 'none';
  508. title.textContent = modalActiveTab === 'scan'
  509. ? (scanPage ? `扫描件查看 - 第 ${scanPage} 页` : '扫描件查看')
  510. : '参考件查看';
  511. } else {
  512. img.style.display = 'none';
  513. img.removeAttribute('src');
  514. emptyState.style.display = 'block';
  515. emptyState.textContent = modalActiveTab === 'scan' ? '暂无扫描件' : '暂无参考件';
  516. title.textContent = '家谱图片查看';
  517. }
  518. currentX = 0;
  519. currentY = 0;
  520. isZoomedIn = false;
  521. updateImageTransform();
  522. }
  523. document.addEventListener('DOMContentLoaded', function() {
  524. if (document.getElementById('detailImageTabNav')) {
  525. switchDetailImageTab(detailActiveTab);
  526. }
  527. });
  528. function confirmDelete() {
  529. if(confirm('确定要删除此成员吗?\n这将同时删除其所有关联关系记录!')) {
  530. document.getElementById('deleteForm').submit();
  531. }
  532. }
  533. function openImageViewer() {
  534. modalActiveTab = detailActiveTab;
  535. switchModalImageTab(modalActiveTab);
  536. var myModal = new bootstrap.Modal(document.getElementById('imageModal'));
  537. myModal.show();
  538. }
  539. // --- Image Viewer Logic (Reused from add_member) ---
  540. // Image State
  541. let imgRotation = 0;
  542. let imgBrightness = 100;
  543. let imgContrast = 100;
  544. // Dragging State
  545. let isDragging = false;
  546. let hasDragged = false;
  547. let startX = 0, startY = 0;
  548. let currentX = 0, currentY = 0; // Relative to center
  549. // Zoom State
  550. let isZoomedIn = false;
  551. const ZOOM_LEVEL = 2.0;
  552. // Elements
  553. const viewer = document.getElementById('viewer');
  554. const magnifier = document.getElementById('magnifier');
  555. const magnifierSwitch = document.getElementById('magnifierSwitch');
  556. const imageWrapper = document.getElementById('imageWrapper');
  557. const refImage = document.getElementById('refImage');
  558. // Initialize Dragging and Zooming
  559. if (imageWrapper) {
  560. // Center initial position logic is handled by CSS (top 50% left 50% translate -50%)
  561. viewer.style.cursor = 'zoom-in';
  562. viewer.addEventListener('mousedown', (e) => {
  563. if (e.target.closest('.image-toolbar') || e.target.closest('.magnifier-glass')) return;
  564. isDragging = true;
  565. hasDragged = false;
  566. startX = e.clientX;
  567. startY = e.clientY;
  568. viewer.style.cursor = 'grabbing';
  569. e.preventDefault();
  570. });
  571. window.addEventListener('mousemove', (e) => {
  572. if (!isDragging) return;
  573. const dx = e.clientX - startX;
  574. const dy = e.clientY - startY;
  575. if (Math.abs(dx) > 2 || Math.abs(dy) > 2) {
  576. hasDragged = true;
  577. }
  578. currentX += dx;
  579. currentY += dy;
  580. startX = e.clientX;
  581. startY = e.clientY;
  582. updateImageTransform();
  583. });
  584. window.addEventListener('mouseup', (e) => {
  585. if (isDragging) {
  586. isDragging = false;
  587. viewer.style.cursor = isZoomedIn ? 'grab' : 'zoom-in';
  588. if (!hasDragged && viewer.contains(e.target)) {
  589. toggleZoom();
  590. }
  591. }
  592. });
  593. }
  594. function toggleZoom() {
  595. isZoomedIn = !isZoomedIn;
  596. if (!isZoomedIn) {
  597. currentX = 0;
  598. currentY = 0;
  599. }
  600. updateImageTransform();
  601. viewer.style.cursor = isZoomedIn ? 'grab' : 'zoom-in';
  602. }
  603. // Magnifier Logic
  604. viewer.addEventListener('mousemove', function(e) {
  605. if (!magnifierSwitch.checked) {
  606. magnifier.style.display = 'none';
  607. return;
  608. }
  609. if (isDragging) {
  610. magnifier.style.display = 'none';
  611. return;
  612. }
  613. if (!refImage) return;
  614. const rect = refImage.getBoundingClientRect();
  615. const x = e.clientX - rect.left;
  616. const y = e.clientY - rect.top;
  617. if (x < 0 || x > rect.width || y < 0 || y > rect.height) {
  618. magnifier.style.display = 'none';
  619. return;
  620. }
  621. magnifier.style.display = 'block';
  622. const glassOffset = 20;
  623. const viewerRect = viewer.getBoundingClientRect();
  624. magnifier.style.left = (e.clientX - viewerRect.left + glassOffset) + 'px';
  625. magnifier.style.top = (e.clientY - viewerRect.top + glassOffset) + 'px';
  626. const zoom = 2.5;
  627. magnifier.style.backgroundImage = `url('${refImage.src}')`;
  628. magnifier.style.backgroundSize = `${rect.width * zoom}px ${rect.height * zoom}px`;
  629. magnifier.style.backgroundPosition = `-${x * zoom - 75}px -${y * zoom - 75}px`;
  630. });
  631. function rotateImage(deg) {
  632. imgRotation = (imgRotation + deg) % 360;
  633. updateImageTransform();
  634. }
  635. function updateImageFilter() {
  636. imgBrightness = document.getElementById('brightnessRange').value;
  637. imgContrast = document.getElementById('contrastRange').value;
  638. applyImageFilters();
  639. }
  640. function resetFilters() {
  641. imgRotation = 0;
  642. imgBrightness = 100;
  643. imgContrast = 100;
  644. currentX = 0;
  645. currentY = 0;
  646. isZoomedIn = false;
  647. document.getElementById('brightnessRange').value = 100;
  648. document.getElementById('contrastRange').value = 100;
  649. if(magnifierSwitch) magnifierSwitch.checked = false;
  650. updateImageTransform();
  651. applyImageFilters();
  652. if(viewer) viewer.style.cursor = 'zoom-in';
  653. }
  654. function updateImageTransform() {
  655. if (imageWrapper) {
  656. const scale = isZoomedIn ? ZOOM_LEVEL : 1;
  657. imageWrapper.style.transform = `translate(calc(-50% + ${currentX}px), calc(-50% + ${currentY}px)) rotate(${imgRotation}deg) scale(${scale})`;
  658. }
  659. }
  660. function applyImageFilters() {
  661. if (refImage) {
  662. refImage.style.filter = `brightness(${imgBrightness}%) contrast(${imgContrast}%)`;
  663. }
  664. }
  665. </script>
  666. {% endblock %}