member_detail.html 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535
  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. /* Modal Image Viewer */
  45. .modal-image-container {
  46. height: 80vh;
  47. display: flex;
  48. flex-direction: column;
  49. }
  50. .image-toolbar {
  51. background: #e9ecef;
  52. padding: 5px 10px;
  53. border-bottom: 1px solid #dee2e6;
  54. display: flex;
  55. gap: 10px;
  56. align-items: center;
  57. flex-wrap: wrap;
  58. }
  59. .image-viewer {
  60. flex: 1;
  61. border: 1px solid #ccc;
  62. background: #f0f0f0;
  63. overflow: hidden;
  64. text-align: center;
  65. position: relative;
  66. cursor: grab;
  67. user-select: none;
  68. }
  69. .image-viewer:active {
  70. cursor: grabbing;
  71. }
  72. .image-wrapper {
  73. display: inline-block;
  74. transition: transform 0.2s ease-out;
  75. transform-origin: center center;
  76. position: absolute;
  77. top: 50%;
  78. left: 50%;
  79. transform: translate(-50%, -50%);
  80. }
  81. .image-wrapper img {
  82. max-width: 100%;
  83. max-height: 100%; /* Fit within viewer initially */
  84. display: block;
  85. pointer-events: none;
  86. user-select: none;
  87. transition: filter 0.2s;
  88. box-shadow: 0 0 20px rgba(0,0,0,0.1);
  89. }
  90. /* 放大镜样式 - Optional if we want it in modal too */
  91. .magnifier-glass {
  92. position: absolute;
  93. border: 3px solid #000;
  94. border-radius: 50%;
  95. cursor: none;
  96. width: 150px;
  97. height: 150px;
  98. box-shadow: 0 0 10px rgba(0,0,0,0.5);
  99. display: none;
  100. z-index: 1000;
  101. background-repeat: no-repeat;
  102. background-color: white;
  103. pointer-events: none;
  104. }
  105. </style>
  106. {% endblock %}
  107. {% block content %}
  108. <div class="container py-4">
  109. <div class="d-flex justify-content-between align-items-center mb-4">
  110. <h2><i class="bi bi-person-badge"></i> {{ member.name }} 的个人详情</h2>
  111. <div class="btn-group">
  112. <a href="{{ url_for('edit_member', member_id=member.id) }}" class="btn btn-primary">
  113. <i class="bi bi-pencil"></i> 编辑信息
  114. </a>
  115. <a href="{{ url_for('members') }}" class="btn btn-outline-secondary">返回列表</a>
  116. </div>
  117. </div>
  118. <div class="row">
  119. <!-- 基本与核心信息 -->
  120. <div class="col-md-8">
  121. <div class="detail-section">
  122. <div class="section-title">基本信息</div>
  123. <div class="row g-3">
  124. <div class="col-md-6">
  125. <span class="info-label">姓名(繁体):</span>
  126. <span class="info-value">{{ member.name }}</span>
  127. </div>
  128. <div class="col-md-6">
  129. <span class="info-label">姓名(简体):</span>
  130. <span class="info-value">{{ member.simplified_name or '-' }}</span>
  131. </div>
  132. <div class="col-md-6">
  133. <span class="info-label">性别:</span>
  134. <span class="info-value">{{ '男' if member.sex == 1 else '女' }}</span>
  135. </div>
  136. <div class="col-md-6">
  137. <span class="info-label">曾用名:</span>
  138. <span class="info-value">{{ member.former_name or '-' }}</span>
  139. </div>
  140. <div class="col-md-6">
  141. <span class="info-label">幼名/乳名:</span>
  142. <span class="info-value">{{ member.childhood_name or '-' }}</span>
  143. </div>
  144. <div class="col-md-6">
  145. <span class="info-label">出生日期:</span>
  146. <span class="info-value">{{ member.birthday_str or '未知' }}</span>
  147. </div>
  148. <div class="col-md-6">
  149. <span class="info-label">民族:</span>
  150. <span class="info-value">{{ member.nation or '-' }}</span>
  151. </div>
  152. <div class="col-md-6">
  153. <span class="info-label">状态:</span>
  154. <span class="info-value">
  155. {% if member.is_pass_away == 1 %}
  156. <span class="text-danger">已故</span>
  157. {% else %}
  158. <span class="text-success">健在</span>
  159. {% endif %}
  160. </span>
  161. </div>
  162. <div class="col-md-6">
  163. <span class="info-label">婚姻状况:</span>
  164. <span class="info-value">
  165. {% set marital_map = {0: '未知', 1: '未婚', 2: '已婚', 3: '离异/丧偶'} %}
  166. {{ marital_map.get(member.marital_status, '未知') }}
  167. </span>
  168. </div>
  169. </div>
  170. <div class="section-title mt-4">谱系资料</div>
  171. <div class="row g-3">
  172. <div class="col-md-6">
  173. <span class="info-label">字辈:</span>
  174. <span class="info-value">{{ member.name_word or '-' }}</span>
  175. </div>
  176. <div class="col-md-6">
  177. <span class="info-label">堂内排行:</span>
  178. <span class="info-value">{{ member.family_rank or '-' }}</span>
  179. </div>
  180. <div class="col-md-6">
  181. <span class="info-label">世系世代:</span>
  182. <span class="info-value">{{ member.name_word_generation or '-' }}</span>
  183. </div>
  184. <div class="col-md-6">
  185. <span class="info-label">名号/封号:</span>
  186. <span class="info-value">{{ member.name_title or '-' }}</span>
  187. </div>
  188. <div class="col-md-6">
  189. <span class="info-label">分房/堂号:</span>
  190. <span class="info-value">{{ member.branch_family_hall or '-' }}</span>
  191. </div>
  192. <div class="col-md-12">
  193. <span class="info-label">聚居地:</span>
  194. <span class="info-value">{{ member.cluster_place or '-' }}</span>
  195. </div>
  196. </div>
  197. <div class="section-title mt-4">联系与地址</div>
  198. <div class="row g-3">
  199. <div class="col-md-6">
  200. <span class="info-label">手机号:</span>
  201. <span class="info-value">{{ member.phone or '-' }}</span>
  202. </div>
  203. <div class="col-md-6">
  204. <span class="info-label">微信号:</span>
  205. <span class="info-value">{{ member.wechat_account or '-' }}</span>
  206. </div>
  207. <div class="col-md-12">
  208. <span class="info-label">现居住址:</span>
  209. <span class="info-value">{{ member.residential_address or '-' }}</span>
  210. </div>
  211. </div>
  212. </div>
  213. <div class="detail-section">
  214. <div class="section-title">个人履历与成就</div>
  215. <div class="mb-4">
  216. <label class="info-label d-block mb-2">标签:</label>
  217. <div class="p-3 bg-light rounded">{{ member.tags or '暂无' }}</div>
  218. </div>
  219. <div class="mb-4">
  220. <label class="info-label d-block mb-2">人员备注:</label>
  221. <div class="p-3 bg-light rounded">{{ member.notes or '暂无' }}</div>
  222. </div>
  223. <div class="mb-4">
  224. <label class="info-label d-block mb-2">职业背景:</label>
  225. <div class="p-3 bg-light rounded">{{ member.occupation or '暂无信息' }}</div>
  226. </div>
  227. <div class="mb-4">
  228. <label class="info-label d-block mb-2">教育经历:</label>
  229. <div class="p-3 bg-light rounded">{{ member.educational or '暂无信息' }}</div>
  230. </div>
  231. <div>
  232. <label class="info-label d-block mb-2">个人成就:</label>
  233. <div class="p-3 bg-light rounded">{{ member.personal_achievements or '暂无信息' }}</div>
  234. </div>
  235. </div>
  236. </div>
  237. <!-- 关系信息与原图 -->
  238. <div class="col-md-4">
  239. {% if member.source_image_url %}
  240. <div class="detail-section">
  241. <div class="section-title">来源原图</div>
  242. <div class="source-image-preview" onclick="openImageViewer()">
  243. <img src="{{ member.source_image_url }}" alt="来源家谱">
  244. <div class="preview-overlay">
  245. <i class="bi bi-arrows-fullscreen"></i> 点击查看大图 (第{{ member.source_page }}页)
  246. </div>
  247. </div>
  248. <div class="mt-3 small text-muted bg-light p-2 rounded">
  249. <div class="mb-1"><i class="bi bi-journal-text me-1"></i><strong>版本名称:</strong> {{ member.genealogy_version or '未提供' }}</div>
  250. <div class="mb-1"><i class="bi bi-archive me-1"></i><strong>版本来源:</strong> {{ member.genealogy_source or '未提供' }}</div>
  251. <div><i class="bi bi-person me-1"></i><strong>文件提供人:</strong> {{ member.upload_person or '未提供' }}</div>
  252. </div>
  253. </div>
  254. {% endif %}
  255. <div class="detail-section">
  256. <div class="section-title">家族关系</div>
  257. <h6 class="fw-bold mb-3"><i class="bi bi-arrow-up-circle me-1"></i> 尊辈/关联人</h6>
  258. {% for p in parents %}
  259. <div class="relation-card">
  260. <div class="d-flex justify-content-between align-items-center">
  261. <a href="{{ url_for('member_detail', member_id=p.id) }}" class="text-decoration-none fw-bold">
  262. {{ p.name }}
  263. </a>
  264. <span class="badge bg-success small">
  265. {% set rel_map = {1: '父亲', 2: '母亲', 10: '配偶', 11: '兄弟', 12: '姐妹'} %}
  266. {{ rel_map.get(p.relation_type, '关联人') }}
  267. </span>
  268. </div>
  269. </div>
  270. {% endfor %}
  271. {% if not parents %}
  272. <p class="text-muted small">暂无上层关系记录</p>
  273. {% endif %}
  274. <h6 class="fw-bold mt-4 mb-3"><i class="bi bi-arrow-down-circle me-1"></i> 子嗣/晚辈</h6>
  275. {% for c in children %}
  276. <div class="relation-card" style="border-left-color: #0d6efd; background: #f0f7ff;">
  277. <div class="d-flex justify-content-between align-items-center">
  278. <a href="{{ url_for('member_detail', member_id=c.id) }}" class="text-decoration-none fw-bold">
  279. {{ c.name }}
  280. </a>
  281. <span class="badge bg-primary small">
  282. {% set rel_map = {1: '子女', 2: '子女', 10: '配偶', 11: '兄弟', 12: '姐妹'} %}
  283. {{ rel_map.get(c.relation_type, '后辈') }}
  284. </span>
  285. </div>
  286. </div>
  287. {% endfor %}
  288. {% if not children %}
  289. <p class="text-muted small">暂无下层关系记录</p>
  290. {% endif %}
  291. </div>
  292. <div class="detail-section text-center">
  293. <div class="section-title">系统操作</div>
  294. <div class="small text-muted mb-3">
  295. 记录创建于:{{ member.create_time }}<br>
  296. 最后修改:{{ member.modified_time }}
  297. </div>
  298. <button class="btn btn-sm btn-outline-danger w-100" onclick="confirmDelete()">
  299. <i class="bi bi-trash"></i> 删除此成员
  300. </button>
  301. <form id="deleteForm" action="{{ url_for('delete_member', member_id=member.id) }}" method="POST" style="display: none;"></form>
  302. </div>
  303. </div>
  304. </div>
  305. </div>
  306. <!-- Image Viewer Modal -->
  307. <div class="modal fade" id="imageModal" tabindex="-1" aria-hidden="true">
  308. <div class="modal-dialog modal-fullscreen">
  309. <div class="modal-content bg-light">
  310. <div class="modal-header py-2">
  311. <h5 class="modal-title fs-6"><i class="bi bi-image"></i> 来源扫描件查看 - 第 {{ member.source_page }} 页</h5>
  312. <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
  313. </div>
  314. <div class="modal-body p-0 modal-image-container">
  315. <div class="image-toolbar">
  316. <div class="btn-group btn-group-sm">
  317. <button type="button" class="btn btn-outline-secondary" onclick="rotateImage(-90)" title="左旋90°"><i class="bi bi-arrow-counterclockwise"></i></button>
  318. <button type="button" class="btn btn-outline-secondary" onclick="rotateImage(90)" title="右旋90°"><i class="bi bi-arrow-clockwise"></i></button>
  319. </div>
  320. <div class="d-flex align-items-center gap-2 mx-3 border-start border-end px-3">
  321. <i class="bi bi-brightness-high" title="亮度"></i>
  322. <input type="range" class="form-range" min="50" max="150" value="100" id="brightnessRange" oninput="updateImageFilter()" style="width: 80px;">
  323. <i class="bi bi-circle-half ms-2" title="对比度"></i>
  324. <input type="range" class="form-range" min="50" max="200" value="100" id="contrastRange" oninput="updateImageFilter()" style="width: 80px;">
  325. <button class="btn btn-link btn-sm text-decoration-none py-0" onclick="resetFilters()">重置</button>
  326. </div>
  327. <div class="form-check form-switch ms-auto mb-0" title="开启后鼠标悬停图片可局部放大">
  328. <input class="form-check-input" type="checkbox" id="magnifierSwitch">
  329. <label class="form-check-label small" for="magnifierSwitch">🔍 放大镜</label>
  330. </div>
  331. </div>
  332. <div class="image-viewer shadow-inner" id="viewer">
  333. <div id="magnifier" class="magnifier-glass"></div>
  334. <div id="imageWrapper" class="image-wrapper">
  335. {% if member.source_image_url %}
  336. <img id="refImage" src="{{ member.source_image_url }}" alt="家谱图片" draggable="false">
  337. {% endif %}
  338. </div>
  339. </div>
  340. </div>
  341. </div>
  342. </div>
  343. </div>
  344. {% endblock %}
  345. {% block extra_js %}
  346. <script>
  347. function confirmDelete() {
  348. if(confirm('确定要删除此成员吗?\n这将同时删除其所有关联关系记录!')) {
  349. document.getElementById('deleteForm').submit();
  350. }
  351. }
  352. function openImageViewer() {
  353. var myModal = new bootstrap.Modal(document.getElementById('imageModal'));
  354. myModal.show();
  355. // Reset state when opening
  356. resetFilters();
  357. }
  358. // --- Image Viewer Logic (Reused from add_member) ---
  359. // Image State
  360. let imgRotation = 0;
  361. let imgBrightness = 100;
  362. let imgContrast = 100;
  363. // Dragging State
  364. let isDragging = false;
  365. let hasDragged = false;
  366. let startX = 0, startY = 0;
  367. let currentX = 0, currentY = 0; // Relative to center
  368. // Zoom State
  369. let isZoomedIn = false;
  370. const ZOOM_LEVEL = 2.0;
  371. // Elements
  372. const viewer = document.getElementById('viewer');
  373. const magnifier = document.getElementById('magnifier');
  374. const magnifierSwitch = document.getElementById('magnifierSwitch');
  375. const imageWrapper = document.getElementById('imageWrapper');
  376. const refImage = document.getElementById('refImage');
  377. // Initialize Dragging and Zooming
  378. if (imageWrapper) {
  379. // Center initial position logic is handled by CSS (top 50% left 50% translate -50%)
  380. viewer.style.cursor = 'zoom-in';
  381. viewer.addEventListener('mousedown', (e) => {
  382. if (e.target.closest('.image-toolbar') || e.target.closest('.magnifier-glass')) return;
  383. isDragging = true;
  384. hasDragged = false;
  385. startX = e.clientX;
  386. startY = e.clientY;
  387. viewer.style.cursor = 'grabbing';
  388. e.preventDefault();
  389. });
  390. window.addEventListener('mousemove', (e) => {
  391. if (!isDragging) return;
  392. const dx = e.clientX - startX;
  393. const dy = e.clientY - startY;
  394. if (Math.abs(dx) > 2 || Math.abs(dy) > 2) {
  395. hasDragged = true;
  396. }
  397. currentX += dx;
  398. currentY += dy;
  399. startX = e.clientX;
  400. startY = e.clientY;
  401. updateImageTransform();
  402. });
  403. window.addEventListener('mouseup', (e) => {
  404. if (isDragging) {
  405. isDragging = false;
  406. viewer.style.cursor = isZoomedIn ? 'grab' : 'zoom-in';
  407. if (!hasDragged && viewer.contains(e.target)) {
  408. toggleZoom();
  409. }
  410. }
  411. });
  412. }
  413. function toggleZoom() {
  414. isZoomedIn = !isZoomedIn;
  415. if (!isZoomedIn) {
  416. currentX = 0;
  417. currentY = 0;
  418. }
  419. updateImageTransform();
  420. viewer.style.cursor = isZoomedIn ? 'grab' : 'zoom-in';
  421. }
  422. // Magnifier Logic
  423. viewer.addEventListener('mousemove', function(e) {
  424. if (!magnifierSwitch.checked) {
  425. magnifier.style.display = 'none';
  426. return;
  427. }
  428. if (isDragging) {
  429. magnifier.style.display = 'none';
  430. return;
  431. }
  432. if (!refImage) return;
  433. const rect = refImage.getBoundingClientRect();
  434. const x = e.clientX - rect.left;
  435. const y = e.clientY - rect.top;
  436. if (x < 0 || x > rect.width || y < 0 || y > rect.height) {
  437. magnifier.style.display = 'none';
  438. return;
  439. }
  440. magnifier.style.display = 'block';
  441. const glassOffset = 20;
  442. const viewerRect = viewer.getBoundingClientRect();
  443. magnifier.style.left = (e.clientX - viewerRect.left + glassOffset) + 'px';
  444. magnifier.style.top = (e.clientY - viewerRect.top + glassOffset) + 'px';
  445. const zoom = 2.5;
  446. magnifier.style.backgroundImage = `url('${refImage.src}')`;
  447. magnifier.style.backgroundSize = `${rect.width * zoom}px ${rect.height * zoom}px`;
  448. magnifier.style.backgroundPosition = `-${x * zoom - 75}px -${y * zoom - 75}px`;
  449. });
  450. function rotateImage(deg) {
  451. imgRotation = (imgRotation + deg) % 360;
  452. updateImageTransform();
  453. }
  454. function updateImageFilter() {
  455. imgBrightness = document.getElementById('brightnessRange').value;
  456. imgContrast = document.getElementById('contrastRange').value;
  457. applyImageFilters();
  458. }
  459. function resetFilters() {
  460. imgRotation = 0;
  461. imgBrightness = 100;
  462. imgContrast = 100;
  463. currentX = 0;
  464. currentY = 0;
  465. isZoomedIn = false;
  466. document.getElementById('brightnessRange').value = 100;
  467. document.getElementById('contrastRange').value = 100;
  468. if(magnifierSwitch) magnifierSwitch.checked = false;
  469. updateImageTransform();
  470. applyImageFilters();
  471. if(viewer) viewer.style.cursor = 'zoom-in';
  472. }
  473. function updateImageTransform() {
  474. if (imageWrapper) {
  475. const scale = isZoomedIn ? ZOOM_LEVEL : 1;
  476. imageWrapper.style.transform = `translate(calc(-50% + ${currentX}px), calc(-50% + ${currentY}px)) rotate(${imgRotation}deg) scale(${scale})`;
  477. }
  478. }
  479. function applyImageFilters() {
  480. if (refImage) {
  481. refImage.style.filter = `brightness(${imgBrightness}%) contrast(${imgContrast}%)`;
  482. }
  483. }
  484. </script>
  485. {% endblock %}