raintee-maputils 1.0.34 → 1.0.36

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,245 +6561,959 @@ class RulerControl {
6561
6561
 
6562
6562
  class CustomOptionsControl {
6563
6563
  constructor(args) {
6564
- const { options, onConfirm } = args;
6564
+ const { title, options, onConfirm, icon } = args;
6565
6565
 
6566
- this._container = null;
6567
6566
  this._map = null;
6567
+ this._container = null;
6568
6568
 
6569
+ this._title = title || '';
6569
6570
  this._options = options || [];
6571
+ this._icon = icon || null;
6570
6572
  this._onConfirm = onConfirm || (() => { });
6571
-
6572
- if (!this._options || !Array.isArray(this._options)) {
6573
- console.error('请传入有效的 options 参数(数组)');
6574
- }
6573
+ this._originalOptions = [...this._options];
6574
+ this._selectedOptions = [];
6575
6575
  }
6576
6576
 
6577
6577
  onAdd(map) {
6578
6578
  this._map = map;
6579
6579
 
6580
- // 创建控件外层容器
6581
6580
  this._container = document.createElement('div');
6582
6581
  this._container.className = 'mapboxgl-ctrl mapboxgl-ctrl-group custom-options-control';
6583
6582
 
6584
- // 创建主按钮(初始只显示“三维模型”文字,点击展开)
6585
6583
  const mainButton = document.createElement('button');
6586
6584
  mainButton.style.cssText = `
6587
- width: 100%;
6588
- height: 100%;
6589
- padding: 0.25rem 0.5rem;
6590
- margin: 0;
6591
- border: 1px solid #ccc;
6585
+ padding: 0;
6592
6586
  background: white;
6593
- border-radius: 4px;
6587
+ border: 0.0625rem solid #ccc;
6594
6588
  cursor: pointer;
6595
- font-size: 14px;
6596
- text-align: left;
6589
+ display: flex;
6590
+ align-items: center;
6591
+ justify-content: center;
6592
+ gap: 0.375rem;
6597
6593
  `;
6598
- mainButton.textContent = '三维模型';
6599
- mainButton.addEventListener('click', () => this._toggleExpanded());
6600
6594
 
6601
- this._container.appendChild(mainButton);
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
+ }
6602
6616
 
6603
- // 创建隐藏的选项面板(默认不显示)
6604
- this._panel = document.createElement('div');
6605
- this._panel.style.display = 'none';
6606
- this._panel.style.padding = '10px';
6607
- this._container.appendChild(this._panel);
6617
+ mainButton.addEventListener('click', () => {
6618
+ this._showModal();
6619
+ });
6620
+
6621
+ this._container.appendChild(mainButton);
6608
6622
 
6609
- // 创建选项列表和按钮组
6610
- this._renderOptionsPanel();
6623
+ // 添加全局样式
6624
+ this._addGlobalStyles();
6611
6625
 
6612
6626
  return this._container;
6613
6627
  }
6614
6628
 
6615
- _toggleExpanded() {
6616
- const isExpanded = this._panel.style.display !== 'none';
6617
- this._setExpanded(!isExpanded);
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);
6618
6738
  }
6619
6739
 
