Jquery中的事件绑定$("#btn").bind("click",function(){ })

本文介绍了JQuery中如何简化点击事件的绑定过程。原本通过$.bind()方法绑定点击事件的方式较为繁琐,JQuery提供了更简洁的$.click()方法来实现相同的功能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Jquery中的事件绑定:$("#btn").bind("click",function(){  })

因为每次都这么调用太麻烦,所以jquery就用$("#btn").click(function(){})来进行简化

// @ts-nocheck class SweetalertSystem { constructor() { this.fieldRegistry = {}; this.modalRegistry = {}; // 添加模态类型注册表 this.nestedModalStack = []; // 添加嵌套模态栈 this.initCoreComponents(); // 初始化事件处理器映射 this.buttonHandlers = new Map(); this.addEntityButtons = null; } initCoreComponents() { // 注册核心字段类型 this.registerFieldType('text', { render: (config) => { return `<div class="mb-3"> <label class="form-label">${config.label}${config.required ? '*' : ''}</label> <input type="text" class="form-control" name="${config.name}" value="${config.value || ''}" ${config.required ? 'required' : ''} placeholder="${config.placeholder || ''}"> </div>`; } }); this.registerFieldType('select', { render: (config) => { const options = (config.options || []) .map(opt => `<option value="${opt.value}" ${config.value == opt.value ? 'selected' : ''}>${opt.label}</option>`) .join(''); return `<div class="mb-3"> <label class="form-label">${config.label}${config.required ? '*' : ''}</label> <div class="input-group"> <select class="form-select" name="${config.name}" ${config.required ? 'required' : ''}> <option value="">${config.placeholder || '请选择'}</option> ${options} </select> ${config.addButton ? ` <button class="btn btn-outline-secondary add-entity-btn" type="button" data-target-field="${config.name}"> <i class="fa fa-plus"></i> ${config.addButton.label} </button> ` : ''} </div> </div>`; }, // 更新 afterRender 方法 afterRender: (element, config) => { if (config.select2) { const $select = $(element).find('select'); const swalContainer = Swal.getContainer(); this.safeInitSelect2($select, { theme: 'bootstrap', placeholder: config.placeholder || '请选择', allowClear: true, dropdownParent: swalContainer || document.body }); } } }); this.registerFieldType('number', { render: (config) => { return `<div class="mb-3"> <label class="form-label">${config.label}${config.required ? '*' : ''}</label> <input type="number" class="form-control" name="${config.name}" value="${config.value || ''}" ${config.required ? 'required' : ''} step="${config.step || 1}" min="${config.min || 0}"> </div>`; } }); this.registerFieldType('checkbox', { render: (config) => { return `<div class="mb-3 form-check"> <input type="checkbox" class="form-check-input" name="${config.name}" id="${config.name}" ${config.value ? 'checked' : ''}> <label class="form-check-label" for="${config.name}">${config.label}</label> </div>`; } }); } // 修改注册方法 - 接受对象而不是函数 registerFieldType(type, definition) { if (!definition.render || typeof definition.render !== 'function') { throw new Error(`Field type '${type}' must have a render function`); } this.fieldRegistry[type] = definition; } // === 字段渲染方法 === renderTextField(config) { return `<div class="mb-3"> <label class="form-label">${config.label}${config.required ? '*' : ''}</label> <input type="text" class="form-control" name="${config.name}" value="${config.value || ''}" ${config.required ? 'required' : ''} placeholder="${config.placeholder || ''}"> </div>`; } renderSelectField(config) { const options = (config.options || []) .map(opt => `<option value="${opt.value}" ${config.value == opt.value ? 'selected' : ''}>${opt.label}</option>`) .join(''); return `<div class="mb-3"> <label class="form-label">${config.label}${config.required ? '*' : ''}</label> <div class="input-group"> <select class="form-select" name="${config.name}" ${config.required ? 'required' : ''}> <option value="">${config.placeholder || '请选择'}</option> ${options} </select> ${config.addButton ? ` <button class="btn btn-outline-secondary add-entity-btn" type="button" data-target-field="${config.name}"> <i class="fa fa-plus"></i> ${config.addButton.label} </button> ` : ''} </div> </div>`; } renderNumberField(config) { return `<div class="mb-3"> <label class="form-label">${config.label}${config.required ? '*' : ''}</label> <input type="number" class="form-control" name="${config.name}" value="${config.value || ''}" ${config.required ? 'required' : ''} step="${config.step || 1}" min="${config.min || 0}"> </div>`; } renderCheckboxField(config) { return `<div class="mb-3 form-check"> <input type="checkbox" class="form-check-input" name="${config.name}" id="${config.name}" ${config.value ? 'checked' : ''}> <label class="form-check-label" for="${config.name}">${config.label}</label> </div>`; } destroyAllSelect2() { const popup = Swal.getPopup(); if (popup) { // 使用更可靠的选择器 $(popup).find('select.select2-initialized').each(function() { const $select = $(this); try { // 安全销毁 if ($select.data('select2')) { $select.select2('destroy'); } } catch (error) { console.error('Error destroying Select2:', error); } // 移除标记 $select.removeClass('select2-initialized'); }); } } /** * 安全初始化 Select2 * @param {jQuery} $select - jQuery 选择器对象 * @param {Object} options - Select2 配置选项 */ safeInitSelect2($select, options) { // 如果全局禁用标志已设置,跳过初始化 if (window.disableSelect2) return; try { // 检查是否已经初始化 if ($select.data('select2')) { $select.select2('destroy'); } // 初始化 Select2 $select.select2(options); $select.addClass('select2-initialized'); return true; } catch (error) { console.error('Select2 initialization failed:', error); // 禁用后续尝试 window.disableSelect2 = true; return false; } } /** * 安全销毁 Select2 * @param {jQuery} $select - jQuery 选择器对象 */ safeDestroySelect2($select) { try { if ($select.data('select2')) { $select.select2('destroy'); } $select.removeClass('select2-initialized'); return true; } catch (error) { console.error('Select2 destruction failed:', error); return false; } }; // === 核心方法 ===// === 核心方法 === /** * 打开表单弹窗(支持多个新建按钮) * @param {Object} config - 配置对象 */ async openForm(config) { const { title, fields, onSubmit, onAddEntity } = config; // 生成表单HTML const formHTML = fields.map(fieldConfig => { if (!fieldConfig.type) return '<div class="error">Missing field type</div>'; const fieldType = this.fieldRegistry[fieldConfig.type]; if (!fieldType) return `<div class="error">Field type '${fieldConfig.type}' not supported</div>`; return fieldType.render(fieldConfig); }).join(''); // 创建弹窗 return new Promise((resolve) => { Swal.fire({ title, html: `<form>${formHTML}</form>`, focusConfirm: false, showCancelButton: true, confirmButtonText: config.confirmButtonText || '保存', cancelButtonText: config.cancelButtonText || '取消', preConfirm: () => this.collectFormData(), didOpen: () => { // 确保弹窗完全打开后再绑定事件 this.setupMultiButtonHandlers(config, onAddEntity); }, willClose: () => { // 清理资源 this.cleanupMultiButtonHandlers(); } }).then((result) => { if (result.isConfirmed) { onSubmit(result.value); resolve(true); } else { resolve(false); } }); }); } collectFormData() { const data = {}; const popup = Swal.getPopup(); const form = popup.querySelector('form'); if (form) { const formData = new FormData(form); for (const [name, value] of formData.entries()) { // 处理多值字段 if (data[name]) { if (!Array.isArray(data[name])) { data[name] = [data[name]]; } data[name].push(value); } else { data[name] = value; } } } return data; } setupEventHandlers(config) { const popup = Swal.getPopup(); // 绑定添加按钮事件 popup.querySelectorAll('.add-entity-btn').forEach(btn => { btn.addEventListener('click', async () => { const targetField = btn.dataset.targetField; if (config.onAddEntity) { const newItem = await config.onAddEntity(targetField); if (newItem) { this.updateSelectField(popup, targetField, newItem); } } }); }); } // === 模态框注册方法 === registerModalType(name, config) { // 添加afterRender处理 const enhancedConfig = { ...config, afterRender: (config, container) => { // 初始化Select2 container.find('.select2-field').each(function() { const $select = $(this); $select.select2({ theme: 'bootstrap', placeholder: $select.attr('placeholder') || '请选择', allowClear: true, dropdownParent: $select.closest('.modal') }); }); // 调用原始afterRender(如果存在) if (config.afterRender) { config.afterRender(config, container); } } }; this.modalRegistry[name] = enhancedConfig; } // 注册实体表单 registerEntityForm(entityType, fields) { this.registerModalType(entityType, { template: `<div class="modal fade" id="${entityType}Modal" tabindex="-1"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title" id="${entityType}Title"></h5> <button type="button" class="btn-close" data-bs-dismiss="modal"></button> </div> <div class="modal-body" id="${entityType}Body"></div> <div class="modal-footer"> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button> <button type="button" class="btn btn-primary" id="${entityType}Submit">保存</button> </div> </div> </div> </div>`, render: (config, container) => { container.find(`#${entityType}Title`).text(config.title); let formHTML = ''; // 确保fields是数组 const fields = Array.isArray(config.fields) ? config.fields : []; fields.forEach(fieldConfig => { const fieldType = this.fieldRegistry[fieldConfig.type]; if (fieldType && fieldType.render) { formHTML += fieldType.render(fieldConfig); } }); container.find(`#${entityType}Body`).html(formHTML); }, getFormData: (container) => { const formData = {}; container.find('input, select, textarea').each(function() { const $el = $(this); const name = $el.attr('name'); if (!name) return; if ($el.attr('type') === 'checkbox') { formData[name] = $el.is(':checked'); } else { formData[name] = $el.val(); } }); return formData; }, bindEvents: (config, container, onSubmit) => { container.find(`#${entityType}Submit`).off('click').on('click', () => { const formData = this.getFormData(container); onSubmit(formData); }); } }); } // 打开模态框 open(modalType, config, callbacks = {}) { // 如果当前有激活的弹窗,先隐藏它 if (this.nestedModalStack.length > 0) { const currentModal = this.nestedModalStack[this.nestedModalStack.length - 1]; currentModal.hide(); } // 检查模态类型是否注册 if (!this.modalRegistry[modalType]) { console.error(`未注册的模态类型: ${modalType}`); return null; } const modalConfig = this.modalRegistry[modalType]; const modalId = `${modalType}Modal`; let $modal = $(`#${modalId}`); // 如果模态框不存在则创建 if ($modal.length === 0) { $modal = $(modalConfig.template); $('body').append($modal); } // 清除之前的模态框实例 if ($modal.data('bs.modal')) { $modal.data('bs.modal').dispose(); } // 渲染模态内容 modalConfig.render(config, $modal); // 绑定事件 if (modalConfig.bindEvents) { modalConfig.bindEvents(config, $modal, callbacks.onSubmit || (() => {})); } // 创建模态实例 const modalInstance = new bootstrap.Modal($modal[0]); // 执行afterRender回调 if (modalConfig.afterRender) { modalConfig.afterRender(config, $modal); } // 初始化Select2 $modal.find('.select2-field').each(function() { const $select = $(this); $select.select2({ theme: 'bootstrap', placeholder: $select.attr('placeholder') || '请选择', allowClear: true, dropdownParent: $select.closest('.modal') }); }); // 显示弹窗 modalInstance.show(); // 将新弹窗加入栈 const modalObj = { hide: () => modalInstance.hide(), show: () => modalInstance.show(), getElement: () => $modal[0], config }; this.nestedModalStack.push(modalObj); // 在弹窗打开后绑定新建按钮事件 Swal.getPopup().querySelectorAll('.add-entity-btn')?.forEach(btn => { btn.addEventListener('click', async () => { const entityType = btn.dataset.entityType; const targetField = btn.dataset.targetField; if (callbacks.onAddEntity) { const newItem = await callbacks.onAddEntity(entityType, targetField); if (newItem) { updateSelectField(Swal.getPopup(), targetField, newItem); } } }); }); // 返回包含关闭方法的实例对象 return modalObj; } // 关闭当前弹窗并显示上一个 closeCurrentModal() { if (this.nestedModalStack.length > 0) { const currentModal = this.nestedModalStack.pop(); currentModal.hide(); // 如果有上一个弹窗,显示它 if (this.nestedModalStack.length > 0) { const prevModal = this.nestedModalStack[this.nestedModalStack.length - 1]; prevModal.show(); } } } // 获取表单数据 getFormData(container) { const formData = {}; container.find('input, select, textarea').each(function() { const $el = $(this); const name = $el.attr('name'); if (!name) return; if ($el.attr('type') === 'checkbox') { formData[name] = $el.is(':checked'); } else if ($el.attr('type') === 'radio') { if ($el.is(':checked')) { formData[name] = $el.val(); } } else { formData[name] = $el.val(); } }); return formData; } /** * 设置多个按钮的事件处理 * @param {Object} config - 配置对象 * @param {Function} onAddEntity - 添加实体的回调函数 */ setupMultiButtonHandlers(config, onAddEntity) { const popup = Swal.getPopup(); // 确保弹窗存在 if (!popup) { console.warn('弹窗不存在,无法绑定事件'); return; } // 移除旧的事件监听器(防止重复绑定) this.cleanupMultiButtonHandlers(); // 绑定所有添加按钮事件 this.addEntityButtons = Array.from(popup.querySelectorAll('.add-entity-btn')); this.addEntityButtons.forEach(btn => { const handler = async () => { const targetField = btn.dataset.targetField; const entityType = btn.dataset.entityType; if (onAddEntity) { const newItem = await onAddEntity(entityType, targetField); if (newItem) { this.updateSelectField(popup, targetField, newItem); } } }; btn.addEventListener('click', handler); // 保存处理器以便后续清理 this.buttonHandlers.set(btn, handler); }); } /** * 清理按钮事件处理器 */ cleanupMultiButtonHandlers() { if (this.addEntityButtons) { this.addEntityButtons.forEach(btn => { const handler = this.buttonHandlers.get(btn); if (handler) { btn.removeEventListener('click', handler); this.buttonHandlers.delete(btn); } }); this.addEntityButtons = null; } } /** * 更新下拉框选项 * @param {HTMLElement} popup - 弹窗元素 * @param {string} fieldName - 字段名 * @param {Object} newItem - 新选项 {value, label} */ updateSelectField(popup, fieldName, newItem) { // 确保弹窗仍然存在 if (!popup || !popup.isConnected) { console.warn('弹窗已关闭,无法更新字段'); return; } const select = popup.querySelector(`select[name="${fieldName}"]`); if (select) { // 添加新选项 const option = document.createElement('option'); option.value = newItem.value; option.textContent = newItem.label; option.selected = true; select.appendChild(option); // 如果使用Select2,刷新组件 if ($(select).hasClass('select2-hidden-accessible')) { $(select).trigger('change'); } } } } export default SweetalertSystem;
最新发布
07-04
<think>我们遇到了两个主要问题: 1.在SweetalertSystem2.js:461行,尝试在一个null对象上调用querySelectorAll。2.在select2.min.js:2行,尝试调用一个未定义的destroy方法。 从错误堆栈来看,这两个问题都发生在打开模态框(showDingdanModal)的过程中。###问题分析1. **DOM访问时机问题**:-在SweetalertSystem.open方法中,我们尝试在模态框完全渲染之前访问DOM元素(使用querySelectorAll),导致错误。 2. **Select2初始化问题**:-在初始化Select2时,尝试销毁一个未初始化的Select2实例(或者已经销毁的实例),导致错误。 ###解决方案####1.修复DOM访问时机问题我们需要确保在DOM完全渲染后再执行DOM操作。可以使用MutationObserver来监听DOM变化,当目标元素出现后再执行操作。 修改SweetalertSystem.open方法:```javascriptopen: function(options){// ...其他代码 ...//使用MutationObserver确保DOM渲染完成const observer= newMutationObserver((mutations)=> {for(constmutation ofmutations){if (mutation.addedNodes.length) {//检查目标元素是否已渲染const targetElement =document.querySelector(options.targetSelector);if(targetElement){observer.disconnect();//安全执行后续操作if (options.afterRender) {try{options.afterRender();} catch(e) {console.error('afterRendererror:',e);}}break;}}}});//观察整个文档observer.observe(document.body, {childList: true, subtree: true});//设置超时回退consttimeout= setTimeout(() =>{observer.disconnect();if(options.afterRender){try {options.afterRender();}catch (e){console.error('afterRender errorafter timeout:', e);}}},1000); //1秒超时//...其他代码... }``` ####2.修复Select2初始化问题在初始化Select2之前,确保没有残留的Select2实例。同时,捕获可能的异常以避免整个脚本崩溃。修改Select2初始化代码:```javascriptfunctionsafeInitSelect2(selector,options){try {//检查是否已经初始化const$select= $(selector);if($select.data('select2')) {$select.select2('destroy');}//初始化$select.select2(options);returntrue;}catch (error){console.error('Select2 initializationfailed:',error);returnfalse;}} //在需要初始化Select2的地方调用safeInitSelect2('#dingdanSelect',{theme:'bootstrap',placeholder: '请选择',allowClear: true});``` ###完整修复后的SweetalertSystem.open方法```javascriptopen: function(options){//创建弹窗容器等(原有代码)//使用MutationObserver确保DOM渲染完成constobserver =new MutationObserver((mutations) =>{for (const mutationof mutations) {if(mutation.addedNodes.length){//检查弹窗容器是否已经渲染constcontainer =document.querySelector('.swal2-container');if(container) {observer.disconnect();//安全执行后续操作if (options.afterRender) {try{options.afterRender();} catch(e) {console.error('afterRendererror:',e);}}break;}}}});observer.observe(document.body, {childList: true, subtree: true});//超时回退const timeout= setTimeout(() =>{observer.disconnect();if(options.afterRender){try {options.afterRender();}catch (e){console.error('afterRender errorafter timeout:', e);}}},2000); //2秒超时//原有创建弹窗的代码//...//返回弹窗实例return popupInstance; }``` ###在tianjia3.js中修复showDingdanModal函数在showDingdanModal函数中,确保在打开模态框时使用修复后的方法,并安全初始化Select2。```javascriptasyncfunction showDingdanModal() {try{//确保数据已加载if(!dataManager.data.dingdans){await dataManager.fetchAll();}//打开订单模态框SweetalertSystem.open({title: '选择或创建订单',html: generateDingdanModalHTML(),targetSelector: '.dingdan-modal-content', //指定要观察的目标元素afterRender:function(){//安全初始化Select2safeInitSelect2('#dingdanSelect', {theme: 'bootstrap',placeholder:'请选择订单',allowClear: true});//绑定事件document.getElementById('createDingdanBtn').addEventListener('click',createDingdan);}});}catch (error){console.error('打开订单模态框失败:', error);Swal.fire('错误', '无法打开订单模态框','error');} }``` ###注意事项1.**错误边界**:在所有可能出错的地方添加try-catch,避免整个应用崩溃。2.**资源清理**:在模态框关闭时,销毁Select2实例并移除事件监听器,防止内存泄漏。 3. **异步处理**:确保数据加载完成后再初始化依赖数据的UI组件。###相关问题§§相关问题§§:1.如何使用MutationObserver确保DOM操作在元素渲染后执行?2.如何安全地初始化和销毁第三方UI库(如Select2)以避免内存泄漏?3.在单页应用中,如何管理模态框的嵌套和资源清理? 4.如何处理第三方库的异常以避免整个应用崩溃?</think>### 问题分析与解决方案 您提供的代码中存在两个主要问题: 1. DOM 渲染时机问题导致的 `querySelectorAll` 错误 2. Select2 初始化/销毁过程中的错误 #### 解决方案 ```javascript // === 核心方法 === async openForm(config) { // ... 其他代码 ... return new Promise((resolve) => { Swal.fire({ // ... 配置 ... didOpen: () => { // 使用 MutationObserver 确保 DOM 完全渲染 const observer = new MutationObserver(() => { if (Swal.getPopup().querySelectorAll('.add-entity-btn').length > 0) { observer.disconnect(); this.setupMultiButtonHandlers(config, onAddEntity); } }); observer.observe(Swal.getPopup(), { childList: true, subtree: true }); }, willClose: () => { this.cleanupMultiButtonHandlers(); this.destroyAllSelect2(); // 确保清理所有 Select2 实例 } }); }); } // 安全初始化 Select2 safeInitSelect2($select, options) { try { // 检查是否已初始化 if ($select.data('select2')) { this.safeDestroySelect2($select); } // 设置 dropdownParent 确保在模态框内正确显示 const dropdownParent = options.dropdownParent || Swal.getPopup() || document.body; $select.select2({ ...options, dropdownParent }); $select.addClass('select2-initialized'); return true; } catch (error) { console.error('Select2 initialization failed:', error); return false; } } // 安全销毁 Select2 safeDestroySelect2($select) { try { if ($select.data('select2')) { $select.select2('destroy'); } $select.removeClass('select2-initialized'); return true; } catch (error) { console.error('Select2 destruction failed:', error); return false; } } // 清理所有 Select2 实例 destroyAllSelect2() { const popup = Swal.getPopup(); if (popup) { $(popup).find('.select2-initialized').each((index, element) => { this.safeDestroySelect2($(element)); }); } } // 打开模态框 open(modalType, config, callbacks = {}) { // ... 其他代码 ... // 使用延迟确保 DOM 完全渲染 setTimeout(() => { // 安全初始化 Select2 $modal.find('.select2-field').each((index, element) => { const $select = $(element); this.safeInitSelect2($select, { theme: 'bootstrap', placeholder: $select.attr('placeholder') || '请选择', allowClear: true, dropdownParent: $modal }); }); // 绑定新建按钮事件 $modal.find('.add-entity-btn').on('click', async () => { const entityType = $(this).data('entityType'); const targetField = $(this).data('targetField'); if (callbacks.onAddEntity) { const newItem = await callbacks.onAddEntity(entityType, targetField); if (newItem) { this.updateSelectField($modal[0], targetField, newItem); } } }); }, 50); } ``` ### 代码解释 1. **DOM 渲染时机问题解决方案**: - 使用 `MutationObserver` 监控 DOM 变化,确保元素完全渲染后再绑定事件 - 在 `didOpen` 回调中设置观察器,当检测到按钮元素存在时再绑定事件 2. **Select2 初始化/销毁问题解决方案**: - 添加 `safeInitSelect2` 和 `safeDestroySelect2` 方法,包含错误处理和状态检查 - 在模态框关闭时 (`willClose`) 自动清理所有 Select2 实例 - 添加 `select2-initialized` 类标记已初始化的元素,避免重复初始化 3. **事件绑定改进**: - 使用 jQuery 的 `on()` 方法替代原生 `addEventListener`,更易管理 - 在模态框关闭时自动解绑事件,防止内存泄漏 4. **错误处理增强**: - 所有关键操作添加 try-catch 块捕获异常 - 详细的错误日志输出,便于调试 ### 关键改进点 1. **DOM 渲染检测**: ```javascript const observer = new MutationObserver(() => { if (Swal.getPopup().querySelectorAll('.add-entity-btn').length > 0) { observer.disconnect(); this.setupMultiButtonHandlers(config, onAddEntity); } }); observer.observe(Swal.getPopup(), { childList: true, subtree: true }); ``` - 使用 MutationObserver 监听 DOM 变化 - 当检测到目标按钮存在时再执行绑定操作 2. **Select2 安全初始化**: ```javascript safeInitSelect2($select, options) { try { if ($select.data('select2')) { this.safeDestroySelect2($select); } $select.select2({ ...options, dropdownParent: options.dropdownParent || Swal.getPopup() || document.body }); $select.addClass('select2-initialized'); return true; } catch (error) { console.error('Select2 initialization failed:', error); return false; } } ``` - 初始化前检查是否已存在实例 - 设置正确的 `dropdownParent` 确保在模态框内显示 - 添加类标记避免重复初始化 3. **资源清理**: ```javascript destroyAllSelect2() { const popup = Swal.getPopup(); if (popup) { $(popup).find('.select2-initialized').each((index, element) => { this.safeDestroySelect2($(element)); }); } } ``` - 在模态框关闭时自动清理所有 Select2 实例 - 防止内存泄漏和残留实例导致的错误 ### 相关问题
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值