|
|
@@ -0,0 +1,733 @@
|
|
|
+/**
|
|
|
+ * jQuery自定义下拉框插件 - 支持搜索、分页、远程数据
|
|
|
+ * 版本: 1.1.0
|
|
|
+ * 依赖: jQuery 3.7.1+
|
|
|
+ */
|
|
|
+(function($) {
|
|
|
+ 'use strict';
|
|
|
+
|
|
|
+ // 默认配置
|
|
|
+ const defaults = {
|
|
|
+ // 数据源配置
|
|
|
+ dataSource: null, // 本地数据数组
|
|
|
+ remoteUrl: null, // 远程API地址
|
|
|
+ remoteMethod: 'POST', // 请求方法 (支持GET/POST)
|
|
|
+ remoteParams: {}, // 额外请求参数
|
|
|
+
|
|
|
+ // 显示配置
|
|
|
+ placeholder: '请选择',
|
|
|
+ searchPlaceholder: '搜索...',
|
|
|
+ itemsPerPage: 10,
|
|
|
+ minSearchLength: 1, // 最小搜索字符长度
|
|
|
+
|
|
|
+ // 字段映射
|
|
|
+ idField: 'id',
|
|
|
+ textField: 'name',
|
|
|
+ valueField: 'id',
|
|
|
+
|
|
|
+ // 分页字段映射 - 根据提供的参数名调整
|
|
|
+ pageField: 'pageNo', // 页码字段
|
|
|
+ sizeField: 'pageRows', // 每页记录数字段
|
|
|
+ searchField: 'keyword', // 搜索字段
|
|
|
+ totalField: 'totalRows', // 总记录数字段
|
|
|
+ dataField: 'dataList', // 数据列表字段
|
|
|
+ pagesField: 'pageCount', // 总页数字段
|
|
|
+
|
|
|
+ // 事件回调
|
|
|
+ onSelect: null,
|
|
|
+ onLoad: null,
|
|
|
+ onError: null,
|
|
|
+ onBeforeLoad: null, // 新增:加载前回调
|
|
|
+
|
|
|
+ // 其他
|
|
|
+ allowClear: true,
|
|
|
+ cacheRemoteData: true,
|
|
|
+ ajaxTimeout: 10000,
|
|
|
+ debounceDelay: 300, // 防抖延迟(毫秒)
|
|
|
+ showLoadingText: '加载中...',
|
|
|
+ noDataText: '暂无数据',
|
|
|
+ noSearchResultText: '没有找到匹配的数据',
|
|
|
+ errorText: '数据加载失败'
|
|
|
+ };
|
|
|
+
|
|
|
+ // 下拉框类
|
|
|
+ class CustomDropdown {
|
|
|
+ constructor(element, options) {
|
|
|
+ this.$element = $(element);
|
|
|
+ this.options = $.extend({}, defaults, options);
|
|
|
+ this.init();
|
|
|
+ }
|
|
|
+
|
|
|
+ init() {
|
|
|
+ // 创建DOM结构
|
|
|
+ this.createStructure();
|
|
|
+
|
|
|
+ // 初始化状态
|
|
|
+ this.currentPage = 1;
|
|
|
+ this.totalPages = 1;
|
|
|
+ this.totalItems = 0;
|
|
|
+ this.searchTerm = '';
|
|
|
+ this.selectedItem = null;
|
|
|
+ this.cachedData = null;
|
|
|
+ this.isLoading = false;
|
|
|
+ this.hasMore = true;
|
|
|
+ this.lastRequestId = 0; // 用于防止重复请求
|
|
|
+
|
|
|
+ // 绑定事件
|
|
|
+ this.bindEvents();
|
|
|
+
|
|
|
+ // 初始化数据
|
|
|
+ this.initData();
|
|
|
+ }
|
|
|
+
|
|
|
+ createStructure() {
|
|
|
+ // 如果已经初始化过,则跳过
|
|
|
+ if (this.$element.find('.dropdown-toggle').length > 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建隐藏的input用于存储选中值
|
|
|
+ const name = this.$element.attr('name') || this.$element.data('name') || 'dropdown_value';
|
|
|
+ const initialValue = this.$element.val() || '';
|
|
|
+
|
|
|
+ this.$element.wrap('<div class="custom-dropdown"></div>');
|
|
|
+ this.$dropdown = this.$element.parent();
|
|
|
+
|
|
|
+ // 添加HTML结构
|
|
|
+ this.$dropdown.html(`
|
|
|
+ <input type="hidden" class="dropdown-value" name="${name}" value="${initialValue}">
|
|
|
+ <button type="button" class="dropdown-toggle">
|
|
|
+ <span class="dropdown-text placeholder">${this.options.placeholder}</span>
|
|
|
+ <span class="dropdown-arrow">▼</span>
|
|
|
+ </button>
|
|
|
+ <div class="dropdown-menu">
|
|
|
+ <div class="dropdown-header">
|
|
|
+ <input type="text" class="search-input" placeholder="${this.options.searchPlaceholder}">
|
|
|
+ </div>
|
|
|
+ <div class="dropdown-items"></div>
|
|
|
+ <div class="dropdown-footer" style="display: none;">
|
|
|
+ <div class="pagination-info">
|
|
|
+ 第 <span class="current-page">1</span> 页,共 <span class="total-pages">1</span> 页
|
|
|
+ <span class="total-info">(共 <span class="total-items">0</span> 条)</span>
|
|
|
+ </div>
|
|
|
+ <div class="pagination-controls">
|
|
|
+ <button class="pagination-btn prev-btn" type="button" disabled>
|
|
|
+ <i class="fas fa-chevron-left"></i> 上一页
|
|
|
+ </button>
|
|
|
+ <button class="pagination-btn next-btn" type="button" disabled>
|
|
|
+ 下一页 <i class="fas fa-chevron-right"></i>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ `);
|
|
|
+
|
|
|
+ // 获取DOM引用
|
|
|
+ this.$toggle = this.$dropdown.find('.dropdown-toggle');
|
|
|
+ this.$text = this.$dropdown.find('.dropdown-text');
|
|
|
+ this.$menu = this.$dropdown.find('.dropdown-menu');
|
|
|
+ this.$search = this.$dropdown.find('.search-input');
|
|
|
+ this.$items = this.$dropdown.find('.dropdown-items');
|
|
|
+ this.$footer = this.$dropdown.find('.dropdown-footer');
|
|
|
+ this.$prevBtn = this.$dropdown.find('.prev-btn');
|
|
|
+ this.$nextBtn = this.$dropdown.find('.next-btn');
|
|
|
+ this.$valueInput = this.$dropdown.find('.dropdown-value');
|
|
|
+ this.$currentPage = this.$dropdown.find('.current-page');
|
|
|
+ this.$totalPages = this.$dropdown.find('.total-pages');
|
|
|
+ this.$totalItems = this.$dropdown.find('.total-items');
|
|
|
+
|
|
|
+ // 隐藏原始select
|
|
|
+ this.$element.hide();
|
|
|
+ }
|
|
|
+
|
|
|
+ bindEvents() {
|
|
|
+ const self = this;
|
|
|
+
|
|
|
+ // 切换下拉菜单
|
|
|
+ this.$toggle.on('click', function(e) {
|
|
|
+ e.stopPropagation();
|
|
|
+ self.toggleDropdown();
|
|
|
+ });
|
|
|
+
|
|
|
+ // 搜索输入(防抖处理)
|
|
|
+ let searchTimeout;
|
|
|
+ this.$search.on('input', function() {
|
|
|
+ clearTimeout(searchTimeout);
|
|
|
+ searchTimeout = setTimeout(() => {
|
|
|
+ self.handleSearch($(this).val());
|
|
|
+ }, self.options.debounceDelay);
|
|
|
+ });
|
|
|
+
|
|
|
+ // 搜索框回车键
|
|
|
+ this.$search.on('keydown', function(e) {
|
|
|
+ if (e.key === 'Enter') {
|
|
|
+ self.handleSearch($(this).val());
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 分页按钮
|
|
|
+ this.$prevBtn.on('click', function() {
|
|
|
+ if (self.currentPage > 1) {
|
|
|
+ self.currentPage--;
|
|
|
+ self.loadData();
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ this.$nextBtn.on('click', function() {
|
|
|
+ if (self.currentPage < self.totalPages) {
|
|
|
+ self.currentPage++;
|
|
|
+ self.loadData();
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 点击外部关闭
|
|
|
+ $(document).on('click', function(e) {
|
|
|
+ if (!self.$dropdown.is(e.target) && self.$dropdown.has(e.target).length === 0) {
|
|
|
+ self.closeDropdown();
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 阻止下拉菜单内部点击事件冒泡
|
|
|
+ this.$menu.on('click', function(e) {
|
|
|
+ e.stopPropagation();
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ initData() {
|
|
|
+ const initialValue = this.$valueInput.val();
|
|
|
+
|
|
|
+ if (initialValue) {
|
|
|
+ this.loadInitialValue(initialValue);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果是本地数据源,直接加载
|
|
|
+ if (this.options.dataSource && !this.options.remoteUrl) {
|
|
|
+ this.cachedData = this.options.dataSource;
|
|
|
+ this.totalItems = this.cachedData.length;
|
|
|
+ this.totalPages = Math.ceil(this.totalItems / this.options.itemsPerPage);
|
|
|
+ this.renderItems();
|
|
|
+ this.updatePagination();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ loadInitialValue(value) {
|
|
|
+ if (!value) return;
|
|
|
+
|
|
|
+ // 如果是远程数据,需要根据ID获取详情
|
|
|
+ if (this.options.remoteUrl) {
|
|
|
+ this.loadItemById(value);
|
|
|
+ } else if (this.options.dataSource) {
|
|
|
+ // 本地数据中查找
|
|
|
+ const item = this.options.dataSource.find(item =>
|
|
|
+ String(item[this.options.valueField]) === String(value)
|
|
|
+ );
|
|
|
+
|
|
|
+ if (item) {
|
|
|
+ this.setSelected(item);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async loadItemById(id) {
|
|
|
+ try {
|
|
|
+ const params = $.extend({}, this.options.remoteParams, {
|
|
|
+ [this.options.idField]: id
|
|
|
+ });
|
|
|
+
|
|
|
+ const requestId = ++this.lastRequestId;
|
|
|
+ this.isLoading = true;
|
|
|
+
|
|
|
+ const response = await $.ajax({
|
|
|
+ url: this.options.remoteUrl,
|
|
|
+ method: this.options.remoteMethod,
|
|
|
+ data: this.options.remoteMethod === 'GET' ? params : JSON.stringify(params),
|
|
|
+ dataType: 'json',
|
|
|
+ contentType: this.options.remoteMethod === 'POST' ? 'application/json' : 'application/x-www-form-urlencoded',
|
|
|
+ timeout: this.options.ajaxTimeout,
|
|
|
+ beforeSend: () => {
|
|
|
+ // 如果请求已过期,中止
|
|
|
+ if (requestId !== this.lastRequestId) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (response && response[this.options.dataField]) {
|
|
|
+ const item = response[this.options.dataField];
|
|
|
+ if (Array.isArray(item) && item.length > 0) {
|
|
|
+ this.setSelected(item[0]);
|
|
|
+ } else if (typeof item === 'object') {
|
|
|
+ this.setSelected(item);
|
|
|
+ }
|
|
|
+ } else if (response && typeof response === 'object') {
|
|
|
+ // 尝试直接使用响应对象
|
|
|
+ this.setSelected(response);
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('加载选中项失败:', error);
|
|
|
+ if (this.options.onError) {
|
|
|
+ this.options.onError.call(this, error, 'loadItem');
|
|
|
+ }
|
|
|
+ } finally {
|
|
|
+ this.isLoading = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ toggleDropdown() {
|
|
|
+ if (this.$menu.is(':visible')) {
|
|
|
+ this.closeDropdown();
|
|
|
+ } else {
|
|
|
+ this.openDropdown();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ openDropdown() {
|
|
|
+ this.$menu.show();
|
|
|
+ this.$toggle.addClass('active');
|
|
|
+
|
|
|
+ // 如果还没有加载过数据,或者需要刷新数据
|
|
|
+ if (!this.cachedData || this.options.remoteUrl) {
|
|
|
+ this.currentPage = 1;
|
|
|
+ this.loadData();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 聚焦搜索框
|
|
|
+ setTimeout(() => this.$search.focus(), 10);
|
|
|
+ }
|
|
|
+
|
|
|
+ closeDropdown() {
|
|
|
+ this.$menu.hide();
|
|
|
+ this.$toggle.removeClass('active');
|
|
|
+ }
|
|
|
+
|
|
|
+ handleSearch(keyword) {
|
|
|
+ const newSearchTerm = keyword.trim();
|
|
|
+
|
|
|
+ // 如果搜索词没变化,不执行搜索
|
|
|
+ if (newSearchTerm === this.searchTerm) return;
|
|
|
+
|
|
|
+ this.searchTerm = newSearchTerm;
|
|
|
+ this.currentPage = 1;
|
|
|
+ this.loadData();
|
|
|
+ }
|
|
|
+
|
|
|
+ async loadData() {
|
|
|
+ // 防止重复加载
|
|
|
+ if (this.isLoading) return;
|
|
|
+
|
|
|
+ // 触发加载前回调
|
|
|
+ if (this.options.onBeforeLoad) {
|
|
|
+ const shouldContinue = this.options.onBeforeLoad.call(this, {
|
|
|
+ page: this.currentPage,
|
|
|
+ searchTerm: this.searchTerm
|
|
|
+ });
|
|
|
+
|
|
|
+ if (shouldContinue === false) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ this.isLoading = true;
|
|
|
+ this.showLoading();
|
|
|
+
|
|
|
+ const requestId = ++this.lastRequestId;
|
|
|
+
|
|
|
+ try {
|
|
|
+ if (this.options.remoteUrl) {
|
|
|
+ await this.loadRemoteData(requestId);
|
|
|
+ } else if (this.options.dataSource) {
|
|
|
+ this.loadLocalData();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查请求是否过期
|
|
|
+ if (requestId !== this.lastRequestId) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ this.updatePagination();
|
|
|
+
|
|
|
+ if (this.options.onLoad) {
|
|
|
+ this.options.onLoad.call(this, this.cachedData, {
|
|
|
+ page: this.currentPage,
|
|
|
+ total: this.totalItems,
|
|
|
+ pages: this.totalPages
|
|
|
+ });
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ // 检查请求是否过期
|
|
|
+ if (requestId !== this.lastRequestId) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ this.showError(this.options.errorText + ': ' + (error.message || '未知错误'));
|
|
|
+ console.error('加载数据失败:', error);
|
|
|
+
|
|
|
+ if (this.options.onError) {
|
|
|
+ this.options.onError.call(this, error, 'loadData');
|
|
|
+ }
|
|
|
+ } finally {
|
|
|
+ // 检查请求是否过期
|
|
|
+ if (requestId === this.lastRequestId) {
|
|
|
+ this.isLoading = false;
|
|
|
+ this.hideLoading();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async loadRemoteData(requestId) {
|
|
|
+ // 构建请求参数
|
|
|
+ const params = {
|
|
|
+ [this.options.pageField]: this.currentPage,
|
|
|
+ [this.options.sizeField]: this.options.itemsPerPage
|
|
|
+ };
|
|
|
+
|
|
|
+ // 添加搜索关键词
|
|
|
+ if (this.searchTerm && this.searchTerm.length >= this.options.minSearchLength) {
|
|
|
+ params[this.options.searchField] = this.searchTerm;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 合并额外参数
|
|
|
+ const requestParams = $.extend({}, this.options.remoteParams, params);
|
|
|
+
|
|
|
+ // 准备AJAX配置
|
|
|
+ const ajaxConfig = {
|
|
|
+ url: this.options.remoteUrl,
|
|
|
+ method: this.options.remoteMethod,
|
|
|
+ dataType: 'json',
|
|
|
+ timeout: this.options.ajaxTimeout,
|
|
|
+ beforeSend: () => {
|
|
|
+ // 如果请求已过期,中止
|
|
|
+ if (requestId !== this.lastRequestId) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 根据请求方法设置数据和内容类型
|
|
|
+ if (this.options.remoteMethod.toUpperCase() === 'GET') {
|
|
|
+ ajaxConfig.data = requestParams;
|
|
|
+ } else {
|
|
|
+ // POST请求
|
|
|
+ ajaxConfig.data = JSON.stringify(requestParams);
|
|
|
+ ajaxConfig.contentType = 'application/json; charset=UTF-8';
|
|
|
+ ajaxConfig.processData = false; // 防止jQuery处理数据
|
|
|
+ }
|
|
|
+
|
|
|
+ const response = await $.ajax(ajaxConfig);
|
|
|
+
|
|
|
+ // 检查请求是否过期
|
|
|
+ if (requestId !== this.lastRequestId) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理响应数据
|
|
|
+ if (response) {
|
|
|
+ // 支持不同的响应格式
|
|
|
+ let data, total, pages;
|
|
|
+
|
|
|
+ // 格式1: 标准格式
|
|
|
+ if (response[this.options.dataField] !== undefined) {
|
|
|
+ data = response[this.options.dataField];
|
|
|
+ total = response[this.options.totalField];
|
|
|
+ pages = response[this.options.pagesField];
|
|
|
+ }
|
|
|
+ // 格式2: 常见REST格式
|
|
|
+ else if (response.data !== undefined) {
|
|
|
+ data = response.data;
|
|
|
+ total = response.total || response.totalRows || response.totalCount;
|
|
|
+ pages = response.pages || response.pageCount || response.totalPages;
|
|
|
+ }
|
|
|
+ // 格式3: 直接是数组
|
|
|
+ else if (Array.isArray(response)) {
|
|
|
+ data = response;
|
|
|
+ total = response.length;
|
|
|
+ pages = 1;
|
|
|
+ }
|
|
|
+ // 格式4: 其他格式
|
|
|
+ else {
|
|
|
+ data = response.items || response.list || response.records || [];
|
|
|
+ total = response.total || response.totalRows || response.totalCount || data.length;
|
|
|
+ pages = response.pages || response.pageCount || response.totalPages ||
|
|
|
+ Math.ceil(total / this.options.itemsPerPage);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 确保数据是数组
|
|
|
+ if (!Array.isArray(data)) {
|
|
|
+ data = [];
|
|
|
+ }
|
|
|
+
|
|
|
+ this.cachedData = data;
|
|
|
+ this.totalItems = total || data.length;
|
|
|
+ this.totalPages = pages || Math.ceil(this.totalItems / this.options.itemsPerPage) || 1;
|
|
|
+
|
|
|
+ this.renderItems();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ loadLocalData() {
|
|
|
+ let filteredData = this.options.dataSource || [];
|
|
|
+
|
|
|
+ // 本地搜索过滤
|
|
|
+ if (this.searchTerm && this.searchTerm.length >= this.options.minSearchLength) {
|
|
|
+ const term = this.searchTerm.toLowerCase();
|
|
|
+ filteredData = filteredData.filter(item => {
|
|
|
+ const text = String(item[this.options.textField] || '').toLowerCase();
|
|
|
+ return text.includes(term);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 分页
|
|
|
+ const startIndex = (this.currentPage - 1) * this.options.itemsPerPage;
|
|
|
+ const endIndex = startIndex + this.options.itemsPerPage;
|
|
|
+ const pageData = filteredData.slice(startIndex, endIndex);
|
|
|
+
|
|
|
+ this.cachedData = pageData;
|
|
|
+ this.totalItems = filteredData.length;
|
|
|
+ this.totalPages = Math.ceil(filteredData.length / this.options.itemsPerPage);
|
|
|
+
|
|
|
+ this.renderItems();
|
|
|
+ }
|
|
|
+
|
|
|
+ renderItems() {
|
|
|
+ this.$items.empty();
|
|
|
+
|
|
|
+ if (!this.cachedData || this.cachedData.length === 0) {
|
|
|
+ this.showNoData();
|
|
|
+ this.$footer.hide();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 显示分页
|
|
|
+ if (this.totalPages > 1) {
|
|
|
+ this.$footer.show();
|
|
|
+ } else {
|
|
|
+ this.$footer.hide();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 渲染数据项
|
|
|
+ this.cachedData.forEach(item => {
|
|
|
+ const id = item[this.options.idField] || item.id;
|
|
|
+ const text = item[this.options.textField] || item.name || item.text || '';
|
|
|
+ const value = item[this.options.valueField] || id;
|
|
|
+
|
|
|
+ const isSelected = this.selectedItem &&
|
|
|
+ String(this.selectedItem[this.options.valueField]) === String(value);
|
|
|
+
|
|
|
+ const $item = $(`
|
|
|
+ <div class="dropdown-item ${isSelected ? 'selected' : ''}"
|
|
|
+ data-id="${id}"
|
|
|
+ data-value="${value}">
|
|
|
+ <div class="item-text">${this.escapeHtml(text)}</div>
|
|
|
+ </div>
|
|
|
+ `);
|
|
|
+
|
|
|
+ // 绑定点击事件
|
|
|
+ $item.on('click', () => {
|
|
|
+ this.selectItem(item);
|
|
|
+ });
|
|
|
+
|
|
|
+ this.$items.append($item);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ selectItem(item) {
|
|
|
+ this.selectedItem = item;
|
|
|
+
|
|
|
+ // 更新显示文本
|
|
|
+ const displayText = item[this.options.textField] || item.name || item.text || '';
|
|
|
+ this.$text.text(displayText).removeClass('placeholder');
|
|
|
+
|
|
|
+ // 更新隐藏的值
|
|
|
+ const value = item[this.options.valueField] || item[this.options.idField] || item.id;
|
|
|
+ this.$valueInput.val(value);
|
|
|
+
|
|
|
+ // 触发原始select的change事件
|
|
|
+ this.$element.val(value).trigger('change');
|
|
|
+
|
|
|
+ // 关闭下拉菜单
|
|
|
+ this.closeDropdown();
|
|
|
+
|
|
|
+ // 触发回调
|
|
|
+ if (this.options.onSelect) {
|
|
|
+ this.options.onSelect.call(this, item, value);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 重新渲染以更新选中状态
|
|
|
+ this.renderItems();
|
|
|
+ }
|
|
|
+
|
|
|
+ setSelected(item) {
|
|
|
+ this.selectedItem = item;
|
|
|
+
|
|
|
+ // 更新显示文本
|
|
|
+ const displayText = item[this.options.textField] || item.name || item.text || '';
|
|
|
+ this.$text.text(displayText).removeClass('placeholder');
|
|
|
+
|
|
|
+ // 更新隐藏的值
|
|
|
+ const value = item[this.options.valueField] || item[this.options.idField] || item.id;
|
|
|
+ this.$valueInput.val(value);
|
|
|
+ this.$element.val(value);
|
|
|
+
|
|
|
+ // 重新渲染以更新选中状态
|
|
|
+ if (this.cachedData) {
|
|
|
+ this.renderItems();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ clearSelection() {
|
|
|
+ this.selectedItem = null;
|
|
|
+ this.$text.text(this.options.placeholder).addClass('placeholder');
|
|
|
+ this.$valueInput.val('');
|
|
|
+ this.$element.val('');
|
|
|
+ this.$element.trigger('change');
|
|
|
+
|
|
|
+ if (this.cachedData) {
|
|
|
+ this.renderItems();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ updatePagination() {
|
|
|
+ this.$currentPage.text(this.currentPage);
|
|
|
+ this.$totalPages.text(this.totalPages);
|
|
|
+ this.$totalItems.text(this.totalItems);
|
|
|
+
|
|
|
+ // 更新按钮状态
|
|
|
+ this.$prevBtn.prop('disabled', this.currentPage <= 1);
|
|
|
+ this.$nextBtn.prop('disabled', this.currentPage >= this.totalPages);
|
|
|
+
|
|
|
+ // 隐藏分页如果只有一页
|
|
|
+ if (this.totalPages <= 1) {
|
|
|
+ this.$footer.hide();
|
|
|
+ } else {
|
|
|
+ this.$footer.show();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ showLoading() {
|
|
|
+ this.$items.html(`<div class="loading-indicator">${this.options.showLoadingText}</div>`);
|
|
|
+ this.$footer.hide();
|
|
|
+ }
|
|
|
+
|
|
|
+ hideLoading() {
|
|
|
+ // 由renderItems处理
|
|
|
+ }
|
|
|
+
|
|
|
+ showNoData() {
|
|
|
+ const message = this.searchTerm ? this.options.noSearchResultText : this.options.noDataText;
|
|
|
+ this.$items.html(`<div class="no-data">${message}</div>`);
|
|
|
+ }
|
|
|
+
|
|
|
+ showError(message) {
|
|
|
+ this.$items.html(`<div class="dropdown-error">${message}</div>`);
|
|
|
+ }
|
|
|
+
|
|
|
+ escapeHtml(text) {
|
|
|
+ const div = document.createElement('div');
|
|
|
+ div.textContent = text;
|
|
|
+ return div.innerHTML;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 公开方法
|
|
|
+ getValue() {
|
|
|
+ return this.$valueInput.val();
|
|
|
+ }
|
|
|
+
|
|
|
+ getSelectedItem() {
|
|
|
+ return this.selectedItem;
|
|
|
+ }
|
|
|
+
|
|
|
+ refresh() {
|
|
|
+ this.currentPage = 1;
|
|
|
+ this.searchTerm = '';
|
|
|
+ this.$search.val('');
|
|
|
+ this.lastRequestId++; // 使之前的请求过期
|
|
|
+ this.loadData();
|
|
|
+ }
|
|
|
+
|
|
|
+ destroy() {
|
|
|
+ // 移除事件绑定
|
|
|
+ this.$toggle.off('click');
|
|
|
+ this.$search.off('input');
|
|
|
+ this.$prevBtn.off('click');
|
|
|
+ this.$nextBtn.off('click');
|
|
|
+ $(document).off('click');
|
|
|
+ this.$menu.off('click');
|
|
|
+
|
|
|
+ // 恢复原始select
|
|
|
+ this.$element.show().unwrap();
|
|
|
+
|
|
|
+ // 移除插件实例
|
|
|
+ this.$element.removeData('customDropdown');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // jQuery插件定义
|
|
|
+ $.fn.customDropdown = function(options) {
|
|
|
+ return this.each(function() {
|
|
|
+ const $this = $(this);
|
|
|
+
|
|
|
+ // 如果已经初始化,返回实例
|
|
|
+ const instance = $this.data('customDropdown');
|
|
|
+ if (instance) {
|
|
|
+ if (typeof options === 'string') {
|
|
|
+ return instance[options]();
|
|
|
+ }
|
|
|
+ return instance;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建新实例
|
|
|
+ const dropdown = new CustomDropdown(this, options);
|
|
|
+ $this.data('customDropdown', dropdown);
|
|
|
+
|
|
|
+ return dropdown;
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ // 添加公共方法
|
|
|
+ $.fn.customDropdown.getValue = function() {
|
|
|
+ const instance = $(this).data('customDropdown');
|
|
|
+ return instance ? instance.getValue() : null;
|
|
|
+ };
|
|
|
+
|
|
|
+ $.fn.customDropdown.getSelectedItem = function() {
|
|
|
+ const instance = $(this).data('customDropdown');
|
|
|
+ return instance ? instance.getSelectedItem() : null;
|
|
|
+ };
|
|
|
+
|
|
|
+ $.fn.customDropdown.clearSelection = function() {
|
|
|
+ return this.each(function() {
|
|
|
+ const instance = $(this).data('customDropdown');
|
|
|
+ if (instance) {
|
|
|
+ instance.clearSelection();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ $.fn.customDropdown.refresh = function() {
|
|
|
+ return this.each(function() {
|
|
|
+ const instance = $(this).data('customDropdown');
|
|
|
+ if (instance) {
|
|
|
+ instance.refresh();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ $.fn.customDropdown.setSelected = function(item) {
|
|
|
+ return this.each(function() {
|
|
|
+ const instance = $(this).data('customDropdown');
|
|
|
+ if (instance) {
|
|
|
+ instance.setSelected(item);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ $.fn.customDropdown.destroy = function() {
|
|
|
+ return this.each(function() {
|
|
|
+ const instance = $(this).data('customDropdown');
|
|
|
+ if (instance) {
|
|
|
+ instance.destroy();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+})(jQuery);
|