6620
- _setExpanded(isExpanded) {
6621
- this._panel.style.display = isExpanded ? 'block' : 'none';
6740
+ _showModal() {
6741
+ this._overlay = document.createElement('div');
6742
+ this._overlay.className = 'custom-modal-overlay';
6743
+ this._overlay.style.cssText = `
6744
+ position: fixed;
6745
+ top: 0; left: 0; right: 0; bottom: 0;
6746
+ background: rgba(0, 0, 0, 0.5);
6747
+ backdrop-filter: blur(4px);
6748
+ -webkit-backdrop-filter: blur(4px);
6749
+ z-index: 10000;
6750
+ display: flex;
6751
+ justify-content: center;
6752
+ align-items: center;
6753
+ padding: 1rem;
6754
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
6755
+ `;
6756
+
6757
+ this._modal = document.createElement('div');
6758
+ this._modal.className = 'custom-modal';
6759
+ this._modal.style.cssText = `
6760
+ width: 100%;
6761
+ max-width: 26rem;
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);
6769
+ overflow: hidden;
6770
+ display: flex;
6771
+ flex-direction: column;
6772
+ `;
6773
+
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('');
6622
6823
  }
6623
6824
 
6624
- _renderOptionsPanel() {
6625
- // 清空面板内容
6626
- this._panel.innerHTML = '';
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
+ `;
6627
6844
 
6628
- // 创建复选框选项列表
6629
- this._options.forEach((opt) => {
6630
- const optionDiv = document.createElement('div');
6631
- optionDiv.style.marginBottom = '6px';
6845
+ // 添加图标
6846
+ const icon = document.createElement('span');
6847
+ icon.innerHTML = '📋';
6848
+ icon.style.fontSize = '1.25rem';
6849
+ title.insertBefore(icon, title.firstChild);
6632
6850
 
6633
- const label = document.createElement('label');
6634
- label.style.display = 'flex';
6635
- label.style.alignItems = 'center';
6636
- label.style.cursor = 'pointer';
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
+ `;
6637
6858
 
6638
- const checkbox = document.createElement('input');
6639
- checkbox.type = 'checkbox';
6640
- checkbox.value = opt.value;
6641
- checkbox.style.marginRight = '8px';
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());
6642
6890
 
6643
- // 绑定选中状态
6644
- checkbox.checked = this._getSelectedOption(opt.value);
6645
- checkbox.addEventListener('change', () => this._toggleOption(opt.value));
6891
+ header.style.position = 'relative';
6892
+ header.appendChild(title);
6893
+ header.appendChild(subtitle);
6894
+ header.appendChild(closeBtn);
6646
6895
 
6647
- const span = document.createElement('span');
6648
- span.textContent = opt.label;
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
+ `;
6649
6909
 
6650
- label.appendChild(checkbox);
6651
- label.appendChild(span);
6652
- optionDiv.appendChild(label);
6653
- this._panel.appendChild(optionDiv);
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
+
6922
+ const searchInput = document.createElement('input');
6923
+ searchInput.placeholder = '搜索选项...';
6924
+ searchInput.className = 'custom-search-input';
6925
+ searchInput.style.cssText = `
6926
+ width: 100%;
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;
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)';
6654
6941
  });
6655
6942
 
6656
- // 创建按钮组:取消 / 确定
6657
- const buttonGroup = document.createElement('div');
6658
- buttonGroup.style.marginTop = '12px';
6659
- buttonGroup.style.display = 'flex';
6660
- buttonGroup.style.gap = '8px';
6661
- buttonGroup.style.justifyContent = 'flex-end';
6662
-
6663
- const cancelButton = document.createElement('button');
6664
- cancelButton.textContent = '取消';
6665
- cancelButton.style.cssText = `
6666
- padding: 6px 12px;
6667
- background: #ccc;
6668
- border: none;
6669
- border-radius: 4px;
6943
+ searchInput.addEventListener('blur', () => {
6944
+ searchInput.style.borderColor = '#e2e8f0';
6945
+ searchInput.style.boxShadow = 'none';
6946
+ });
6947
+
6948
+ searchInput.addEventListener('input', (e) => {
6949
+ this._renderList(e.target.value);
6950
+ });
6951
+
6952
+ searchContainer.appendChild(searchIcon);
6953
+ searchContainer.appendChild(searchInput);
6954
+ section.appendChild(searchContainer);
6955
+
6956
+ return section;
6957
+ }
6958
+
6959
+ _createFooter() {
6960
+ const footer = document.createElement('div');
6961
+ footer.style.cssText = `
6962
+ padding: 1rem 1.25rem;
6963
+ background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
6964
+ border-top: 1px solid #e2e8f0;
6965
+ display: flex;
6966
+ justify-content: flex-end;
6967
+ gap: 0.75rem;
6968
+ `;
6969
+
6970
+ const cancelBtn = document.createElement('button');
6971
+ cancelBtn.textContent = '取消';
6972
+ cancelBtn.style.cssText = `
6973
+ padding: 0.5rem 1rem;
6974
+ background: white;
6975
+ border: 1px solid #d1d5db;
6976
+ border-radius: 0.375rem;
6670
6977
  cursor: pointer;
