raintee-maputils 1.0.35 → 1.0.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -6561,13 +6561,14 @@ class RulerControl {
6561
6561
 
6562
6562
  class CustomOptionsControl {
6563
6563
  constructor(args) {
6564
- const { title, options, onConfirm } = args;
6564
+ const { title, options, onConfirm, icon } = args;
6565
6565
 
6566
6566
  this._map = null;
6567
6567
  this._container = null;
6568
6568
 
6569
+ this._title = title || '';
6569
6570
  this._options = options || [];
6570
- this._title = title || '标题';
6571
+ this._icon = icon || null;
6571
6572
  this._onConfirm = onConfirm || (() => { });
6572
6573
  this._originalOptions = [...this._options];
6573
6574
  this._selectedOptions = [];
@@ -6580,119 +6581,450 @@ class CustomOptionsControl {
6580
6581
  this._container.className = 'mapboxgl-ctrl mapboxgl-ctrl-group custom-options-control';
6581
6582
 
6582
6583
  const mainButton = document.createElement('button');
6583
- mainButton.textContent = this._title;
6584
6584
  mainButton.style.cssText = `
6585
- padding: 4px 8px;
6585
+ padding: 0;
6586
6586
  background: white;
6587
- border: 1px solid #ccc;
6587
+ border: 0.0625rem solid #ccc;
6588
6588
  cursor: pointer;
6589
+ display: flex;
6590
+ align-items: center;
6591
+ justify-content: center;
6592
+ gap: 0.375rem;
6589
6593
  `;
6590
6594
 
6595
+ if (this._icon) {
6596
+ const iconWrapper = document.createElement('span');
6597
+ iconWrapper.style.cssText = `
6598
+ width: 1rem;
6599
+ height: 1rem;
6600
+ display: inline-flex;
6601
+ align-items: center;
6602
+ justify-content: center;
6603
+ `;
6604
+
6605
+ if (this._icon.trim().startsWith('<svg')) {
6606
+ iconWrapper.innerHTML = this._icon;
6607
+ } else {
6608
+ const img = document.createElement('img');
6609
+ img.src = this._icon;
6610
+ img.style.cssText = `width: 1rem; height: 1rem; object-fit: contain;`;
6611
+ iconWrapper.appendChild(img);
6612
+ }
6613
+
6614
+ mainButton.appendChild(iconWrapper);
6615
+ }
6616
+
6591
6617
  mainButton.addEventListener('click', () => {
6592
6618
  this._showModal();
6593
6619
  });
6594
6620
 
6595
6621
  this._container.appendChild(mainButton);
6622
+
6623
+ // 添加全局样式
6624
+ this._addGlobalStyles();
6625
+
6596
6626
  return this._container;
6597
6627
  }
6598
6628
 
6599
- /** 创建遮罩弹窗 */
6629
+ _addGlobalStyles() {
6630
+ if (document.querySelector('#custom-options-styles')) return;
6631
+
6632
+ const styles = document.createElement('style');
6633
+ styles.id = 'custom-options-styles';
6634
+ styles.textContent = `
6635
+ @keyframes modalSlideIn {
6636
+ from {
6637
+ opacity: 0;
6638
+ transform: scale(0.95) translateY(-20px);
6639
+ }
6640
+ to {
6641
+ opacity: 1;
6642
+ transform: scale(1) translateY(0);
6643
+ }
6644
+ }
6645
+
6646
+ @keyframes modalBackdropFadeIn {
6647
+ from { opacity: 0; }
6648
+ to { opacity: 1; }
6649
+ }
6650
+
6651
+ @keyframes fadeInUp {
6652
+ from {
6653
+ opacity: 0;
6654
+ transform: translateY(20px);
6655
+ }
6656
+ to {
6657
+ opacity: 1;
6658
+ transform: translateY(0);
6659
+ }
6660
+ }
6661
+
6662
+ .custom-modal-overlay {
6663
+ animation: modalBackdropFadeIn 0.2s ease-out;
6664
+ }
6665
+
6666
+ .custom-modal {
6667
+ animation: modalSlideIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
6668
+ }
6669
+
6670
+ .custom-search-input:focus {
6671
+ border-color: #667eea !important;
6672
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1) !important;
6673
+ }
6674
+
6675
+ .custom-checkbox {
6676
+ position: relative;
6677
+ appearance: none;
6678
+ width: 1rem;
6679
+ height: 1rem;
6680
+ border: 2px solid #d1d5db;
6681
+ border-radius: 0.25rem;
6682
+ background: white;
6683
+ cursor: pointer;
6684
+ transition: all 0.2s ease;
6685
+ margin-right: 0.75rem;
6686
+ }
6687
+
6688
+ .custom-checkbox:checked {
6689
+ background: linear-gradient(135deg, #667eea, #764ba2);
6690
+ border-color: #667eea;
6691
+ }
6692
+
6693
+ .custom-checkbox:checked::after {
6694
+ content: '✓';
6695
+ position: absolute;
6696
+ top: 50%;
6697
+ left: 50%;
6698
+ transform: translate(-50%, -50%);
6699
+ color: white;
6700
+ font-size: 0.75rem;
6701
+ font-weight: bold;
6702
+ }
6703
+
6704
+ .custom-checkbox:hover {
6705
+ border-color: #667eea;
6706
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
6707
+ }
6708
+
6709
+ .custom-list-item {
6710
+ transition: all 0.2s ease;
6711
+ }
6712
+
6713
+ .custom-list-item:hover {
6714
+ background-color: #f8fafc !important;
6715
+ border-color: #e2e8f0 !important;
6716
+ transform: translateX(4px) !important;
6717
+ }
6718
+
6719
+ .custom-modal::-webkit-scrollbar {
6720
+ width: 6px;
6721
+ }
6722
+
6723
+ .custom-modal::-webkit-scrollbar-track {
6724
+ background: #f1f5f9;
6725
+ border-radius: 3px;
6726
+ }
6727
+
6728
+ .custom-modal::-webkit-scrollbar-thumb {
6729
+ background: linear-gradient(135deg, #667eea, #764ba2);
6730
+ border-radius: 3px;
6731
+ }
6732
+
6733
+ .custom-modal::-webkit-scrollbar-thumb:hover {
6734
+ background: linear-gradient(135deg, #5a67d8, #6b46c1);
6735
+ }
6736
+ `;
6737
+ document.head.appendChild(styles);
6738
+ }
6739
+
6600
6740
  _showModal() {
6601
- // 创建遮罩层
6602
6741
  this._overlay = document.createElement('div');
6603
6742
  this._overlay.className = 'custom-modal-overlay';
6604
6743
  this._overlay.style.cssText = `
6605
6744
  position: fixed;
6606
6745
  top: 0; left: 0; right: 0; bottom: 0;
6607
- background: rgba(0,0,0,0.5);
6608
- z-index: 9999;
6746
+ background: rgba(0, 0, 0, 0.5);
6747
+ backdrop-filter: blur(4px);
6748
+ -webkit-backdrop-filter: blur(4px);
6749
+ z-index: 10000;
6609
6750
  display: flex;
6610
6751
  justify-content: center;
6611
6752
  align-items: center;
6753
+ padding: 1rem;
6754
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
6612
6755
  `;
6613
6756
 
6614
- // 弹窗容器
6615
6757
  this._modal = document.createElement('div');
6616
6758
  this._modal.className = 'custom-modal';
6617
6759
  this._modal.style.cssText = `
6618
- width: 360px;
6760
+ width: 100%;
6761
+ max-width: 26rem;
6619
6762
  max-height: 80vh;
6763
+ background: linear-gradient(145deg, #ffffff 0%, #f8fafc 100%);
6764
+ border-radius: 0.75rem;
6765
+ box-shadow:
6766
+ 0 20px 25px -5px rgba(0, 0, 0, 0.1),
6767
+ 0 10px 10px -5px rgba(0, 0, 0, 0.04);
6768
+ border: 1px solid rgba(255, 255, 255, 0.8);
6620
6769
  overflow: hidden;
6621
- background: white;
6622
- border-radius: 8px;
6623
- padding: 20px;
6624
6770
  display: flex;
6625
6771
  flex-direction: column;
6626
6772
  `;
6627
6773
 
6628
- // 搜索框
6774
+ // 创建头部
6775
+ const header = this._createHeader();
6776
+ this._modal.appendChild(header);
6777
+
6778
+ // 创建搜索区域
6779
+ const searchSection = this._createSearchSection();
6780
+ this._modal.appendChild(searchSection);
6781
+
6782
+ // 创建选项列表
6783
+ this._listBox = document.createElement('div');
6784
+ this._listBox.style.cssText = `
6785
+ flex: 1;
6786
+ overflow-y: auto;
6787
+ padding: 0 1.25rem 1.25rem 1.25rem;
6788
+ min-height: 10rem;
6789
+ `;
6790
+ this._modal.appendChild(this._listBox);
6791
+
6792
+ // 创建底部按钮
6793
+ const footer = this._createFooter();
6794
+ this._modal.appendChild(footer);
6795
+
6796
+ this._overlay.appendChild(this._modal);
6797
+ document.body.appendChild(this._overlay);
6798
+
6799
+ // 聚焦搜索框
6800
+ // setTimeout(() => {
6801
+ // const searchInput = this._modal.querySelector('.custom-search-input');
6802
+ // if (searchInput) {
6803
+ // searchInput.focus();
6804
+ // }
6805
+ // }, 100);
6806
+
6807
+ // 点击遮罩关闭
6808
+ this._overlay.addEventListener('click', (e) => {
6809
+ if (e.target === this._overlay) {
6810
+ this._closeModal();
6811
+ }
6812
+ });
6813
+
6814
+ // ESC 键关闭
6815
+ this._handleKeydown = (e) => {
6816
+ if (e.key === 'Escape') {
6817
+ this._closeModal();
6818
+ }
6819
+ };
6820
+ document.addEventListener('keydown', this._handleKeydown);
6821
+
6822
+ this._renderList('');
6823
+ }
6824
+
6825
+ _createHeader() {
6826
+ const header = document.createElement('div');
6827
+ header.style.cssText = `
6828
+ padding: 1.25rem 1.25rem 1rem 1.25rem;
6829
+ border-bottom: 1px solid #e2e8f0;
6830
+ background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
6831
+ `;
6832
+
6833
+ const title = document.createElement('h2');
6834
+ title.textContent = this._title;
6835
+ title.style.cssText = `
6836
+ margin: 0 0 0.5rem 0;
6837
+ font-size: 1.125rem;
6838
+ font-weight: 600;
6839
+ color: #1e293b;
6840
+ display: flex;
6841
+ align-items: center;
6842
+ gap: 0.5rem;
6843
+ `;
6844
+
6845
+ // 添加图标
6846
+ const icon = document.createElement('span');
6847
+ icon.innerHTML = '📋';
6848
+ icon.style.fontSize = '1.25rem';
6849
+ title.insertBefore(icon, title.firstChild);
6850
+
6851
+ const subtitle = document.createElement('p');
6852
+ subtitle.textContent = `共 ${this._originalOptions.length} 个选项,已选择 ${this._selectedOptions.length} 个`;
6853
+ subtitle.style.cssText = `
6854
+ margin: 0;
6855
+ font-size: 0.8125rem;
6856
+ color: #64748b;
6857
+ `;
6858
+
6859
+ const closeBtn = document.createElement('button');
6860
+ closeBtn.innerHTML = '✕';
6861
+ closeBtn.style.cssText = `
6862
+ position: absolute;
6863
+ top: 1rem;
6864
+ right: 1rem;
6865
+ width: 1.75rem;
6866
+ height: 1.75rem;
6867
+ border: none;
6868
+ background: #f1f5f9;
6869
+ color: #64748b;
6870
+ border-radius: 50%;
6871
+ cursor: pointer;
6872
+ display: flex;
6873
+ align-items: center;
6874
+ justify-content: center;
6875
+ font-size: 0.875rem;
6876
+ transition: all 0.2s ease;
6877
+ font-weight: 500;
6878
+ `;
6879
+ closeBtn.addEventListener('mouseenter', () => {
6880
+ closeBtn.style.background = '#ef4444';
6881
+ closeBtn.style.color = 'white';
6882
+ closeBtn.style.transform = 'rotate(90deg)';
6883
+ });
6884
+ closeBtn.addEventListener('mouseleave', () => {
6885
+ closeBtn.style.background = '#f1f5f9';
6886
+ closeBtn.style.color = '#64748b';
6887
+ closeBtn.style.transform = 'rotate(0deg)';
6888
+ });
6889
+ closeBtn.addEventListener('click', () => this._closeModal());
6890
+
6891
+ header.style.position = 'relative';
6892
+ header.appendChild(title);
6893
+ header.appendChild(subtitle);
6894
+ header.appendChild(closeBtn);
6895
+
6896
+ return header;
6897
+ }
6898
+
6899
+ _createSearchSection() {
6900
+ const section = document.createElement('div');
6901
+ section.style.cssText = `
6902
+ padding: 1.25rem 1.25rem 1rem 1.25rem;
6903
+ `;
6904
+
6905
+ const searchContainer = document.createElement('div');
6906
+ searchContainer.style.cssText = `
6907
+ position: relative;
6908
+ `;
6909
+
6910
+ const searchIcon = document.createElement('div');
6911
+ searchIcon.innerHTML = '🔍';
6912
+ searchIcon.style.cssText = `
6913
+ position: absolute;
6914
+ left: 0.875rem;
6915
+ top: 50%;
6916
+ transform: translateY(-50%);
6917
+ font-size: 0.875rem;
6918
+ color: #94a3b8;
6919
+ pointer-events: none;
6920
+ `;
6921
+
6629
6922
  const searchInput = document.createElement('input');
6630
6923
  searchInput.placeholder = '搜索选项...';
6631
6924
  searchInput.className = 'custom-search-input';
6632
6925
  searchInput.style.cssText = `
6633
6926
  width: 100%;
6634
- padding: 8px;
6635
- border: 1px solid #ccc;
6636
- border-radius: 4px;
6637
- margin-bottom: 12px;
6927
+ padding: 0.75rem 1rem 0.75rem 2.5rem;
6928
+ border: 2px solid #e2e8f0;
6929
+ border-radius: 0.5rem;
6930
+ font-size: 0.875rem;
6931
+ background: white;
6932
+ transition: all 0.3s ease;
6933
+ box-sizing: border-box;
6934
+ font-family: inherit;
6935
+ outline: none;
6638
6936
  `;
6937
+
6938
+ searchInput.addEventListener('focus', () => {
6939
+ searchInput.style.borderColor = '#667eea';
6940
+ searchInput.style.boxShadow = '0 0 0 3px rgba(102, 126, 234, 0.1)';
6941
+ });
6942
+
6943
+ searchInput.addEventListener('blur', () => {
6944
+ searchInput.style.borderColor = '#e2e8f0';
6945
+ searchInput.style.boxShadow = 'none';
6946
+ });
6947
+
6639
6948
  searchInput.addEventListener('input', (e) => {
6640
6949
  this._renderList(e.target.value);
6641
6950
  });
6642
6951
 
6643
- this._modal.appendChild(searchInput);
6952
+ searchContainer.appendChild(searchIcon);
6953
+ searchContainer.appendChild(searchInput);
6954
+ section.appendChild(searchContainer);
6644
6955
 
6645
- // 列表区域
6646
- this._listBox = document.createElement('div');
6647
- this._listBox.style.cssText = `
6648
- flex: 1;
6649
- overflow-y: auto;
6650
- padding-right: 4px;
6651
- `;
6652
- this._modal.appendChild(this._listBox);
6956
+ return section;
6957
+ }
6653
6958
 
6654
- // 底部按钮
6959
+ _createFooter() {
6655
6960
  const footer = document.createElement('div');
6656
6961
  footer.style.cssText = `
6657
- margin-top: 12px;
6962
+ padding: 1rem 1.25rem;
6963
+ background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
6964
+ border-top: 1px solid #e2e8f0;
6658
6965
  display: flex;
6659
6966
  justify-content: flex-end;
6660
- gap: 8px;
6967
+ gap: 0.75rem;
6661
6968
  `;
6662
6969
 
6663
6970
  const cancelBtn = document.createElement('button');
6664
6971
  cancelBtn.textContent = '取消';
6665
6972
  cancelBtn.style.cssText = `
6666
- padding: 6px 12px;
6667
- background: #ccc;
6668
- border: none;
6669
- border-radius: 4px;
6973
+ padding: 0.5rem 1rem;
6974
+ background: white;
6975
+ border: 1px solid #d1d5db;
6976
+ border-radius: 0.375rem;
6977
+ cursor: pointer;
6978
+ font-size: 0.875rem;
6979
+ font-weight: 500;
6980
+ color: #374151;
6981
+ transition: all 0.2s ease;
6982
+ font-family: inherit;
6670
6983
  `;
6984
+ cancelBtn.addEventListener('mouseenter', () => {
6985
+ cancelBtn.style.borderColor = '#ef4444';
6986
+ cancelBtn.style.color = '#ef4444';
6987
+ cancelBtn.style.transform = 'translateY(-1px)';
6988
+ });
6989
+ cancelBtn.addEventListener('mouseleave', () => {
6990
+ cancelBtn.style.borderColor = '#d1d5db';
6991
+ cancelBtn.style.color = '#374151';
6992
+ cancelBtn.style.transform = 'translateY(0)';
6993
+ });
6671
6994
  cancelBtn.addEventListener('click', () => this._closeModal());
6672
6995
 
6673
6996
  const okBtn = document.createElement('button');
6674
6997
  okBtn.textContent = '确定';
6675
6998
  okBtn.style.cssText = `
6676
- padding: 6px 12px;
6999
+ padding: 0.5rem 1rem;
6677
7000
  background: #007cba;
6678
7001
  color: white;
6679
7002
  border: none;
6680
- border-radius: 4px;
7003
+ border-radius: 0.375rem;
7004
+ cursor: pointer;
7005
+ font-size: 0.875rem;
7006
+ font-weight: 500;
7007
+ transition: all 0.2s ease;
7008
+ font-family: inherit;
7009
+ box-shadow: 0 2px 4px rgba(0, 124, 186, 0.2);
6681
7010
  `;
7011
+ okBtn.addEventListener('mouseenter', () => {
7012
+ okBtn.style.background = '#005a87';
7013
+ okBtn.style.transform = 'translateY(-1px)';
7014
+ okBtn.style.boxShadow = '0 4px 8px rgba(0, 124, 186, 0.3)';
7015
+ });
7016
+ okBtn.addEventListener('mouseleave', () => {
7017
+ okBtn.style.background = '#007cba';
7018
+ okBtn.style.transform = 'translateY(0)';
7019
+ okBtn.style.boxShadow = '0 2px 4px rgba(0, 124, 186, 0.2)';
7020
+ });
6682
7021
  okBtn.addEventListener('click', () => this._confirm());
6683
7022
 
6684
7023
  footer.appendChild(cancelBtn);
6685
7024
  footer.appendChild(okBtn);
6686
- this._modal.appendChild(footer);
6687
-
6688
- this._overlay.appendChild(this._modal);
6689
- document.body.appendChild(this._overlay);
6690
-
6691
- // 初始化列表
6692
- this._renderList('');
7025
+ return footer;
6693
7026
  }
6694
7027
 
6695
- /** 搜索过滤 */
6696
7028
  _renderList(keyword) {
6697
7029
  this._listBox.innerHTML = '';
6698
7030
 
@@ -6706,53 +7038,126 @@ class CustomOptionsControl {
6706
7038
 
6707
7039
  if (list.length === 0) {
6708
7040
  const empty = document.createElement('div');
6709
- empty.textContent = `未找到相关结果`;
6710
- empty.style.textAlign = 'center';
6711
- empty.style.padding = '20px';
7041
+ empty.style.cssText = `
7042
+ text-align: center;
7043
+ padding: 2.5rem 1rem;
7044
+ color: #94a3b8;
7045
+ `;
7046
+ empty.innerHTML = `
7047
+ <div style="font-size: 2.5rem; margin-bottom: 0.75rem; opacity: 0.6;">🔍</div>
7048
+ <div style="font-size: 1rem; font-weight: 500; margin-bottom: 0.25rem; color: #64748b;">未找到相关结果</div>
7049
+ <div style="font-size: 0.8125rem;">尝试使用其他关键词搜索</div>
7050
+ `;
6712
7051
  this._listBox.appendChild(empty);
6713
7052
  return;
6714
7053
  }
6715
7054
 
6716
- list.forEach((opt) => {
7055
+ // 添加选项统计
7056
+ const stats = document.createElement('div');
7057
+ stats.style.cssText = `
7058
+ padding: 0.5rem 0;
7059
+ font-size: 0.8125rem;
7060
+ color: #64748b;
7061
+ border-bottom: 1px solid #f1f5f9;
7062
+ margin-bottom: 0.5rem;
7063
+ `;
7064
+ stats.textContent = `找到 ${list.length} 个相关选项`;
7065
+ this._listBox.appendChild(stats);
7066
+
7067
+ list.forEach((opt, index) => {
6717
7068
  const item = document.createElement('label');
7069
+ item.className = 'custom-list-item';
6718
7070
  item.style.cssText = `
6719
7071
  display: flex;
6720
7072
  align-items: center;
6721
- padding: 6px 0;
7073
+ padding: 0.75rem;
6722
7074
  cursor: pointer;
7075
+ border-radius: 0.5rem;
7076
+ margin-bottom: 0.375rem;
7077
+ transition: all 0.2s ease;
7078
+ border: 2px solid transparent;
7079
+ background: white;
7080
+ position: relative;
7081
+ overflow: hidden;
6723
7082
  `;
6724
7083
 
6725
7084
  const checkbox = document.createElement('input');
6726
7085
  checkbox.type = 'checkbox';
7086
+ checkbox.className = 'custom-checkbox';
6727
7087
  checkbox.value = opt.value;
6728
7088
  checkbox.checked = this._selectedOptions.includes(opt.value);
6729
- checkbox.style.marginRight = '8px';
6730
- checkbox.addEventListener('change', () => this._toggle(opt.value));
7089
+ checkbox.style.marginRight = '0.75rem';
6731
7090
 
6732
7091
  const labelSpan = document.createElement('span');
6733
7092
  labelSpan.innerHTML = this._highlight(opt.label, keyword);
7093
+ labelSpan.style.cssText = `
7094
+ flex: 1;
7095
+ font-size: 0.875rem;
7096
+ color: #374151;
7097
+ line-height: 1.4;
7098
+ font-weight: 500;
7099
+ `;
7100
+
7101
+ // 添加选中状态的视觉反馈
7102
+ if (checkbox.checked) {
7103
+ item.style.backgroundColor = '#f0f4ff';
7104
+ item.style.borderColor = '#667eea';
7105
+ }
7106
+
7107
+ checkbox.addEventListener('change', () => {
7108
+ this._toggle(opt.value);
7109
+
7110
+ // 更新项目样式
7111
+ if (checkbox.checked) {
7112
+ item.style.backgroundColor = '#f0f4ff';
7113
+ item.style.borderColor = '#667eea';
7114
+ } else {
7115
+ item.style.backgroundColor = 'white';
7116
+ item.style.borderColor = 'transparent';
7117
+ }
7118
+
7119
+ // 更新标题中的统计信息
7120
+ const subtitle = this._modal.querySelector('p');
7121
+ if (subtitle) {
7122
+ subtitle.textContent = `共 ${this._originalOptions.length} 个选项,已选择 ${this._selectedOptions.length} 个`;
7123
+ }
7124
+ });
6734
7125
 
6735
7126
  item.appendChild(checkbox);
6736
7127
  item.appendChild(labelSpan);
6737
7128
 
7129
+ // 添加进入动画
7130
+ item.style.opacity = '0';
7131
+ item.style.transform = 'translateY(10px)';
6738
7132
  this._listBox.appendChild(item);
7133
+
7134
+ setTimeout(() => {
7135
+ item.style.transition = 'all 0.3s ease';
7136
+ item.style.opacity = '1';
7137
+ item.style.transform = 'translateY(0)';
7138
+ }, index * 30);
6739
7139
  });
6740
7140
  }
6741
7141
 
6742
- /** 高亮关键词 */
6743
7142
  _highlight(text, key) {
6744
7143
  if (!key) return text;
6745
- return text.replace(new RegExp(`(${key})`, 'gi'), `<mark>$1</mark>`);
7144
+ const regex = new RegExp(`(${this._escapeRegExp(key)})`, 'gi');
7145
+ return text.replace(regex, '<mark style="background: linear-gradient(120deg, #fef3c7 0%, #fde68a 100%); padding: 0.125rem 0.25rem; border-radius: 0.25rem; font-weight: 600;">$1</mark>');
7146
+ }
7147
+
7148
+ _escapeRegExp(string) {
7149
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
6746
7150
  }
6747
7151
 
6748
- /** 选择切换 */
6749
7152
  _toggle(value) {
6750
7153
  const index = this._selectedOptions.indexOf(value);
6751
- if (index >= 0) this._selectedOptions.splice(index, 1);
6752
- else this._selectedOptions.push(value);
7154
+ if (index >= 0) {
7155
+ this._selectedOptions.splice(index, 1);
7156
+ } else {
7157
+ this._selectedOptions.push(value);
7158
+ }
6753
7159
  }
6754
7160
 
6755
- /** 点击确定 */
6756
7161
  _confirm() {
6757
7162
  this._onConfirm({
6758
7163
  selectedOptions: this._selectedOptions,
@@ -6764,340 +7169,104 @@ class CustomOptionsControl {
6764
7169
  this._closeModal();
6765
7170
  }
6766
7171
 
6767
- /** 销毁弹窗 */
6768
7172
  _closeModal() {
6769
7173
  if (this._overlay) {
6770
- document.body.removeChild(this._overlay);
6771
- this._overlay = null;
6772
- this._modal = null;
7174
+ // 添加退出动画
7175
+ this._modal.style.animation = 'modalSlideIn 0.2s ease-out reverse';
7176
+ this._overlay.style.animation = 'modalBackdropFadeIn 0.2s ease-out reverse';
7177
+
7178
+ setTimeout(() => {
7179
+ if (this._overlay && this._overlay.parentNode) {
7180
+ document.body.removeChild(this._overlay);
7181
+ }
7182
+ this._overlay = null;
7183
+ this._modal = null;
7184
+ }, 200);
7185
+
7186
+ document.removeEventListener('keydown', this._handleKeydown);
6773
7187
  }
6774
7188
  }
6775
7189
 
6776
7190
  onRemove() {
6777
- if (this._container) this._container.remove();
7191
+ if (this._container) {
7192
+ this._container.remove();
7193
+ }
6778
7194
  this._closeModal();
6779
7195
  }
6780
7196
  }
6781
7197
 
6782
- // 自定义控件类:ToggleControl(带搜索过滤功能)
7198
+ // 自定义控件类:ToggleControl
6783
7199
  class CustomToggleControl {
6784
7200
  /**
6785
7201
  * 构造函数
6786
7202
  * @param {Object} options
6787
- * @param {string} options.name - 控件名称
6788
- * @param {string} options.field - 控制的字段名
6789
- * @param {boolean} options.defaultValue - 默认值
6790
- * @param {string} options.svgIcon - SVG 图标字符串
6791
- * @param {Function} [options.onToggle] - 状态切换回调
6792
- * @param {Array} [options.options] - 可选项目数组 [{id, name, icon?}]
6793
- * @param {Function} [options.onOptionSelect] - 选项选择回调
6794
- * @param {boolean} [options.showSearch] - 是否显示搜索框,默认 true
7203
+ * @param {string} options.name - 控件名称(用于识别,可选展示)
7204
+ * @param {string} options.field - 控制的字段名,对应 map.SourceMap[field]
7205
+ * @param {boolean} options.defaultValue - 默认值(当 field 未定义时使用)
7206
+ * @param {string} options.svgIcon - SVG 图标字符串,例如 '<svg>...</svg>'
7207
+ * @param {Function} [options.onToggle] - 可选的回调函数,状态切换后调用,参数为最新的布尔值
6795
7208
  */
6796
- constructor({
6797
- name,
6798
- field,
6799
- defaultValue = false,
6800
- svgIcon,
6801
- onToggle,
6802
- options = [],
6803
- onOptionSelect,
6804
- showSearch = true
6805
- }) {
7209
+ constructor({ name, field, defaultValue = false, svgIcon, onToggle }) {
6806
7210
  this.name = name;
6807
7211
  this.field = field;
6808
7212
  this.defaultValue = defaultValue;
6809
7213
  this.svgIcon = svgIcon;
6810
- this.onToggle = onToggle;
6811
- this.options = options; // 可选项目
6812
- this.onOptionSelect = onOptionSelect; // 选项选择回调
6813
- this.showSearch = showSearch; // 是否显示搜索框
7214
+ this.onToggle = onToggle; // ✅ 新增:回调函数
6814
7215
 
6815
- // 控件状态
7216
+ // 控件容器
6816
7217
  this._container = null;
6817
7218
  this._button = null;
6818
- this._dropdown = null;
6819
- this._searchInput = null;
6820
- this._optionsList = null;
6821
- this.isActive = false;
6822
- this.isDropdownOpen = false;
6823
- this.filteredOptions = [...options]; // 过滤后的选项
6824
-
6825
- // 绑定事件处理函数
6826
- this._boundHandleClickOutside = this._handleClickOutside.bind(this);
7219
+ this.isActive = false; // 当前是否为激活状态(高亮/true)
6827
7220
  }
6828
7221
 
6829
7222
  // Mapbox 要求的 onAdd 方法
6830
7223
  onAdd(map) {
6831
7224
  this.map = map;
6832
7225
 
6833
- // 创建外层容器
7226
+ // 创建外层容器 div
6834
7227
  this._container = document.createElement('div');
6835
- this._container.className = 'mapboxgl-ctrl mapboxgl-ctrl-group custom-toggle-control';
7228
+ this._container.className = 'mapboxgl-ctrl mapboxgl-ctrl-group';
6836
7229
 
6837
- // 创建主按钮
7230
+ // 创建 button 元素
6838
7231
  this._button = document.createElement('button');
6839
7232
  this._button.type = 'button';
6840
7233
  this._button.innerHTML = this.svgIcon;
6841
- this._button.title = this.name;
6842
7234
 
6843
7235
  // 设置按钮样式
6844
- this._setupButtonStyles();
7236
+ this._button.style.cursor = 'pointer';
7237
+ this._button.style.border = 'none';
7238
+ this._button.style.background = 'none';
7239
+ this._button.style.padding = '0';
7240
+ this._button.style.display = 'flex';
7241
+ this._button.style.alignItems = 'center';
7242
+ this._button.style.justifyContent = 'center';
6845
7243
 
6846
7244
  // 初始化状态
6847
7245
  this.updateStatus();
6848
7246
 
6849
- // 绑定事件
6850
- this._button.addEventListener('click', (e) => {
6851
- e.stopPropagation();
7247
+ // 绑定点击事件
7248
+ this._button.addEventListener('click', () => {
6852
7249
  this.toggle();
6853
7250
  });
6854
7251
 
6855
7252
  this._container.appendChild(this._button);
6856
7253
 
6857
- // 创建下拉菜单(如果有选项)
6858
- if (this.options.length > 0) {
6859
- this._createDropdown();
6860
- }
6861
-
6862
7254
  return this._container;
6863
7255
  }
6864
7256
 
6865
- // 设置按钮样式
6866
- _setupButtonStyles() {
6867
- Object.assign(this._button.style, {
6868
- cursor: 'pointer',
6869
- border: 'none',
6870
- background: 'none',
6871
- padding: '8px',
6872
- display: 'flex',
6873
- alignItems: 'center',
6874
- justifyContent: 'center',
6875
- borderRadius: '4px',
6876
- transition: 'all 0.2s ease'
6877
- });
6878
- }
6879
-
6880
- // 创建下拉菜单
6881
- _createDropdown() {
6882
- // 创建下拉容器
6883
- this._dropdown = document.createElement('div');
6884
- this._dropdown.className = 'custom-toggle-dropdown';
6885
- Object.assign(this._dropdown.style, {
6886
- position: 'absolute',
6887
- top: '100%',
6888
- right: '0',
6889
- backgroundColor: 'white',
6890
- border: '1px solid #ccc',
6891
- borderRadius: '4px',
6892
- boxShadow: '0 2px 10px rgba(0,0,0,0.1)',
6893
- zIndex: '1000',
6894
- minWidth: '200px',
6895
- maxWidth: '300px',
6896
- display: 'none',
6897
- marginTop: '4px'
6898
- });
6899
-
6900
- // 创建搜索框(如果需要)
6901
- if (this.showSearch && this.options.length > 5) {
6902
- this._createSearchBox();
6903
- }
6904
-
6905
- // 创建选项列表
6906
- this._createOptionsList();
6907
-
6908
- this._container.appendChild(this._dropdown);
6909
- }
6910
-
6911
- // 创建搜索框
6912
- _createSearchBox() {
6913
- const searchContainer = document.createElement('div');
6914
- Object.assign(searchContainer.style, {
6915
- padding: '8px',
6916
- borderBottom: '1px solid #eee'
6917
- });
6918
-
6919
- this._searchInput = document.createElement('input');
6920
- this._searchInput.type = 'text';
6921
- this._searchInput.placeholder = '搜索...';
6922
- this._searchInput.style.cssText = `
6923
- width: 100%;
6924
- padding: 6px 8px;
6925
- border: 1px solid #ddd;
6926
- border-radius: 4px;
6927
- fontSize: 12px;
6928
- outline: none;
6929
- box-sizing: border-box;
6930
- `;
6931
-
6932
- // 绑定搜索事件
6933
- this._searchInput.addEventListener('input', (e) => {
6934
- this._filterOptions(e.target.value);
6935
- });
6936
-
6937
- // 阻止下拉菜单关闭
6938
- this._searchInput.addEventListener('click', (e) => {
6939
- e.stopPropagation();
6940
- });
6941
-
6942
- searchContainer.appendChild(this._searchInput);
6943
- this._dropdown.appendChild(searchContainer);
6944
- }
6945
-
6946
- // 创建选项列表
6947
- _createOptionsList() {
6948
- this._optionsList = document.createElement('div');
6949
- Object.assign(this._optionsList.style, {
6950
- maxHeight: '200px',
6951
- overflowY: 'auto',
6952
- padding: '4px 0'
6953
- });
6954
-
6955
- this._renderOptionsList();
6956
- this._dropdown.appendChild(this._optionsList);
6957
- }
6958
-
6959
- // 渲染选项列表
6960
- _renderOptionsList() {
6961
- if (!this._optionsList) return;
6962
-
6963
- this._optionsList.innerHTML = '';
6964
-
6965
- this.filteredOptions.forEach(option => {
6966
- const optionItem = document.createElement('div');
6967
- optionItem.className = 'custom-toggle-option';
6968
- optionItem.dataset.id = option.id;
6969
-
6970
- Object.assign(optionItem.style, {
6971
- padding: '8px 12px',
6972
- cursor: 'pointer',
6973
- display: 'flex',
6974
- alignItems: 'center',
6975
- gap: '8px',
6976
- fontSize: '13px',
6977
- transition: 'background-color 0.2s'
6978
- });
6979
-
6980
- // 添加图标(如果有)
6981
- if (option.icon) {
6982
- const icon = document.createElement('span');
6983
- icon.innerHTML = option.icon;
6984
- icon.style.fontSize = '14px';
6985
- optionItem.appendChild(icon);
6986
- }
6987
-
6988
- // 添加文本
6989
- const text = document.createElement('span');
6990
- text.textContent = option.name;
6991
- optionItem.appendChild(text);
6992
-
6993
- // 绑定点击事件
6994
- optionItem.addEventListener('click', (e) => {
6995
- e.stopPropagation();
6996
- this._selectOption(option);
6997
- });
6998
-
6999
- // 鼠标悬停效果
7000
- optionItem.addEventListener('mouseenter', () => {
7001
- optionItem.style.backgroundColor = '#f5f5f5';
7002
- });
7003
-
7004
- optionItem.addEventListener('mouseleave', () => {
7005
- optionItem.style.backgroundColor = 'transparent';
7006
- });
7007
-
7008
- this._optionsList.appendChild(optionItem);
7009
- });
7010
-
7011
- // 如果没有匹配的选项
7012
- if (this.filteredOptions.length === 0) {
7013
- const noResults = document.createElement('div');
7014
- noResults.textContent = '无匹配项';
7015
- Object.assign(noResults.style, {
7016
- padding: '12px',
7017
- textAlign: 'center',
7018
- color: '#999',
7019
- fontSize: '12px'
7020
- });
7021
- this._optionsList.appendChild(noResults);
7022
- }
7023
- }
7024
-
7025
- // 过滤选项
7026
- _filterOptions(searchTerm) {
7027
- const term = searchTerm.toLowerCase().trim();
7028
-
7029
- if (!term) {
7030
- this.filteredOptions = [...this.options];
7031
- } else {
7032
- this.filteredOptions = this.options.filter(option =>
7033
- option.name.toLowerCase().includes(term) ||
7034
- (option.id && option.id.toString().toLowerCase().includes(term))
7035
- );
7036
- }
7037
-
7038
- this._renderOptionsList();
7039
- }
7040
-
7041
- // 选择选项
7042
- _selectOption(option) {
7043
- // 调用选项选择回调
7044
- if (this.onOptionSelect) {
7045
- this.onOptionSelect(option);
7046
- }
7047
-
7048
- // 关闭下拉菜单
7049
- this.closeDropdown();
7050
- }
7051
-
7052
- // 切换下拉菜单显示状态
7053
- toggle() {
7054
- if (this.isDropdownOpen) {
7055
- this.closeDropdown();
7056
- } else {
7057
- this.openDropdown();
7058
- }
7059
- }
7060
-
7061
- // 打开下拉菜单
7062
- openDropdown() {
7063
- if (!this._dropdown || this.options.length === 0) return;
7064
-
7065
- this.isDropdownOpen = true;
7066
- this._dropdown.style.display = 'block';
7067
-
7068
- // 添加全局点击监听
7069
- document.addEventListener('click', this._boundHandleClickOutside);
7070
-
7071
- // 聚焦搜索框
7072
- if (this._searchInput) {
7073
- setTimeout(() => {
7074
- this._searchInput.focus();
7075
- this._searchInput.value = '';
7076
- this._filterOptions('');
7077
- }, 100);
7078
- }
7079
- }
7080
-
7081
- // 关闭下拉菜单
7082
- closeDropdown() {
7083
- if (!this._dropdown) return;
7084
-
7085
- this.isDropdownOpen = false;
7086
- this._dropdown.style.display = 'none';
7087
-
7088
- // 移除全局点击监听
7089
- document.removeEventListener('click', this._boundHandleClickOutside);
7090
- }
7091
-
7092
- // 处理外部点击
7093
- _handleClickOutside(event) {
7094
- if (!this._container.contains(event.target)) {
7095
- this.closeDropdown();
7257
+ // Mapbox 要求的 onRemove 方法
7258
+ onRemove() {
7259
+ if (this._container && this._container.parentNode) {
7260
+ this._container.parentNode.removeChild(this._container);
7096
7261
  }
7262
+ this.map = null;
7263
+ this._container = null;
7264
+ this._button = null;
7097
7265
  }
7098
7266
 
7099
- // 更新控件状态
7267
+ // 更新控件状态(根据当前 field 值)
7100
7268
  updateStatus() {
7269
+ // 从 map.SourceMap 中获取当前 field 的值,如果不存在则使用 defaultValue
7101
7270
  let currentValue = this.defaultValue;
7102
7271
 
7103
7272
  if (
@@ -7108,22 +7277,21 @@ class CustomToggleControl {
7108
7277
  currentValue = this.map.SourceMap[this.field];
7109
7278
  }
7110
7279
 
7111
- this.isActive = currentValue;
7280
+ this.isActive = currentValue; // true 表示激活,false 表示未激活
7112
7281
 
7113
7282
  // 更新按钮样式
7114
7283
  if (this.isActive) {
7115
7284
  this._button.style.opacity = '1';
7116
7285
  this._button.style.filter = 'none';
7117
- this._button.style.backgroundColor = '#e6f3ff';
7118
7286
  } else {
7119
- this._button.style.opacity = '0.7';
7287
+ this._button.style.opacity = '0.4';
7120
7288
  this._button.style.filter = 'grayscale(100%)';
7121
- this._button.style.backgroundColor = 'transparent';
7122
7289
  }
7123
7290
  }
7124
7291
 
7125
- // 切换状态(保持原有功能)
7126
- toggleState() {
7292
+ // 切换状态
7293
+ toggle() {
7294
+ // 获取当前值
7127
7295
  let currentValue = this.defaultValue;
7128
7296
 
7129
7297
  if (
@@ -7134,34 +7302,22 @@ class CustomToggleControl {
7134
7302
  currentValue = this.map.SourceMap[this.field];
7135
7303
  }
7136
7304
 
7305
+ // 切换值
7137
7306
  const newValue = !currentValue;
7138
7307
 
7308
+ // 更新到 map.SourceMap 中
7139
7309
  if (this.map && this.map.SourceMap) {
7140
7310
  this.map.SourceMap[this.field] = newValue;
7141
7311
  }
7142
7312
 
7313
+ // 更新内部状态
7143
7314
  this.isActive = newValue;
7144
- this.updateStatus();
7145
-
7146
- if (this.onToggle) {
7147
- this.onToggle(newValue);
7148
- }
7149
- }
7150
-
7151
- // Mapbox 要求的 onRemove 方法
7152
- onRemove() {
7153
- this.closeDropdown();
7154
7315
 
7155
- if (this._container && this._container.parentNode) {
7156
- this._container.parentNode.removeChild(this._container);
7157
- }
7316
+ // 更新 UI 样式
7317
+ this.updateStatus();
7158
7318
 
7159
- this.map = null;
7160
- this._container = null;
7161
- this._button = null;
7162
- this._dropdown = null;
7163
- this._searchInput = null;
7164
- this._optionsList = null;
7319
+ // 调用回调函数(如果传入了 onToggle),并传入最新的值
7320
+ this.onToggle?.(newValue); // 安全调用,仅当函数存在时才执行
7165
7321
  }
7166
7322
  }
7167
7323