/** * Copyright 2012-2026 ShopeX (https://www.shopex.cn) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * 自定义Select组件 - 兼容微信小程序端 * 通用版本,适用于整个项目 * 解决原生select在微信小程序端的兼容性问题 */ // 性能优化:防抖函数 function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } // 检测环境函数 function detectEnvironment() { const ua = navigator.userAgent.toLowerCase(); const isWechat = /micromessenger/i.test(ua); const isMac = /macintosh|mac os x/i.test(ua); const isWindows = /windows/i.test(ua); const isMobile = /mobile|android|iphone|ipad|phone/i.test(ua); return { isWechat: isWechat, isMobile: isMobile, isIOS: /iphone|ipad|ipod/i.test(ua), isAndroid: /android/i.test(ua), isMac: isMac, isWindows: isWindows, // 专门检测Mac版微信小程序 isMacWechat: isWechat && isMac && !isMobile, // 检测Windows版微信小程序 isWindowsWechat: isWechat && isWindows && !isMobile, // 检测桌面版微信小程序 isDesktopWechat: isWechat && !isMobile && (isMac || isWindows) }; } // 全局实例管理 window.customSelectInstances = window.customSelectInstances || []; class CustomSelect { constructor(element, options = {}) { // 验证输入 if (!element) { throw new Error('CustomSelect: element is required'); } this.element = element; this.options = { placeholder: '请选择', searchable: false, disabled: false, clearable: false, maxHeight: '12.5rem', onChange: null, onOpen: null, onClose: null, ...options }; this.isOpen = false; this.selectedValue = ''; this.selectedText = ''; this.originalSelect = null; this.id = 'custom-select-' + Math.random().toString(36).substr(2, 9); // 防抖处理 this.debouncedClose = debounce(() => this.close(), 100); try { this.init(); // 添加到全局实例管理 window.customSelectInstances.push(this); } catch (error) { console.error('CustomSelect initialization failed:', error); throw error; } } init() { // 如果传入的是原生select元素,保存引用并隐藏 if (this.element.tagName === 'SELECT') { this.originalSelect = this.element; this.originalSelect.style.display = 'none'; // 获取原生select的选项 this.optionsData = Array.from(this.originalSelect.options).map(option => ({ value: option.value, text: option.textContent, selected: option.selected, disabled: option.disabled })); // 获取默认选中项 const selectedOption = this.optionsData.find(opt => opt.selected); if (selectedOption) { this.selectedValue = selectedOption.value; this.selectedText = selectedOption.text; } // 在原生select后面插入自定义组件 this.container = this.createCustomSelect(); // 检查是否在.image-text_2容器中 const imageTextContainer = this.originalSelect.closest('.image-text_2'); if (imageTextContainer) { // 如果在.image-text_2容器中,直接替换到容器内 imageTextContainer.appendChild(this.container); } else { // 否则在原生select后面插入 this.originalSelect.parentNode.insertBefore(this.container, this.originalSelect.nextSibling); } } else { // 如果传入的是容器元素 this.container = this.element; this.optionsData = this.options.data || []; } this.bindEvents(); this.updateDisplay(); } createCustomSelect() { const container = document.createElement('div'); container.className = 'custom-select'; // 复制原生select的类名和属性 if (this.originalSelect) { const classes = this.originalSelect.className.split(' ').filter(cls => cls !== 'select-box'); container.className += ' ' + classes.join(' '); // 复制name属性 if (this.originalSelect.name) { container.setAttribute('data-name', this.originalSelect.name); } } // 检查是否在.image-text_2容器中,如果是则不显示箭头(使用原有的箭头) const isInImageText = this.originalSelect && this.originalSelect.closest('.image-text_2'); // 获取箭头图标路径 const arrowSrc = this.getArrowIconPath(); container.innerHTML = `
${this.selectedText || this.options.placeholder} ${!isInImageText ? `arrow` : ''}
${this.renderOptions()}
`; return container; } getArrowIconPath() { // 尝试获取正确的箭头图标路径 const possiblePaths = [ './icon-arrow-down.png', '../img/icon-arrow-down.png', '/app/wap/statics/img/icon-arrow-down.png' ]; // 如果页面中已有箭头图标,使用相同路径 const existingArrow = document.querySelector('.thumbnail_2, img[src*="arrow-down"]'); if (existingArrow && existingArrow.src) { return existingArrow.src; } return possiblePaths[0]; // 默认使用相对路径 } renderOptions() { if (!this.optionsData || this.optionsData.length === 0) { return '
暂无选项
'; } return this.optionsData.map(option => `
${option.text}
`).join(''); } bindEvents() { const trigger = this.container.querySelector('.custom-select-trigger'); const mask = this.container.querySelector('.custom-select-mask'); const options = this.container.querySelectorAll('.custom-select-option'); // 点击触发器 - 支持触摸和点击 const handleTriggerActivation = (e) => { e.preventDefault(); e.stopPropagation(); this.toggle(); }; trigger.addEventListener('click', handleTriggerActivation); trigger.addEventListener('touchend', handleTriggerActivation); // 防止触摸滚动时意外触发 trigger.addEventListener('touchstart', (e) => { this.touchStartY = e.touches[0].clientY; }); trigger.addEventListener('touchmove', (e) => { if (this.touchStartY) { const touchMoveY = e.touches[0].clientY; const diff = Math.abs(touchMoveY - this.touchStartY); if (diff > 10) { // 如果移动超过10px,认为是滚动 this.isTouchScrolling = true; } } }); trigger.addEventListener('touchend', (e) => { if (this.isTouchScrolling) { this.isTouchScrolling = false; e.preventDefault(); return; } this.touchStartY = null; }); // 点击遮罩关闭 mask.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); this.close(); }); // 点击选项 - 优化触摸体验 options.forEach(option => { const handleOptionSelect = (e) => { e.preventDefault(); e.stopPropagation(); if (option.getAttribute('data-disabled') === 'true') { return; } const value = option.getAttribute('data-value'); const text = option.textContent.trim(); this.select(value, text); }; // 添加触摸反馈 option.addEventListener('touchstart', (e) => { if (option.getAttribute('data-disabled') !== 'true') { option.style.backgroundColor = '#f0f0f0'; } }); option.addEventListener('touchend', (e) => { option.style.backgroundColor = ''; handleOptionSelect(e); }); option.addEventListener('touchcancel', (e) => { option.style.backgroundColor = ''; }); option.addEventListener('click', handleOptionSelect); }); // 阻止选项容器的点击事件冒泡 const optionsContainer = this.container.querySelector('.custom-select-options'); optionsContainer.addEventListener('click', (e) => { e.stopPropagation(); }); // 键盘事件支持 trigger.addEventListener('keydown', (e) => { switch (e.key) { case 'Enter': case ' ': e.preventDefault(); this.toggle(); break; case 'Escape': this.close(); break; case 'ArrowDown': e.preventDefault(); if (!this.isOpen) { this.open(); } else { this.selectNext(); } break; case 'ArrowUp': e.preventDefault(); if (this.isOpen) { this.selectPrevious(); } break; } }); // 全局点击关闭 document.addEventListener('click', (e) => { if (!this.container.contains(e.target)) { this.close(); } }); } toggle() { if (this.options.disabled) return; if (this.isOpen) { this.close(); } else { this.open(); } } open() { if (this.options.disabled || this.isOpen) return; this.isOpen = true; this.container.classList.add('open'); // 触发回调 if (this.options.onOpen) { this.options.onOpen(); } // 滚动到选中项 this.scrollToSelected(); } close() { if (!this.isOpen) return; this.isOpen = false; this.container.classList.remove('open'); // 触发回调 if (this.options.onClose) { this.options.onClose(); } } select(value, text) { const oldValue = this.selectedValue; this.selectedValue = value; this.selectedText = text; // 更新显示 this.updateDisplay(); // 更新原生select的值 if (this.originalSelect) { this.originalSelect.value = value; // 触发原生select的change事件 const changeEvent = new Event('change', { bubbles: true }); this.originalSelect.dispatchEvent(changeEvent); } // 更新选中状态 this.updateSelectedState(); // 关闭下拉框 this.close(); // 触发回调 if (this.options.onChange && oldValue !== value) { this.options.onChange(value, text); } } updateDisplay() { const textElement = this.container.querySelector('.custom-select-text'); textElement.textContent = this.selectedText || this.options.placeholder; } updateSelectedState() { const options = this.container.querySelectorAll('.custom-select-option'); options.forEach(option => { const value = option.getAttribute('data-value'); if (value === this.selectedValue) { option.classList.add('selected'); } else { option.classList.remove('selected'); } }); } scrollToSelected() { const selectedOption = this.container.querySelector('.custom-select-option.selected'); if (selectedOption) { selectedOption.scrollIntoView({ block: 'nearest' }); } } selectNext() { const options = Array.from(this.container.querySelectorAll('.custom-select-option:not([data-disabled])')); const currentIndex = options.findIndex(opt => opt.classList.contains('selected')); const nextIndex = currentIndex < options.length - 1 ? currentIndex + 1 : 0; if (options[nextIndex]) { const value = options[nextIndex].getAttribute('data-value'); const text = options[nextIndex].textContent; this.select(value, text); } } selectPrevious() { const options = Array.from(this.container.querySelectorAll('.custom-select-option:not([data-disabled])')); const currentIndex = options.findIndex(opt => opt.classList.contains('selected')); const prevIndex = currentIndex > 0 ? currentIndex - 1 : options.length - 1; if (options[prevIndex]) { const value = options[prevIndex].getAttribute('data-value'); const text = options[prevIndex].textContent; this.select(value, text); } } // 公共方法 getValue() { return this.selectedValue; } setText(text) { this.selectedText = text; this.updateDisplay(); } setValue(value) { const option = this.optionsData.find(opt => opt.value === value); if (option) { this.select(value, option.text); } } disable() { this.options.disabled = true; this.container.classList.add('disabled'); this.close(); } enable() { this.options.disabled = false; this.container.classList.remove('disabled'); } destroy() { try { // 关闭下拉框 this.close(); // 移除全局实例引用 const index = window.customSelectInstances.indexOf(this); if (index > -1) { window.customSelectInstances.splice(index, 1); } // 移除DOM元素 if (this.container && this.container.parentNode) { this.container.remove(); } // 显示原生select if (this.originalSelect) { this.originalSelect.style.display = ''; } // 清理引用 this.element = null; this.container = null; this.originalSelect = null; this.optionsData = null; } catch (error) { console.error('CustomSelect destroy failed:', error); } } } // 全局初始化函数 function initCustomSelects(selector = 'select.select-box') { const selects = document.querySelectorAll(selector); const customSelects = []; const errors = []; if (selects.length === 0) { console.warn('CustomSelect: No select elements found with selector:', selector); return customSelects; } selects.forEach((select, index) => { try { // 检查是否已经初始化过 if (select.dataset.customSelectInitialized === 'true') { console.warn('CustomSelect: Element already initialized, skipping:', select); return; } const customSelect = new CustomSelect(select, { onChange: (value, text) => { // 触发自定义事件 const customEvent = new CustomEvent('customSelectChange', { detail: { value, text, element: select }, bubbles: true }); select.dispatchEvent(customEvent); } }); // 标记为已初始化 select.dataset.customSelectInitialized = 'true'; customSelects.push(customSelect); } catch (error) { console.error(`CustomSelect: Failed to initialize select at index ${index}:`, error); errors.push({ index, element: select, error }); } }); if (window.customSelectDebug) { console.log(`CustomSelect: Initialized ${customSelects.length} of ${selects.length} selects`); if (errors.length > 0) { console.warn('CustomSelect: Initialization errors:', errors); } } return customSelects; } // 自动初始化 document.addEventListener('DOMContentLoaded', () => { const env = detectEnvironment(); // 专门针对Mac版微信小程序和移动端微信的兼容性问题 let shouldInitCustomSelect = false; let initReason = ''; if (env.isMacWechat) { shouldInitCustomSelect = true; initReason = 'Mac版微信小程序select兼容性问题'; } else if (env.isWindowsWechat) { shouldInitCustomSelect = true; initReason = 'Windows版微信小程序select兼容性问题'; } else if (env.isWechat && env.isMobile) { shouldInitCustomSelect = true; initReason = '移动端微信select兼容性问题'; } else if (env.isMobile) { shouldInitCustomSelect = true; initReason = '移动端浏览器select体验优化'; } if (shouldInitCustomSelect) { initCustomSelects(); if (window.customSelectDebug) { console.log('CustomSelect 启用原因:', initReason); } } // 调试信息 if (window.customSelectDebug) { console.log('CustomSelect Environment:', env); console.log('CustomSelect 是否启用:', shouldInitCustomSelect); console.log('CustomSelect 启用原因:', initReason); console.log('CustomSelect Instances:', window.customSelectInstances); } }); // 导出类和初始化函数 if (typeof module !== 'undefined' && module.exports) { module.exports = { CustomSelect, initCustomSelects }; } else { window.CustomSelect = CustomSelect; window.initCustomSelects = initCustomSelects; }