6671
- width: auto;
6978
+ font-size: 0.875rem;
6979
+ font-weight: 500;
6980
+ color: #374151;
6981
+ transition: all 0.2s ease;
6982
+ font-family: inherit;
6672
6983
  `;
6673
- cancelButton.addEventListener('click', () => this._handleCancel());
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
+ });
6994
+ cancelBtn.addEventListener('click', () => this._closeModal());
6674
6995
 
6675
- const confirmButton = document.createElement('button');
6676
- confirmButton.textContent = '确定';
6677
- confirmButton.style.cssText = `
6678
- padding: 6px 12px;
6996
+ const okBtn = document.createElement('button');
6997
+ okBtn.textContent = '确定';
6998
+ okBtn.style.cssText = `
6999
+ padding: 0.5rem 1rem;
6679
7000
  background: #007cba;
6680
7001
  color: white;
6681
7002
  border: none;
6682
- border-radius: 4px;
7003
+ border-radius: 0.375rem;
6683
7004
  cursor: pointer;
6684
- width: auto;
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);
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
+ });
7021
+ okBtn.addEventListener('click', () => this._confirm());
7022
+
7023
+ footer.appendChild(cancelBtn);
7024
+ footer.appendChild(okBtn);
7025
+ return footer;
7026
+ }
7027
+
7028
+ _renderList(keyword) {
7029
+ this._listBox.innerHTML = '';
7030
+
7031
+ const list = keyword
7032
+ ? this._originalOptions.filter(
7033
+ (o) =>
7034
+ o.label.toLowerCase().includes(keyword.toLowerCase()) ||
7035
+ String(o.value).toLowerCase().includes(keyword.toLowerCase())
7036
+ )
7037
+ : this._originalOptions;
7038
+
7039
+ if (list.length === 0) {
7040
+ const empty = document.createElement('div');
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
+ `;
7051
+ this._listBox.appendChild(empty);
7052
+ return;
7053
+ }
7054
+
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;
6685
7063
  `;
6686
- confirmButton.addEventListener('click', () => this._handleConfirm());
7064
+ stats.textContent = `找到 ${list.length} 个相关选项`;
7065
+ this._listBox.appendChild(stats);
7066
+
7067
+ list.forEach((opt, index) => {
7068
+ const item = document.createElement('label');
7069
+ item.className = 'custom-list-item';
7070
+ item.style.cssText = `
7071
+ display: flex;
7072
+ align-items: center;
7073
+ padding: 0.75rem;
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;
7082
+ `;
7083
+
7084
+ const checkbox = document.createElement('input');
7085
+ checkbox.type = 'checkbox';
7086
+ checkbox.className = 'custom-checkbox';
7087
+ checkbox.value = opt.value;
7088
+ checkbox.checked = this._selectedOptions.includes(opt.value);
7089
+ checkbox.style.marginRight = '0.75rem';
7090
+
7091
+ const labelSpan = document.createElement('span');
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
+ }
6687
7118
 
6688
- buttonGroup.appendChild(cancelButton);
6689
- buttonGroup.appendChild(confirmButton);
6690
- this._panel.appendChild(buttonGroup);
7119
+ // 更新标题中的统计信息
7120
+ const subtitle = this._modal.querySelector('p');
7121
+ if (subtitle) {
7122
+ subtitle.textContent = `共 ${this._originalOptions.length} 个选项,已选择 ${this._selectedOptions.length} 个`;
7123
+ }
7124
+ });
7125
+
7126
+ item.appendChild(checkbox);
7127
+ item.appendChild(labelSpan);
7128
+
7129
+ // 添加进入动画
7130
+ item.style.opacity = '0';
7131
+ item.style.transform = 'translateY(10px)';
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);
7139
+ });
6691
7140
  }
6692
7141
 
6693
- _getSelectedOption(value) {
6694
- return this._selectedOptions?.includes(value) || false;
7142
+ _highlight(text, key) {
7143
+ if (!key) return text;
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>');
6695
7146
  }
6696
7147
 
6697
- _toggleOption(value) {
6698
- if (!this._selectedOptions) this._selectedOptions = [];
7148
+ _escapeRegExp(string) {
7149
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
7150
+ }
6699
7151
 
6700
- const idx = this._selectedOptions.indexOf(value);
6701
- if (idx > -1) {
6702
- this._selectedOptions.splice(idx, 1);
7152
+ _toggle(value) {
7153
+ const index = this._selectedOptions.indexOf(value);
7154
+ if (index >= 0) {
7155
+ this._selectedOptions.splice(index, 1);
6703
7156
  } else {
6704
7157
  this._selectedOptions.push(value);
6705
7158
  }
6706
-
6707
- // 可选:重新渲染复选框状态(如果需要动态更新 UI,但目前 onchange 已处理)
6708
7159
  }
6709
7160
 
6710
- _handleConfirm() {
6711
- this._setExpanded(false);
6712
-
7161
+ _confirm() {
6713
7162
  this._onConfirm({
6714
- selectedOptions: this._selectedOptions || [],
6715
- unselectedOptions: this._options.filter((item) => !this._selectedOptions?.includes(item.value)),
6716
- allOptions: this._options,
7163
+ selectedOptions: this._selectedOptions,
7164
+ unselectedOptions: this._originalOptions.filter(
7165
+ (o) => !this._selectedOptions.includes(o.value)
7166
+ ),
7167
+ allOptions: this._originalOptions
6717
7168
  });
7169
+ this._closeModal();
6718
7170
  }
6719
7171
 
6720
- _handleCancel() {
6721
- this._setExpanded(false);
7172
+ _closeModal() {
7173
+ if (this._overlay) {
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);
7187
+ }
6722
7188
  }
6723
7189
 
6724
7190
  onRemove() {
6725
- if (this._container && this._container.parentNode) {
6726
- this._container.parentNode.removeChild(this._container);
7191
+ if (this._container) {
7192
+ this._container.remove();
6727
7193
  }
7194
+ this._closeModal();
6728
7195
  }
6729
7196
  }
6730
7197
 
6731
- // 自定义控件类:ToggleControl
7198
+ // 自定义控件类:ToggleControl(带搜索过滤功能)
6732
7199
  class CustomToggleControl {
6733
7200
  /**
6734
7201
  * 构造函数
6735
7202
  * @param {Object} options
6736
- * @param {string} options.name - 控件名称(用于识别,可选展示)
6737
- * @param {string} options.field - 控制的字段名,对应 map.SourceMap[field]
6738
- * @param {boolean} options.defaultValue - 默认值(当 field 未定义时使用)
6739
- * @param {string} options.svgIcon - SVG 图标字符串,例如 '<svg>...</svg>'
6740
- * @param {Function} [options.onToggle] - 可选的回调函数,状态切换后调用,参数为最新的布尔值
7203
+ * @param {string} options.name - 控件名称
7204
+ * @param {string} options.field - 控制的字段名
7205
+ * @param {boolean} options.defaultValue - 默认值
7206
+ * @param {string} options.svgIcon - SVG 图标字符串
7207
+ * @param {Function} [options.onToggle] - 状态切换回调
7208
+ * @param {Array} [options.options] - 可选项目数组 [{id, name, icon?}]
7209
+ * @param {Function} [options.onOptionSelect] - 选项选择回调
7210
+ * @param {boolean} [options.showSearch] - 是否显示搜索框,默认 true
6741
7211
  */
6742
- constructor({ name, field, defaultValue = false, svgIcon, onToggle }) {
7212
+ constructor({
7213
+ name,
7214
+ field,
7215
+ defaultValue = false,
7216
+ svgIcon,
7217
+ onToggle,
7218
+ options = [],
7219
+ onOptionSelect,
7220
+ showSearch = true
7221
+ }) {
6743
7222
  this.name = name;
6744
7223
  this.field = field;
6745
7224
  this.defaultValue = defaultValue;
6746
7225
  this.svgIcon = svgIcon;
6747
- this.onToggle = onToggle; // ✅ 新增:回调函数
7226
+ this.onToggle = onToggle;
7227
+ this.options = options; // 可选项目
7228
+ this.onOptionSelect = onOptionSelect; // 选项选择回调
7229
+ this.showSearch = showSearch; // 是否显示搜索框
6748
7230
 
6749
- // 控件容器
7231
+ // 控件状态
6750
7232
  this._container = null;
6751
7233
  this._button = null;
6752
- this.isActive = false; // 当前是否为激活状态(高亮/true)
7234
+ this._dropdown = null;
7235
+ this._searchInput = null;
7236
+ this._optionsList = null;
7237
+ this.isActive = false;
7238
+ this.isDropdownOpen = false;
7239
+ this.filteredOptions = [...options]; // 过滤后的选项
7240
+
7241
+ // 绑定事件处理函数
7242
+ this._boundHandleClickOutside = this._handleClickOutside.bind(this);
6753
7243
  }
6754
7244
 
6755
7245
  // Mapbox 要求的 onAdd 方法
6756
7246
  onAdd(map) {
6757
7247
  this.map = map;
6758
7248
 
6759
- // 创建外层容器 div
7249
+ // 创建外层容器
6760
7250
  this._container = document.createElement('div');
6761
- this._container.className = 'mapboxgl-ctrl mapboxgl-ctrl-group';
7251
+ this._container.className = 'mapboxgl-ctrl mapboxgl-ctrl-group custom-toggle-control';
6762
7252
 
6763
- // 创建 button 元素
7253
+ // 创建主按钮
6764
7254
  this._button = document.createElement('button');
6765
7255
  this._button.type = 'button';
6766
7256
  this._button.innerHTML = this.svgIcon;
7257
+ this._button.title = this.name;
6767
7258
 
6768
7259
  // 设置按钮样式
6769
- this._button.style.cursor = 'pointer';
6770
- this._button.style.border = 'none';
6771
- this._button.style.background = 'none';
6772
- this._button.style.padding = '0';
6773
- this._button.style.display = 'flex';
6774
- this._button.style.alignItems = 'center';
6775
- this._button.style.justifyContent = 'center';
7260
+ this._setupButtonStyles();
6776
7261
 
6777
7262
  // 初始化状态
6778
7263
  this.updateStatus();
6779
7264
 
6780
- // 绑定点击事件
6781
- this._button.addEventListener('click', () => {
7265
+ // 绑定事件
7266
+ this._button.addEventListener('click', (e) => {
7267
+ e.stopPropagation();
6782
7268
  this.toggle();
6783
7269
  });
6784
7270
 
6785
7271
  this._container.appendChild(this._button);
6786
7272
 
7273
+ // 创建下拉菜单(如果有选项)
7274
+ if (this.options.length > 0) {
7275
+ this._createDropdown();
7276
+ }
7277
+
6787
7278
  return this._container;
6788
7279
  }
6789
7280
 
6790
- // Mapbox 要求的 onRemove 方法
6791
- onRemove() {
6792
- if (this._container && this._container.parentNode) {
6793
- this._container.parentNode.removeChild(this._container);
7281
+ // 设置按钮样式
7282
+ _setupButtonStyles() {
7283
+ Object.assign(this._button.style, {
7284
+ cursor: 'pointer',
7285
+ border: 'none',
7286
+ background: 'none',
7287
+ padding: '8px',
7288
+ display: 'flex',
7289
+ alignItems: 'center',
7290
+ justifyContent: 'center',
7291
+ borderRadius: '4px',
7292
+ transition: 'all 0.2s ease'
7293
+ });
7294
+ }
7295
+
7296
+ // 创建下拉菜单
7297
+ _createDropdown() {
7298
+ // 创建下拉容器
7299
+ this._dropdown = document.createElement('div');
7300
+ this._dropdown.className = 'custom-toggle-dropdown';
7301
+ Object.assign(this._dropdown.style, {
7302
+ position: 'absolute',
7303
+ top: '100%',
7304
+ right: '0',
7305
+ backgroundColor: 'white',
7306
+ border: '1px solid #ccc',
7307
+ borderRadius: '4px',
7308
+ boxShadow: '0 2px 10px rgba(0,0,0,0.1)',
7309
+ zIndex: '1000',
7310
+ minWidth: '200px',
7311
+ maxWidth: '300px',
7312
+ display: 'none',
7313
+ marginTop: '4px'
7314
+ });
7315
+
7316
+ // 创建搜索框(如果需要)
7317
+ if (this.showSearch && this.options.length > 5) {
7318
+ this._createSearchBox();
6794
7319
  }
6795
- this.map = null;
6796
- this._container = null;
6797
- this._button = null;
7320
+
7321
+ // 创建选项列表
7322
+ this._createOptionsList();
7323
+
7324
+ this._container.appendChild(this._dropdown);
7325
+ }
7326
+
7327
+ // 创建搜索框
7328
+ _createSearchBox() {
7329
+ const searchContainer = document.createElement('div');
7330
+ Object.assign(searchContainer.style, {
7331
+ padding: '8px',
7332
+ borderBottom: '1px solid #eee'
7333
+ });
7334
+
7335
+ this._searchInput = document.createElement('input');
7336
+ this._searchInput.type = 'text';
7337
+ this._searchInput.placeholder = '搜索...';
7338
+ this._searchInput.style.cssText = `
7339
+ width: 100%;
7340
+ padding: 6px 8px;
7341
+ border: 1px solid #ddd;
7342
+ border-radius: 4px;
7343
+ fontSize: 12px;
7344
+ outline: none;
7345
+ box-sizing: border-box;
7346
+ `;
7347
+
7348
+ // 绑定搜索事件
7349
+ this._searchInput.addEventListener('input', (e) => {
7350
+ this._filterOptions(e.target.value);
7351
+ });
7352
+
7353
+ // 阻止下拉菜单关闭
7354
+ this._searchInput.addEventListener('click', (e) => {
7355
+ e.stopPropagation();
7356
+ });
7357
+
7358
+ searchContainer.appendChild(this._searchInput);
7359
+ this._dropdown.appendChild(searchContainer);
6798
7360
  }
6799
7361
 
6800
- // 更新控件状态(根据当前 field 值)
7362
+ // 创建选项列表
7363
+ _createOptionsList() {
7364
+ this._optionsList = document.createElement('div');
7365
+ Object.assign(this._optionsList.style, {
7366
+ maxHeight: '200px',
7367
+ overflowY: 'auto',
7368
+ padding: '4px 0'
7369
+ });
7370
+
7371
+ this._renderOptionsList();
7372
+ this._dropdown.appendChild(this._optionsList);
7373
+ }
7374
+
7375
+ // 渲染选项列表
7376
+ _renderOptionsList() {
7377
+ if (!this._optionsList) return;
7378
+
7379
+ this._optionsList.innerHTML = '';
7380
+
7381
+ this.filteredOptions.forEach(option => {
7382
+ const optionItem = document.createElement('div');
7383
+ optionItem.className = 'custom-toggle-option';
7384
+ optionItem.dataset.id = option.id;
7385
+
7386
+ Object.assign(optionItem.style, {
7387
+ padding: '8px 12px',
7388
+ cursor: 'pointer',
7389
+ display: 'flex',
7390
+ alignItems: 'center',
7391
+ gap: '8px',
7392
+ fontSize: '13px',
7393
+ transition: 'background-color 0.2s'
7394
+ });
7395
+
7396
+ // 添加图标(如果有)
7397
+ if (option.icon) {
7398
+ const icon = document.createElement('span');
7399
+ icon.innerHTML = option.icon;
7400
+ icon.style.fontSize = '14px';
7401
+ optionItem.appendChild(icon);
7402
+ }
7403
+
7404
+ // 添加文本
7405
+ const text = document.createElement('span');
7406
+ text.textContent = option.name;
7407
+ optionItem.appendChild(text);
7408
+
7409
+ // 绑定点击事件
7410
+ optionItem.addEventListener('click', (e) => {
7411
+ e.stopPropagation();
7412
+ this._selectOption(option);
7413
+ });
7414
+
7415
+ // 鼠标悬停效果
7416
+ optionItem.addEventListener('mouseenter', () => {
7417
+ optionItem.style.backgroundColor = '#f5f5f5';
7418
+ });
7419
+
7420
+ optionItem.addEventListener('mouseleave', () => {
7421
+ optionItem.style.backgroundColor = 'transparent';
7422
+ });
7423
+
7424
+ this._optionsList.appendChild(optionItem);
7425
+ });
7426
+
7427
+ // 如果没有匹配的选项
7428
+ if (this.filteredOptions.length === 0) {
7429
+ const noResults = document.createElement('div');
7430
+ noResults.textContent = '无匹配项';
7431
+ Object.assign(noResults.style, {
7432
+ padding: '12px',
7433
+ textAlign: 'center',
7434
+ color: '#999',
7435
+ fontSize: '12px'
7436
+ });
7437
+ this._optionsList.appendChild(noResults);
7438
+ }
7439
+ }
7440
+
7441
+ // 过滤选项
7442
+ _filterOptions(searchTerm) {
7443
+ const term = searchTerm.toLowerCase().trim();
7444
+
7445
+ if (!term) {
7446
+ this.filteredOptions = [...this.options];
7447
+ } else {
7448
+ this.filteredOptions = this.options.filter(option =>
7449
+ option.name.toLowerCase().includes(term) ||
7450
+ (option.id && option.id.toString().toLowerCase().includes(term))
7451
+ );
7452
+ }
7453
+
7454
+ this._renderOptionsList();
7455
+ }
7456
+
7457
+ // 选择选项
7458
+ _selectOption(option) {
7459
+ // 调用选项选择回调
7460
+ if (this.onOptionSelect) {
7461
+ this.onOptionSelect(option);
7462
+ }
7463
+
7464
+ // 关闭下拉菜单
7465
+ this.closeDropdown();
7466
+ }
7467
+
7468
+ // 切换下拉菜单显示状态
7469
+ toggle() {
7470
+ if (this.isDropdownOpen) {
7471
+ this.closeDropdown();
7472
+ } else {
7473
+ this.openDropdown();
7474
+ }
7475
+ }
7476
+
7477
+ // 打开下拉菜单
7478
+ openDropdown() {
7479
+ if (!this._dropdown || this.options.length === 0) return;
7480
+
7481
+ this.isDropdownOpen = true;
7482
+ this._dropdown.style.display = 'block';
7483
+
7484
+ // 添加全局点击监听
7485
+ document.addEventListener('click', this._boundHandleClickOutside);
7486
+
7487
+ // 聚焦搜索框
7488
+ if (this._searchInput) {
7489
+ setTimeout(() => {
7490
+ this._searchInput.focus();
7491
+ this._searchInput.value = '';
7492
+ this._filterOptions('');
7493
+ }, 100);
7494
+ }
7495
+ }
7496
+
7497
+ // 关闭下拉菜单
7498
+ closeDropdown() {
7499
+ if (!this._dropdown) return;
7500
+
7501
+ this.isDropdownOpen = false;
7502
+ this._dropdown.style.display = 'none';
7503
+
7504
+ // 移除全局点击监听
7505
+ document.removeEventListener('click', this._boundHandleClickOutside);
7506
+ }
7507
+
7508
+ // 处理外部点击
7509
+ _handleClickOutside(event) {
7510
+ if (!this._container.contains(event.target)) {
7511
+ this.closeDropdown();
7512
+ }
7513
+ }
7514
+
7515
+ // 更新控件状态
6801
7516
  updateStatus() {
6802
- // 从 map.SourceMap 中获取当前 field 的值,如果不存在则使用 defaultValue
6803
7517
  let currentValue = this.defaultValue;
6804
7518
 
6805
7519
  if (
@@ -6810,21 +7524,22 @@ class CustomToggleControl {
6810
7524
  currentValue = this.map.SourceMap[this.field];
6811
7525
  }
6812
7526
 
6813
- this.isActive = currentValue; // true 表示激活,false 表示未激活
7527
+ this.isActive = currentValue;
6814
7528
 
6815
7529
  // 更新按钮样式
6816
7530
  if (this.isActive) {
6817
7531
  this._button.style.opacity = '1';
6818
7532
  this._button.style.filter = 'none';
7533
+ this._button.style.backgroundColor = '#e6f3ff';
6819
7534
  } else {
6820
- this._button.style.opacity = '0.4';
7535
+ this._button.style.opacity = '0.7';
6821
7536
  this._button.style.filter = 'grayscale(100%)';
7537
+ this._button.style.backgroundColor = 'transparent';
6822
7538
  }
6823
7539
  }
6824
7540
 
6825
- // 切换状态
6826
- toggle() {
6827
- // 获取当前值
7541
+ // 切换状态(保持原有功能)
7542
+ toggleState() {
6828
7543
  let currentValue = this.defaultValue;
6829
7544
 
6830
7545
  if (
@@ -6835,22 +7550,34 @@ class CustomToggleControl {
6835
7550
  currentValue = this.map.SourceMap[this.field];
6836
7551
  }
6837
7552
 
6838
- // 切换值
6839
7553
  const newValue = !currentValue;
6840
7554
 
6841
- // 更新到 map.SourceMap 中
6842
7555
  if (this.map && this.map.SourceMap) {
6843
7556
  this.map.SourceMap[this.field] = newValue;
6844
7557
  }
6845
7558
 
6846
- // 更新内部状态
6847
7559
  this.isActive = newValue;
6848
-
6849
- // 更新 UI 样式
6850
7560
  this.updateStatus();
6851
7561
 
6852
- // 调用回调函数(如果传入了 onToggle),并传入最新的值
6853
- this.onToggle?.(newValue); // 安全调用,仅当函数存在时才执行
7562
+ if (this.onToggle) {
7563
+ this.onToggle(newValue);
7564
+ }
7565
+ }
7566
+
7567
+ // Mapbox 要求的 onRemove 方法
7568
+ onRemove() {
7569
+ this.closeDropdown();
7570
+
7571
+ if (this._container && this._container.parentNode) {
7572
+ this._container.parentNode.removeChild(this._container);
7573
+ }
7574
+
7575
+ this.map = null;
7576
+ this._container = null;
7577
+ this._button = null;
7578
+ this._dropdown = null;
7579
+ this._searchInput = null;
7580
+ this._optionsList = null;
6854
7581
  }
6855
7582
  }
6856
7583