settlements.html 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856
  1. {% extends 'layout.html' %}
  2. {% block content %}
  3. <script>
  4. window.isSuperAdmin = {{ session.get('is_super_admin') | tojson }};
  5. </script>
  6. <script type="text/javascript">
  7. window.AMapPromise = new Promise(function(resolve, reject) {
  8. if (typeof AMap !== 'undefined') {
  9. resolve(AMap);
  10. return;
  11. }
  12. const script = document.createElement('script');
  13. script.src = 'https://webapi.amap.com/maps?v=2.0&key=7bbc2afa6f49b58024780bf647c6f038';
  14. script.onload = function() { resolve(AMap); };
  15. script.onerror = function() { reject('高德地图加载失败'); };
  16. document.head.appendChild(script);
  17. });
  18. </script>
  19. <div class="container-fluid">
  20. <div class="row">
  21. <div class="col-md-12">
  22. <div class="d-flex justify-content-between align-items-center mb-4">
  23. <h2 class="page-header">聚落地图</h2>
  24. <div class="d-flex gap-2">
  25. <div class="btn-group" role="group">
  26. <button type="button" class="btn btn-primary" id="view-map-btn">地图查看</button>
  27. <button type="button" class="btn btn-outline-primary" id="view-list-btn">列表查看</button>
  28. </div>
  29. {% if session.get('is_super_admin') %}
  30. <button type="button" class="btn btn-success" data-bs-toggle="modal" data-bs-target="#addSettlementModal">
  31. <i class="bi bi-plus-circle"></i> 添加聚落
  32. </button>
  33. {% endif %}
  34. </div>
  35. </div>
  36. <div id="map-view" class="mb-4" style="display: block;">
  37. <div id="map-container" style="min-height: 500px; max-height: 80vh; border-radius: 12px; border: 1px solid #e2e8f0; overflow: hidden;">
  38. <div class="map-loading" style="position: absolute; inset: 0; background: linear-gradient(135deg, #e0f2fe 0%, #f0f9ff 100%); display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 100;">
  39. <div class="spinner-border text-primary" role="status">
  40. <span class="visually-hidden">加载中...</span>
  41. </div>
  42. <p class="mt-3 text-gray-500">地图加载中...</p>
  43. </div>
  44. </div>
  45. </div>
  46. <div id="list-view" class="mb-4" style="display: none;">
  47. <div class="card">
  48. <div class="card-body">
  49. <div class="table-responsive">
  50. <table class="table table-hover">
  51. <thead>
  52. <tr>
  53. <th>编号</th>
  54. <th>聚落名称</th>
  55. <th>所属区域</th>
  56. <th>人数</th>
  57. <th>代表人物</th>
  58. <th>姓氏类型</th>
  59. <th>创建时间</th>
  60. {% if session.get('is_super_admin') %}
  61. <th>操作</th>
  62. {% endif %}
  63. </tr>
  64. </thead>
  65. <tbody id="settlements-table-body">
  66. </tbody>
  67. </table>
  68. </div>
  69. </div>
  70. </div>
  71. </div>
  72. </div>
  73. </div>
  74. </div>
  75. <div class="modal fade" id="addSettlementModal" tabindex="-1" aria-labelledby="addSettlementModalLabel" aria-hidden="true">
  76. <div class="modal-dialog modal-lg">
  77. <div class="modal-content">
  78. <div class="modal-header">
  79. <h5 class="modal-title" id="addSettlementModalLabel">添加聚落</h5>
  80. <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
  81. </div>
  82. <div class="modal-body">
  83. <form id="settlement-form">
  84. <input type="hidden" id="settlement-id">
  85. <div class="row g-3">
  86. <div class="col-md-6">
  87. <label class="form-label">聚落名称 *</label>
  88. <input type="text" class="form-control" id="settlement-name" required>
  89. </div>
  90. <div class="col-md-6">
  91. <label class="form-label">所属区域 *</label>
  92. <div class="input-group">
  93. <input type="text" class="form-control" id="settlement-region" placeholder="搜索城市或区域名称" required>
  94. <button type="button" class="btn btn-outline-primary" id="search-region-btn">
  95. <i class="bi bi-search"></i>
  96. </button>
  97. </div>
  98. <div id="region-suggestions" class="mt-2" style="display: none;"></div>
  99. </div>
  100. <div class="col-md-6 d-none">
  101. <label class="form-label">纬度</label>
  102. <input type="number" class="form-control" id="settlement-latitude" step="0.0000001">
  103. </div>
  104. <div class="col-md-6 d-none">
  105. <label class="form-label">经度</label>
  106. <input type="number" class="form-control" id="settlement-longitude" step="0.0000001">
  107. </div>
  108. <div class="col-md-12">
  109. <div class="alert alert-info alert-dismissible fade show" id="location-info" style="display: none;">
  110. <i class="bi bi-info-circle me-2"></i>
  111. <span id="location-info-text"></span>
  112. <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
  113. </div>
  114. </div>
  115. <div class="col-md-6">
  116. <label class="form-label">聚落人数</label>
  117. <input type="number" class="form-control" id="settlement-population" min="0">
  118. </div>
  119. <div class="col-md-6">
  120. <label class="form-label">代表人物</label>
  121. <div class="input-group">
  122. <input type="text" id="settlement-representative-display" class="form-control" placeholder="点击选择代表人物" readonly>
  123. <input type="hidden" id="settlement-representative-id">
  124. <button type="button" class="btn btn-outline-primary" data-bs-toggle="modal" data-bs-target="#memberSelectModal">
  125. <i class="bi bi-search"></i>
  126. </button>
  127. </div>
  128. </div>
  129. <div class="col-md-6">
  130. <label class="form-label">姓氏类型</label>
  131. <select class="form-select" id="settlement-surname-type">
  132. <option value="0">留姓</option>
  133. <option value="1">改姓</option>
  134. </select>
  135. </div>
  136. <div class="col-md-6" id="new-surname-container" style="display: none;">
  137. <label class="form-label">改后姓氏</label>
  138. <input type="text" class="form-control" id="settlement-new-surname" placeholder="输入改后的姓氏">
  139. </div>
  140. <div class="col-md-12">
  141. <label class="form-label">备注说明</label>
  142. <textarea class="form-control" id="settlement-description" rows="3"></textarea>
  143. </div>
  144. </div>
  145. </form>
  146. </div>
  147. <div class="modal-footer">
  148. <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
  149. <button type="button" class="btn btn-primary" id="save-settlement-btn">保存</button>
  150. </div>
  151. </div>
  152. </div>
  153. </div>
  154. <div class="modal fade" id="memberSelectModal" tabindex="-1" aria-labelledby="memberSelectModalLabel" aria-hidden="true">
  155. <div class="modal-dialog modal-lg">
  156. <div class="modal-content">
  157. <div class="modal-header">
  158. <h5 class="modal-title" id="memberSelectModalLabel">选择代表人物</h5>
  159. <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
  160. </div>
  161. <div class="modal-body">
  162. <div class="mb-3">
  163. <input type="text" id="member-search-input" class="form-control" placeholder="搜索成员名称...">
  164. </div>
  165. <div id="member-list-container" style="max-height: 400px; overflow-y: auto;">
  166. </div>
  167. </div>
  168. <div class="modal-footer">
  169. <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
  170. </div>
  171. </div>
  172. </div>
  173. </div>
  174. <div class="modal fade" id="settlementDetailModal" tabindex="-1" aria-labelledby="settlementDetailModalLabel" aria-hidden="true">
  175. <div class="modal-dialog">
  176. <div class="modal-content">
  177. <div class="modal-header">
  178. <h5 class="modal-title" id="settlementDetailModalLabel">聚落详情</h5>
  179. <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
  180. </div>
  181. <div class="modal-body" id="settlement-detail-content">
  182. </div>
  183. <div class="modal-footer">
  184. <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
  185. {% if session.get('is_super_admin') %}
  186. <button type="button" class="btn btn-primary" id="edit-settlement-btn">编辑</button>
  187. {% endif %}
  188. </div>
  189. </div>
  190. </div>
  191. </div>
  192. <script>
  193. let map = null;
  194. let markers = [];
  195. let settlementsData = [];
  196. function resizeMapContainer() {
  197. const mapContainer = document.getElementById('map-container');
  198. const headerHeight = document.querySelector('.page-header')?.offsetHeight || 80;
  199. const toolbarHeight = 60;
  200. const padding = 40;
  201. const windowHeight = window.innerHeight;
  202. const sidebarWidth = 250;
  203. const availableHeight = windowHeight - headerHeight - toolbarHeight - padding;
  204. if (window.innerWidth > 768) {
  205. mapContainer.style.height = Math.max(500, Math.min(availableHeight, windowHeight * 0.85)) + 'px';
  206. } else {
  207. mapContainer.style.height = Math.max(400, availableHeight * 0.8) + 'px';
  208. }
  209. if (window.map) {
  210. window.map.resize();
  211. }
  212. }
  213. document.addEventListener('DOMContentLoaded', function() {
  214. loadSettlements();
  215. initRegionSearch();
  216. resizeMapContainer();
  217. window.addEventListener('resize', resizeMapContainer);
  218. document.getElementById('addSettlementModal').addEventListener('show.bs.modal', function() {
  219. if (!window.isEditingSettlement) {
  220. resetSettlementForm();
  221. }
  222. window.isEditingSettlement = false;
  223. });
  224. document.getElementById('view-map-btn').addEventListener('click', function() {
  225. document.getElementById('map-view').style.display = 'block';
  226. document.getElementById('list-view').style.display = 'none';
  227. this.classList.add('btn-primary');
  228. this.classList.remove('btn-outline-primary');
  229. document.getElementById('view-list-btn').classList.add('btn-outline-primary');
  230. document.getElementById('view-list-btn').classList.remove('btn-primary');
  231. });
  232. document.getElementById('view-list-btn').addEventListener('click', function() {
  233. document.getElementById('map-view').style.display = 'none';
  234. document.getElementById('list-view').style.display = 'block';
  235. this.classList.add('btn-primary');
  236. this.classList.remove('btn-outline-primary');
  237. document.getElementById('view-map-btn').classList.add('btn-outline-primary');
  238. document.getElementById('view-map-btn').classList.remove('btn-primary');
  239. });
  240. document.getElementById('member-search-input').addEventListener('input', function() {
  241. searchMembers(this.value);
  242. });
  243. document.getElementById('save-settlement-btn').addEventListener('click', saveSettlement);
  244. document.getElementById('edit-settlement-btn').addEventListener('click', function() {
  245. const id = document.getElementById('settlement-id').value;
  246. if (id) {
  247. loadSettlementForEdit(id);
  248. }
  249. const modal = bootstrap.Modal.getInstance(document.getElementById('settlementDetailModal'));
  250. modal.hide();
  251. });
  252. document.getElementById('settlement-surname-type').addEventListener('change', function() {
  253. toggleNewSurnameField(this.value);
  254. });
  255. });
  256. function loadSettlements() {
  257. fetch('/manager/api/settlements', { credentials: 'include' })
  258. .then(response => response.json())
  259. .then(data => {
  260. if (data.success) {
  261. settlementsData = data.settlements;
  262. renderSettlementList(data.settlements);
  263. initMapWithData(data.settlements);
  264. }
  265. })
  266. .catch(error => {
  267. console.error('加载聚落失败:', error);
  268. });
  269. }
  270. function initMapWithData(settlements) {
  271. window.AMapPromise.then(function(AMap) {
  272. document.querySelector('.map-loading').style.display = 'none';
  273. if (!map) {
  274. let initialCenter = [116.397428, 39.90923];
  275. let initialZoom = 4;
  276. if (settlements.length > 0) {
  277. const firstLng = parseFloat(settlements[0].longitude) || 116.397428;
  278. const firstLat = parseFloat(settlements[0].latitude) || 39.90923;
  279. initialCenter = [firstLng, firstLat];
  280. initialZoom = settlements.length === 1 ? 10 : 6;
  281. }
  282. map = new AMap.Map('map-container', {
  283. center: initialCenter,
  284. zoom: initialZoom,
  285. resizeEnable: true,
  286. mapStyle: 'amap://styles/normal',
  287. features: ['bg', 'road', 'building', 'point', 'label'],
  288. viewMode: '2D'
  289. });
  290. AMap.plugin(['AMap.Scale', 'AMap.ToolBar', 'AMap.LabelsLayer'], function() {
  291. map.addControl(new AMap.Scale({ position: 'LB' }));
  292. map.addControl(new AMap.ToolBar({ position: 'RB' }));
  293. });
  294. }
  295. clearMarkers();
  296. if (settlements.length > 0) {
  297. const bounds = new AMap.Bounds();
  298. settlements.forEach(settlement => {
  299. const lng = parseFloat(settlement.longitude) || 116.397428;
  300. const lat = parseFloat(settlement.latitude) || 39.90923;
  301. const circle = new AMap.Circle({
  302. center: [lng, lat],
  303. radius: 50000,
  304. strokeColor: '#3B82F6',
  305. strokeOpacity: 0.8,
  306. strokeWeight: 3,
  307. fillColor: '#3B82F6',
  308. fillOpacity: 0.15,
  309. cursor: 'pointer'
  310. });
  311. circle.settlementData = settlement;
  312. circle.on('click', function() { showSettlementDetail(settlement); });
  313. circle.on('mouseover', function(e) {
  314. circle.setOptions({
  315. fillOpacity: 0.25,
  316. strokeOpacity: 1
  317. });
  318. showMarkerTooltip(e, settlement);
  319. });
  320. circle.on('mouseout', function() {
  321. circle.setOptions({
  322. fillOpacity: 0.15,
  323. strokeOpacity: 0.8
  324. });
  325. hideMarkerTooltip();
  326. });
  327. map.add(circle);
  328. markers.push(circle);
  329. bounds.extend([lng, lat]);
  330. });
  331. if (settlements.length === 1) {
  332. const lng = parseFloat(settlements[0].longitude) || 116.397428;
  333. const lat = parseFloat(settlements[0].latitude) || 39.90923;
  334. map.setCenter([lng, lat]);
  335. map.setZoom(10);
  336. } else {
  337. setTimeout(function() {
  338. map.setFitView(bounds, false, [50, 50, 50, 50]);
  339. }, 100);
  340. }
  341. }
  342. }).catch(function(err) {
  343. console.error('高德地图加载失败:', err);
  344. document.querySelector('.map-loading p').textContent = '地图加载失败';
  345. });
  346. }
  347. function clearMarkers() {
  348. markers.forEach(marker => { map.remove(marker); });
  349. markers = [];
  350. }
  351. function renderSettlementList(settlements) {
  352. const tbody = document.getElementById('settlements-table-body');
  353. tbody.innerHTML = '';
  354. if (settlements.length === 0) {
  355. tbody.innerHTML = '<tr><td colspan="8" class="text-center text-gray-500">暂无聚落数据</td></tr>';
  356. return;
  357. }
  358. settlements.forEach((settlement, index) => {
  359. const surnameType = getSurnameTypeText(settlement.surname_type);
  360. const newSurname = settlement.new_surname ? '(' + settlement.new_surname + ')' : '';
  361. const row = document.createElement('tr');
  362. row.innerHTML = `
  363. <td>${index + 1}</td>
  364. <td>${settlement.name}</td>
  365. <td>${settlement.region || '-'}</td>
  366. <td>${settlement.population || 0}</td>
  367. <td>${settlement.representative_name || '-'}</td>
  368. <td>${surnameType}${newSurname}</td>
  369. <td>${formatDateTime(settlement.created_at)}</td>
  370. ${window.isSuperAdmin ? `
  371. <td>
  372. <button class="btn btn-sm btn-primary edit-settlement-btn" data-id="${settlement.id}">编辑</button>
  373. <button class="btn btn-sm btn-danger delete-settlement-btn" data-id="${settlement.id}">删除</button>
  374. </td>
  375. ` : ''}
  376. `;
  377. row.addEventListener('click', function() { showSettlementDetail(settlement); });
  378. tbody.appendChild(row);
  379. });
  380. document.querySelectorAll('.edit-settlement-btn').forEach(btn => {
  381. btn.addEventListener('click', function(e) {
  382. e.stopPropagation();
  383. loadSettlementForEdit(this.dataset.id);
  384. });
  385. });
  386. document.querySelectorAll('.delete-settlement-btn').forEach(btn => {
  387. btn.addEventListener('click', function(e) {
  388. e.stopPropagation();
  389. if (confirm('确定要删除这个聚落吗?')) {
  390. deleteSettlement(this.dataset.id);
  391. }
  392. });
  393. });
  394. }
  395. function showSettlementDetail(settlement) {
  396. document.getElementById('settlement-id').value = settlement.id;
  397. const surnameType = getSurnameTypeText(settlement.surname_type);
  398. const content = document.getElementById('settlement-detail-content');
  399. content.innerHTML = `
  400. <div class="mb-4">
  401. <h4>${settlement.name}</h4>
  402. ${settlement.region ? '<p class="text-muted">区域:' + settlement.region + '</p>' : ''}
  403. </div>
  404. <div class="row">
  405. <div class="col-md-6">
  406. <div class="card">
  407. <div class="card-body">
  408. <h6 class="card-title">基本信息</h6>
  409. <p class="card-text"><strong>人数:</strong>${settlement.population || 0} 人</p>
  410. ${settlement.latitude ? '<p class="card-text"><strong>纬度:</strong>' + settlement.latitude + '</p>' : ''}
  411. ${settlement.longitude ? '<p class="card-text"><strong>经度:</strong>' + settlement.longitude + '</p>' : ''}
  412. <p class="card-text"><strong>姓氏类型:</strong>${surnameType}${settlement.new_surname ? '(改后:' + settlement.new_surname + ')' : ''}</p>
  413. </div>
  414. </div>
  415. </div>
  416. <div class="col-md-6">
  417. <div class="card">
  418. <div class="card-body">
  419. <h6 class="card-title">代表人物</h6>
  420. <p class="card-text">${settlement.representative_name || '未设置'}</p>
  421. </div>
  422. </div>
  423. </div>
  424. </div>
  425. ${settlement.description ? '<div class="mt-4"><h6>备注说明</h6><p>' + settlement.description + '</p></div>' : ''}
  426. <div class="mt-4 text-muted text-sm">
  427. 创建时间:${formatDateTime(settlement.created_at)}
  428. ${settlement.updated_at !== settlement.created_at ? '<br>更新时间:' + formatDateTime(settlement.updated_at) : ''}
  429. </div>
  430. `;
  431. const modal = new bootstrap.Modal(document.getElementById('settlementDetailModal'));
  432. modal.show();
  433. }
  434. function showSettlementDetailById(id) {
  435. fetch('/manager/api/settlements/' + id)
  436. .then(response => response.json())
  437. .then(data => {
  438. if (data.success) {
  439. showSettlementDetail(data.settlement);
  440. }
  441. });
  442. }
  443. function loadSettlementForEdit(id) {
  444. fetch('/manager/api/settlements/' + id)
  445. .then(response => response.json())
  446. .then(data => {
  447. if (data.success) {
  448. const s = data.settlement;
  449. document.getElementById('settlement-id').value = s.id;
  450. document.getElementById('settlement-name').value = s.name;
  451. document.getElementById('settlement-region').value = s.region || '';
  452. document.getElementById('settlement-latitude').value = s.latitude || '';
  453. document.getElementById('settlement-longitude').value = s.longitude || '';
  454. document.getElementById('settlement-population').value = s.population || '';
  455. document.getElementById('settlement-representative-id').value = s.representative_id || '';
  456. document.getElementById('settlement-representative-display').value = s.representative_name || '';
  457. document.getElementById('settlement-description').value = s.description || '';
  458. document.getElementById('settlement-surname-type').value = s.surname_type || 0;
  459. document.getElementById('settlement-new-surname').value = s.new_surname || '';
  460. toggleNewSurnameField(s.surname_type);
  461. document.getElementById('addSettlementModalLabel').textContent = '编辑聚落';
  462. if (s.region && s.latitude) {
  463. document.getElementById('location-info-text').innerHTML = '已选择区域:' + s.region + ',坐标:' + s.latitude + ', ' + s.longitude;
  464. document.getElementById('location-info').style.display = 'block';
  465. }
  466. window.isEditingSettlement = true;
  467. const modal = new bootstrap.Modal(document.getElementById('addSettlementModal'));
  468. modal.show();
  469. }
  470. });
  471. }
  472. function resetSettlementForm() {
  473. document.getElementById('settlement-id').value = '';
  474. document.getElementById('settlement-name').value = '';
  475. document.getElementById('settlement-region').value = '';
  476. document.getElementById('settlement-latitude').value = '';
  477. document.getElementById('settlement-longitude').value = '';
  478. document.getElementById('settlement-population').value = '';
  479. document.getElementById('settlement-representative-id').value = '';
  480. document.getElementById('settlement-representative-display').value = '';
  481. document.getElementById('settlement-description').value = '';
  482. document.getElementById('settlement-surname-type').value = '0';
  483. document.getElementById('settlement-new-surname').value = '';
  484. document.getElementById('location-info').style.display = 'none';
  485. document.getElementById('region-suggestions').style.display = 'none';
  486. document.getElementById('new-surname-container').style.display = 'none';
  487. document.getElementById('addSettlementModalLabel').textContent = '添加聚落';
  488. }
  489. function saveSettlement() {
  490. const data = {
  491. id: document.getElementById('settlement-id').value,
  492. name: document.getElementById('settlement-name').value,
  493. region: document.getElementById('settlement-region').value,
  494. latitude: document.getElementById('settlement-latitude').value,
  495. longitude: document.getElementById('settlement-longitude').value,
  496. population: document.getElementById('settlement-population').value,
  497. representative_id: document.getElementById('settlement-representative-id').value,
  498. description: document.getElementById('settlement-description').value,
  499. surname_type: document.getElementById('settlement-surname-type').value,
  500. new_surname: document.getElementById('settlement-new-surname').value
  501. };
  502. const url = data.id ? '/manager/api/settlements/' + data.id : '/manager/api/settlements';
  503. const method = data.id ? 'PUT' : 'POST';
  504. fetch(url, {
  505. method: method,
  506. headers: { 'Content-Type': 'application/json' },
  507. body: JSON.stringify(data),
  508. credentials: 'include'
  509. })
  510. .then(response => response.json())
  511. .then(data => {
  512. if (data.success) {
  513. alert('保存成功!');
  514. const modal = bootstrap.Modal.getInstance(document.getElementById('addSettlementModal'));
  515. modal.hide();
  516. loadSettlements();
  517. } else {
  518. alert(data.message || '保存失败');
  519. }
  520. })
  521. .catch(error => {
  522. console.error('保存失败:', error);
  523. alert('保存失败,请检查网络连接');
  524. });
  525. }
  526. function deleteSettlement(id) {
  527. fetch('/manager/api/settlements/' + id, {
  528. method: 'DELETE',
  529. credentials: 'include'
  530. })
  531. .then(response => response.json())
  532. .then(data => {
  533. if (data.success) {
  534. loadSettlements();
  535. } else {
  536. alert(data.message || '删除失败');
  537. }
  538. });
  539. }
  540. function searchMembers(keyword) {
  541. fetch('/manager/api/search_member', {
  542. method: 'POST',
  543. headers: { 'Content-Type': 'application/json' },
  544. body: JSON.stringify({ keyword: keyword })
  545. })
  546. .then(response => response.json())
  547. .then(data => {
  548. const container = document.getElementById('member-list-container');
  549. if (data.success && data.members.length > 0) {
  550. container.innerHTML = data.members.map(member => {
  551. const simplified = member.simplified_name && member.simplified_name !== member.name ? '(' + member.simplified_name + ')' : '';
  552. return '<div class="member-item p-3 border-bottom cursor-pointer hover:bg-gray-50" onclick="selectRepresentative(' + member.id + ', \'' + member.name + '\', \'' + (member.simplified_name || '') + '\')">' +
  553. '<div style="font-weight: 500;">' + member.name + '</div>' +
  554. (member.simplified_name ? '<div style="font-size: 12px; color: #64748b;">' + simplified + '</div>' : '') +
  555. '</div>';
  556. }).join('');
  557. } else {
  558. container.innerHTML = '<div class="text-center py-4 text-gray-500">未找到匹配的成员</div>';
  559. }
  560. });
  561. }
  562. function selectRepresentative(id, name, simplifiedName) {
  563. document.getElementById('settlement-representative-id').value = id;
  564. let displayText = name;
  565. if (simplifiedName && simplifiedName !== name) {
  566. displayText += ' (' + simplifiedName + ')';
  567. }
  568. document.getElementById('settlement-representative-display').value = displayText;
  569. const modal = bootstrap.Modal.getInstance(document.getElementById('memberSelectModal'));
  570. modal.hide();
  571. }
  572. function formatDateTime(dateStr) {
  573. if (!dateStr) return '-';
  574. const date = new Date(dateStr);
  575. return date.toLocaleString('zh-CN');
  576. }
  577. function initRegionSearch() {
  578. const regionInput = document.getElementById('settlement-region');
  579. const suggestionsDiv = document.getElementById('region-suggestions');
  580. const searchBtn = document.getElementById('search-region-btn');
  581. searchBtn.addEventListener('click', function() {
  582. const keyword = regionInput.value.trim();
  583. if (keyword) {
  584. searchRegion(keyword);
  585. }
  586. });
  587. let searchTimeout = null;
  588. regionInput.addEventListener('input', function() {
  589. const keyword = this.value.trim();
  590. if (searchTimeout) clearTimeout(searchTimeout);
  591. if (keyword.length >= 2) {
  592. searchTimeout = setTimeout(() => { searchRegion(keyword); }, 300);
  593. } else {
  594. suggestionsDiv.style.display = 'none';
  595. }
  596. });
  597. regionInput.addEventListener('keypress', function(e) {
  598. if (e.key === 'Enter') {
  599. e.preventDefault();
  600. const keyword = this.value.trim();
  601. if (keyword) {
  602. searchRegion(keyword);
  603. }
  604. }
  605. });
  606. document.addEventListener('click', function(e) {
  607. if (!e.target.closest('#settlement-region') && !e.target.closest('#search-region-btn') && !e.target.closest('#region-suggestions')) {
  608. suggestionsDiv.style.display = 'none';
  609. }
  610. });
  611. }
  612. function searchRegion(keyword) {
  613. const suggestionsDiv = document.getElementById('region-suggestions');
  614. window.AMapPromise.then(function(AMap) {
  615. AMap.plugin(['AMap.PlaceSearch'], function() {
  616. const placeSearch = new AMap.PlaceSearch({
  617. city: '全国',
  618. type: 'district',
  619. pageSize: 10
  620. });
  621. placeSearch.search(keyword, function(status, result) {
  622. if (status === 'complete' && result && result.poiList && result.poiList.pois.length > 0) {
  623. const pois = result.poiList.pois;
  624. const formattedData = pois.map(poi => ({
  625. name: poi.name,
  626. adcode: poi.adcode,
  627. address: poi.address || poi.name,
  628. location: {
  629. lng: poi.location.lng || poi.location[0],
  630. lat: poi.location.lat || poi.location[1]
  631. }
  632. }));
  633. renderRegionSuggestions(formattedData);
  634. } else {
  635. searchRegionFallback(keyword);
  636. }
  637. });
  638. });
  639. }).catch(function() {
  640. searchRegionFallback(keyword);
  641. });
  642. }
  643. function searchRegionFallback(keyword) {
  644. const suggestionsDiv = document.getElementById('region-suggestions');
  645. const localData = [
  646. { name: '泉州市鲤城区', adcode: '350502', address: '福建省泉州市鲤城区', location: { lng: 118.58809, lat: 24.90757 } },
  647. { name: '泉州市丰泽区', adcode: '350503', address: '福建省泉州市丰泽区', location: { lng: 118.62062, lat: 24.88269 } },
  648. { name: '泉州市洛江区', adcode: '350504', address: '福建省泉州市洛江区', location: { lng: 118.74387, lat: 24.92516 } },
  649. { name: '泉州市泉港区', adcode: '350505', address: '福建省泉州市泉港区', location: { lng: 118.96936, lat: 25.13699 } },
  650. { name: '泉州市永春县', adcode: '350525', address: '福建省泉州市永春县', location: { lng: 118.38267, lat: 25.34437 } },
  651. { name: '泉州市安溪县', adcode: '350524', address: '福建省泉州市安溪县', location: { lng: 118.18598, lat: 25.06531 } },
  652. { name: '泉州市德化县', adcode: '350526', address: '福建省泉州市德化县', location: { lng: 118.15722, lat: 25.53543 } },
  653. { name: '泉州市金门县', adcode: '350527', address: '福建省泉州市金门县', location: { lng: 118.32556, lat: 24.43788 } },
  654. { name: '泉州市南安市', adcode: '350583', address: '福建省泉州市南安市', location: { lng: 118.32211, lat: 24.96694 } },
  655. { name: '泉州市晋江市', adcode: '350582', address: '福建省泉州市晋江市', location: { lng: 118.56388, lat: 24.80951 } },
  656. { name: '泉州市石狮市', adcode: '350581', address: '福建省泉州市石狮市', location: { lng: 118.65021, lat: 24.74517 } },
  657. { name: '漳州市芗城区', adcode: '350602', address: '福建省漳州市芗城区', location: { lng: 117.62588, lat: 24.61406 } },
  658. { name: '漳州市龙文区', adcode: '350603', address: '福建省漳州市龙文区', location: { lng: 117.71692, lat: 24.62148 } },
  659. { name: '漳州市龙海市', adcode: '350681', address: '福建省漳州市龙海市', location: { lng: 117.80713, lat: 24.47173 } },
  660. { name: '漳州市华安县', adcode: '350629', address: '福建省漳州市华安县', location: { lng: 117.4898, lat: 24.7568 } },
  661. { name: '厦门市思明区', adcode: '350203', address: '福建省厦门市思明区', location: { lng: 118.08942, lat: 24.47977 } },
  662. { name: '厦门市湖里区', adcode: '350206', address: '福建省厦门市湖里区', location: { lng: 118.14991, lat: 24.52864 } },
  663. { name: '厦门市同安区', adcode: '350212', address: '福建省厦门市同安区', location: { lng: 118.16728, lat: 24.72657 } },
  664. { name: '厦门市翔安区', adcode: '350213', address: '福建省厦门市翔安区', location: { lng: 118.26543, lat: 24.63788 } },
  665. { name: '厦门市集美区', adcode: '350211', address: '福建省厦门市集美区', location: { lng: 118.09767, lat: 24.60224 } },
  666. { name: '厦门市海沧区', adcode: '350205', address: '福建省厦门市海沧区', location: { lng: 117.97257, lat: 24.53645 } },
  667. { name: '龙岩市漳平市', adcode: '350881', address: '福建省龙岩市漳平市', location: { lng: 117.45528, lat: 25.36948 } },
  668. { name: '三明市大田县', adcode: '350425', address: '福建省三明市大田县', location: { lng: 117.83028, lat: 25.69053 } },
  669. { name: '福州市鼓楼区', adcode: '350102', address: '福建省福州市鼓楼区', location: { lng: 119.29648, lat: 26.08744 } },
  670. { name: '福州市台江区', adcode: '350103', address: '福建省福州市台江区', location: { lng: 119.30476, lat: 26.05936 } },
  671. { name: '福州市仓山区', adcode: '350104', address: '福建省福州市仓山区', location: { lng: 119.27744, lat: 26.00716 } },
  672. { name: '信阳市固始县', adcode: '411525', address: '河南省信阳市固始县', location: { lng: 115.68042, lat: 32.14447 } },
  673. { name: '六安市寿县', adcode: '341521', address: '安徽省六安市寿县', location: { lng: 116.68658, lat: 32.57654 } },
  674. { name: '六安市霍邱县', adcode: '341522', address: '安徽省六安市霍邱县', location: { lng: 116.10428, lat: 32.38912 } },
  675. { name: '六安市李集', adcode: '341522', address: '安徽省六安市霍邱县李集镇', location: { lng: 115.92136, lat: 32.38456 } },
  676. { name: '长沙市望城区', adcode: '430112', address: '湖南省长沙市望城区', location: { lng: 112.83488, lat: 28.31353 } },
  677. { name: '广州市增城区', adcode: '440118', address: '广东省广州市增城区', location: { lng: 113.86262, lat: 23.23745 } },
  678. { name: '吉安市永新县', adcode: '360830', address: '江西省吉安市永新县', location: { lng: 114.22658, lat: 26.90836 } },
  679. { name: '丽水市青田县', adcode: '331121', address: '浙江省丽水市青田县', location: { lng: 120.28158, lat: 28.19935 } },
  680. { name: '温州市泰顺县', adcode: '330329', address: '浙江省温州市泰顺县', location: { lng: 119.75248, lat: 27.67386 } },
  681. { name: '温州市文成县', adcode: '330328', address: '浙江省温州市文成县', location: { lng: 120.08258, lat: 27.68862 } },
  682. { name: '杭州市临安区', adcode: '330112', address: '浙江省杭州市临安区', location: { lng: 119.72768, lat: 30.23397 } },
  683. { name: '金华市兰溪市', adcode: '330781', address: '浙江省金华市兰溪市', location: { lng: 119.48938, lat: 29.29735 } },
  684. { name: '衢州市衢江区', adcode: '330803', address: '浙江省衢州市衢江区', location: { lng: 118.88088, lat: 28.93562 } },
  685. { name: '衢州市开化县', adcode: '330824', address: '浙江省衢州市开化县', location: { lng: 118.30888, lat: 29.29412 } },
  686. { name: '长春市农安县', adcode: '220122', address: '吉林省长春市农安县', location: { lng: 125.16718, lat: 44.45183 } },
  687. { name: '新北市三重区', adcode: '710101', address: '台湾省新北市三重区', location: { lng: 121.48888, lat: 25.05722 } },
  688. { name: '台北市', adcode: '710100', address: '台湾省台北市', location: { lng: 121.50906, lat: 25.04433 } },
  689. { name: '云林县', adcode: '710600', address: '台湾省云林县', location: { lng: 120.43758, lat: 23.71465 } },
  690. { name: '彰化县', adcode: '710500', address: '台湾省彰化县', location: { lng: 120.54869, lat: 24.08279 } },
  691. { name: '台中市清水区', adcode: '710206', address: '台湾省台中市清水区', location: { lng: 120.56496, lat: 24.26271 } },
  692. { name: '菲律宾马尼拉', adcode: 'PH001', address: '菲律宾马尼拉', location: { lng: 120.9842, lat: 14.5995 } },
  693. { name: '北京市', adcode: '110000', address: '北京市', location: { lng: 116.407394, lat: 39.904211 } },
  694. { name: '上海市', adcode: '310000', address: '上海市', location: { lng: 121.473701, lat: 31.230416 } },
  695. { name: '广州市', adcode: '440100', address: '广东省广州市', location: { lng: 113.264385, lat: 23.12911 } },
  696. { name: '深圳市', adcode: '440300', address: '广东省深圳市', location: { lng: 114.057964, lat: 22.543099 } },
  697. { name: '杭州市', adcode: '330100', address: '浙江省杭州市', location: { lng: 120.197855, lat: 30.274084 } },
  698. { name: '南京市', adcode: '320100', address: '江苏省南京市', location: { lng: 118.796875, lat: 32.060255 } },
  699. { name: '成都市', adcode: '510100', address: '四川省成都市', location: { lng: 104.066801, lat: 30.572816 } },
  700. { name: '武汉市', adcode: '420100', address: '湖北省武汉市', location: { lng: 114.287924, lat: 30.592855 } }
  701. ];
  702. const filteredData = localData.filter(item => item.name.includes(keyword) || item.address.includes(keyword));
  703. if (filteredData.length > 0) {
  704. renderRegionSuggestions(filteredData);
  705. } else {
  706. suggestionsDiv.innerHTML = '<div class="alert alert-warning">未找到匹配的区域,请尝试其他关键词</div>';
  707. suggestionsDiv.style.display = 'block';
  708. }
  709. }
  710. function renderRegionSuggestions(pois) {
  711. const suggestionsDiv = document.getElementById('region-suggestions');
  712. let html = '<div class="list-group" style="max-height: 300px; overflow-y: auto;">';
  713. pois.slice(0, 8).forEach(poi => {
  714. const lng = poi.location.lng || poi.location[0];
  715. const lat = poi.location.lat || poi.location[1];
  716. html += '<button type="button" class="list-group-item list-group-item-action" onclick="selectRegion(\'' + poi.name + '\', ' + lng + ', ' + lat + ')">' +
  717. '<div class="d-flex justify-content-between">' +
  718. '<span style="font-weight: 500;">' + poi.name + '</span>' +
  719. '<span class="text-muted text-sm">' + poi.adcode + '</span>' +
  720. '</div>' +
  721. (poi.address ? '<div class="text-sm text-muted">' + poi.address + '</div>' : '') +
  722. '</button>';
  723. });
  724. html += '</div>';
  725. suggestionsDiv.innerHTML = html;
  726. suggestionsDiv.style.display = 'block';
  727. }
  728. function toggleNewSurnameField(surnameType) {
  729. const container = document.getElementById('new-surname-container');
  730. if (surnameType == 1) {
  731. container.style.display = 'block';
  732. } else {
  733. container.style.display = 'none';
  734. document.getElementById('settlement-new-surname').value = '';
  735. }
  736. }
  737. function getSurnameTypeText(type) {
  738. return type == 1 ? '改姓' : '留姓';
  739. }
  740. function selectRegion(name, lng, lat) {
  741. document.getElementById('settlement-region').value = name;
  742. document.getElementById('settlement-latitude').value = lat;
  743. document.getElementById('settlement-longitude').value = lng;
  744. document.getElementById('region-suggestions').style.display = 'none';
  745. const infoText = document.getElementById('location-info-text');
  746. infoText.innerHTML = '已选择区域:' + name + ',坐标:' + lat.toFixed(6) + ', ' + lng.toFixed(6);
  747. document.getElementById('location-info').style.display = 'block';
  748. }
  749. let tooltipDiv = null;
  750. function showMarkerTooltip(e, settlement) {
  751. if (!tooltipDiv) {
  752. tooltipDiv = document.createElement('div');
  753. tooltipDiv.style.cssText = 'position: fixed; background: rgba(30, 41, 59, 0.95); color: white; padding: 12px; border-radius: 8px; min-width: 200px; box-shadow: 0 4px 20px rgba(0,0,0,0.3); z-index: 1000; pointer-events: none; font-size: 13px;';
  754. document.body.appendChild(tooltipDiv);
  755. }
  756. const surnameType = getSurnameTypeText(settlement.surname_type);
  757. tooltipDiv.innerHTML = '<div style="font-weight: 600; margin-bottom: 8px; font-size: 14px;">' + settlement.name + '</div>' +
  758. (settlement.region ? '<div style="margin-bottom: 4px; color: #94a3b8;">区域:' + settlement.region + '</div>' : '') +
  759. '<div style="margin-bottom: 4px; color: #94a3b8;">人数:' + (settlement.population || 0) + ' 人</div>' +
  760. (settlement.representative_name ? '<div style="margin-bottom: 4px; color: #94a3b8;">代表:' + settlement.representative_name + '</div>' : '') +
  761. '<div style="margin-bottom: 4px; color: #94a3b8;">姓氏:' + surnameType + (settlement.new_surname ? '(改后:' + settlement.new_surname + ')' : '') + '</div>' +
  762. '<div style="border-top: 1px solid rgba(255,255,255,0.1); padding-top: 8px; margin-top: 4px;">' +
  763. '<button onclick="event.stopPropagation(); showSettlementDetail(' + JSON.stringify(settlement).replace(/"/g, '&quot;') + '); hideMarkerTooltip();" style="background: #3B82F6; color: white; border: none; padding: 4px 12px; border-radius: 4px; font-size: 11px; cursor: pointer;">查看详情</button>' +
  764. '</div>';
  765. const containerRect = document.getElementById('map-container').getBoundingClientRect();
  766. tooltipDiv.style.left = (containerRect.left + e.pixel.x + 10) + 'px';
  767. tooltipDiv.style.top = (containerRect.top + e.pixel.y - tooltipDiv.offsetHeight / 2) + 'px';
  768. tooltipDiv.style.display = 'block';
  769. }
  770. function hideMarkerTooltip() {
  771. if (tooltipDiv) {
  772. tooltipDiv.style.display = 'none';
  773. }
  774. }
  775. </script>
  776. {% endblock %}