jQuery 进阶应用:5 个高频复杂场景与实战方案

在实际开发中,jQuery 进阶应用的核心是 “解决复杂业务需求”—— 如动态表单、可编辑表格、数据可视化交互等场景。本文精选企业项目中最常用的 5 个复杂场景,每个场景都从 “业务痛点→技术选型→完整实现” 展开,提供可直接复用的代码方案,同时讲解背后的设计思路,帮你掌握 jQuery 在进阶开发中的实用技巧。

一、场景 1:复杂动态表单(后台管理系统高频需求)

业务痛点:基础表单仅支持固定字段,而后台管理系统中,常需 “动态添加 / 删除字段”(如多联系人信息、多商品规格),且字段间存在联动校验(如 “数量” 变化时自动计算 “小计金额”),提交时需统一收集所有动态字段数据。

需求拆解

实现 “动态添加” 按钮,点击新增一组 “商品规格” 字段(规格名称、单价、数量、小计);

实现 “删除” 按钮,点击删除当前字段组(至少保留 1 组);

联动校验:数量输入时自动计算小计(单价 × 数量),单价为空或非数字时提示错误;

提交表单时,收集所有动态字段数据,格式化为数组提交(如[{name: "颜色", price: 50, quantity: 2, total: 100}, ...])。

技术方案

用jQuery.template()或 HTML 字符串模板定义字段组结构,避免重复拼接 DOM;

事件委托绑定 “添加 / 删除” 按钮事件,处理动态生成的字段;

封装calculateTotal()函数,监听数量 / 单价输入事件,实时计算小计;

封装collectFormData()函数,提交前遍历所有字段组,收集并格式化数据。

完整代码

<!DOCTYPE html>

<html lang="zh-CN">

<head>

  <meta charset="UTF-8">

  <title>jQuery进阶:动态商品规格表单</title>

  <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.7.1/jquery.min.js"></script>

  <style>

    .form-container { max-width: 800px; margin: 40px auto; padding: 0 20px; }

    .form-group { margin-bottom: 24px; }

    .field-group {

      display: flex; gap: 16px; align-items: center;

      padding: 16px; border: 1px solid #e5e7eb; border-radius: 6px; margin-bottom: 12px;

    }

    .field-group .form-control {

      flex: 1; padding: 8px 12px; border: 1px solid #d1d5db; border-radius: 4px;

    }

    .field-group .form-control.error { border-color: #ef4444; }

    .error-tip { color: #ef4444; font-size: 12px; margin-top: 4px; height: 16px; }

    .btn {

      padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer;

      font-size: 14px; transition: background 0.2s;

    }

    .btn-primary { background: #3b82f6; color: white; }

    .btn-danger { background: #ef4444; color: white; }

    .btn-add { margin-bottom: 12px; }

  </style>

</head>

<body>

  <div class="form-container">

    <h3>商品规格配置(动态表单)</h3>

    <button class="btn btn-primary btn-add" id="addFieldGroup">+ 添加规格组</button>


    <!-- 动态字段组容器 -->

    <div id="fieldGroupsContainer">

      <!-- 初始字段组 -->

      <div class="field-group">

        <input type="text" class="form-control spec-name" placeholder="规格名称(如颜色/尺寸)" required>

        <input type="number" class="form-control spec-price" placeholder="单价(元)" min="0" step="0.01" required>

        <input type="number" class="form-control spec-quantity" placeholder="数量" min="1" value="1" required>

< href="zhiq.zhaopin.com/moment/82695614  ">

< href="zhiq.zhaopin.com/moment/82695618  ">

< href="zhiq.zhaopin.com/moment/82695904  ">

< href="zhiq.zhaopin.com/moment/82695933  ">

< href="zhiq.zhaopin.com/moment/82695937  ">

< href="zhiq.zhaopin.com/moment/82695941  ">

< href="zhiq.zhaopin.com/moment/82695944  ">

< href="zhiq.zhaopin.com/moment/82695947  ">

< href="zhiq.zhaopin.com/moment/82695949  ">

< href="zhiq.zhaopin.com/moment/82695953  ">

        <input type="number" class="form-control spec-total" placeholder="小计(元)" readonly>

        <button class="btn btn-danger delete-field-group">删除</button>

        <div class="error-tip"></div>

      </div>

    </div>

    <button class="btn btn-primary" id="submitForm">提交规格数据</button>

    <pre id="submitResult" style="margin-top: 20px; padding: 16px; background: #f9fafb; border-radius: 6px; display: none;"></pre>

  </div>

  <script>

    $(function() {

      // 1. 字段组模板(复用结构,避免重复拼接)

      const fieldGroupTemplate = `

        <div class="field-group">

          <input type="text" class="form-control spec-name" placeholder="规格名称(如颜色/尺寸)" required>

          <input type="number" class="form-control spec-price" placeholder="单价(元)" min="0" step="0.01" required>

          <input type="number" class="form-control spec-quantity" placeholder="数量" min="1" value="1" required>

          <input type="number" class="form-control spec-total" placeholder="小计(元)" readonly>

          <button class="btn btn-danger delete-field-group">删除</button>

          <div class="error-tip"></div>

        </div>

      `;

      // 2. 获取DOM元素

      const $addBtn = $('#addFieldGroup');

      const $container = $('#fieldGroupsContainer');

      const $submitBtn = $('#submitForm');

      const $submitResult = $('#submitResult');

      // 3. 动态添加字段组

      $addBtn.on('click', function() {

        const $newFieldGroup = $(fieldGroupTemplate);

        $container.append($newFieldGroup);

        // 给新字段组绑定事件(数量/单价输入时计算小计)

        bindFieldEvents($newFieldGroup);

      });

      // 4. 绑定字段事件(计算小计+校验)

      function bindFieldEvents($fieldGroup) {

        // 监听单价/数量输入,计算小计

        const $price = $fieldGroup.find('.spec-price');

        const $quantity = $fieldGroup.find('.spec-quantity');

        const $total = $fieldGroup.find('.spec-total');

        const $errorTip = $fieldGroup.find('.error-tip');

        function calculateTotal() {

          const price = parseFloat($price.val()) || 0;

          const quantity = parseInt($quantity.val()) || 0;

          const total = (price * quantity).toFixed(2);

          $total.val(total);

          // 校验单价(非负数+数字)

          if (price < 0 || isNaN(price)) {

            $price.addClass('error');

            $errorTip.text('单价需为非负数字');

          } else {

            $price.removeClass('error');

            $errorTip.text('');

          }

        }

        $price.on('input', calculateTotal);

        $quantity.on('input', calculateTotal);

        // 初始化计算一次小计

        calculateTotal();

      }

      // 5. 事件委托:删除字段组(处理动态生成的删除按钮)

      $container.on('click', '.delete-field-group', function() {

        const $fieldGroup = $(this).closest('.field-group');

        // 至少保留1组字段

        if ($container.find('.field-group').length > 1) {

          $fieldGroup.remove();

        } else {

          alert('至少保留1组规格数据');

        }

      });

      // 6. 收集表单数据(格式化提交格式)

      function collectFormData() {

        const formData = [];

        let isValid = true;

        $container.find('.field-group').each(function() {

          const $group = $(this);

          const name = $group.find('.spec-name').val().trim();

          const price = parseFloat($group.find('.spec-price').val()) || 0;

          const quantity = parseInt($group.find('.spec-quantity').val()) || 0;

          const total = parseFloat($group.find('.spec-total').val()) || 0;

          const $errorTip = $group.find('.error-tip');

          // 校验必填字段

          if (!name) {

            $group.find('.spec-name').addClass('error');

            $errorTip.text('规格名称不能为空');

            isValid = false;

            return; // 跳过后续校验,继续下一组

          } else {

            $group.find('.spec-name').removeClass('error');

            $errorTip.text('');

          }

          // 校验单价和数量

          if (price <= 0 || isNaN(price)) {

            $group.find('.spec-price').addClass('error');

            $errorTip.text('单价需大于0');

            isValid = false;

            return;

          }

          if (quantity < 1 || isNaN(quantity)) {

            $group.find('.spec-quantity').addClass('error');

            $errorTip.text('数量需大于0');

            isValid = false;

            return;

          }

          // 校验小计一致性(防止手动修改readonly字段)

          const calculatedTotal = (price * quantity).toFixed(2);

          if (total.toFixed(2) !== calculatedTotal) {

            $group.find('.spec-total').addClass('error');

            $errorTip.text('小计计算异常,请重新输入单价/数量');

            isValid = false;

            return;

          }

          // 收集有效数据

          formData.push({

            specName: name,

            price: price,

            quantity: quantity,

            total: parseFloat(calculatedTotal)

          });

        });

        return isValid ? formData : null;

      }

      // 7. 提交表单(模拟接口请求)

      $submitBtn.on('click', function() {

        const formData = collectFormData();

        if (!formData) {

          return; // 校验失败,不提交

        }

        // 模拟接口提交(实际项目替换为$.ajax)

        $submitResult.show().text(`提交成功!数据格式:\n${JSON.stringify(formData, null, 2)}`);

        // (可选)提交后重置表单

        // $container.find('.field-group').not(':first').remove();

        // $container.find('.field-group:first').find('input').val('');

        // $container.find('.field-group:first').find('.spec-quantity').val(1);

        // bindFieldEvents($container.find('.field-group:first'));

      });

      // 8. 初始化绑定初始字段组事件

      bindFieldEvents($container.find('.field-group:first'));

    });

  </script>

</body>

</html>

核心要点

模板复用:用fieldGroupTemplate定义字段组结构,避免重复编写 HTML,提升维护性;

事件委托:删除按钮用事件委托绑定,无需给每个动态生成的按钮单独绑定事件;

数据校验:提交前全量校验,包括必填项、数据合法性、计算一致性,确保提交数据正确;

性能优化:仅监听当前字段组的输入事件,避免全局事件导致的性能损耗。

二、场景 2:基于 jQuery 的数据可视化交互(数据看板场景)

业务痛点:后台数据看板需展示 “多维度数据图表”(如折线图 + 柱状图组合),且支持 “图表联动”(如点击折线图某点,柱状图显示该点的明细数据)、“时间范围筛选”(如切换今日 / 本周 / 本月数据),传统静态图表无法满足交互需求。

需求拆解

用 Chart.js 结合 jQuery 实现 “折线图(日销售额趋势)+ 柱状图(各商品销售额占比)” 组合图表;

实现时间筛选下拉框,切换 “今日 / 本周 / 本月” 时,同步更新两个图表数据;

实现图表联动:点击折线图的某日期点,柱状图切换为该日期的 “各商品销售额明细”;

图表加载时显示 loading 状态,数据加载失败时提示错误。

技术方案

引入 Chart.js(轻量级图表库),用 jQuery 初始化图表实例;

封装fetchChartData(timeRange)函数,根据时间范围请求数据(模拟接口);

封装updateLineChart(data)和updateBarChart(data)函数,统一更新图表数据;

绑定折线图click事件,实现图表联动,触发柱状图数据更新。

完整代码

<!DOCTYPE html>

<html lang="zh-CN">

<head>

  <meta charset="UTF-8">

  <title>jQuery进阶:数据可视化交互看板</title>

  <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.7.1/jquery.min.js"></script>

  <!-- 引入Chart.js -->

  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>

  <style>

    .dashboard-container { max-width: 1200px; margin: 40px auto; padding: 0 20px; }

    .chart-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }

    .time-filter { padding: 8px 12px; border: 1px solid #d1d5db; border-radius: 4px; }

    .chart-wrapper {

      display: flex; gap: 24px; margin-bottom: 30px;

      padding: 20px; border: 1px solid #e5e7eb; border-radius: 8px; background: white;

    }

    .chart-card { flex: 1; min-height: 300px; }

    .loading {

      position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);

      font-size: 16px; color: #6b7280;

    }

    .chart-container { position: relative; width: 100%; height: 100%; }

  </style>

</head>

<body>

  <div class="dashboard-container">

    <div class="chart-header">

      <h3>商品销售数据看板</h3>

      <select class="time-filter" id="timeFilter">

< href="zhiq.zhaopin.com/moment/82695955  ">

< href="zhiq.zhaopin.com/moment/82696368  ">

< href="zhiq.zhaopin.com/moment/82696397  ">

< href="zhiq.zhaopin.com/moment/82696412  ">

< href="zhiq.zhaopin.com/moment/82696423  ">

< href="zhiq.zhaopin.com/moment/82696438  ">

< href="zhiq.zhaopin.com/moment/82696450  ">

< href="zhiq.zhaopin.com/moment/82696465  ">

< href="zhiq.zhaopin.com/moment/82696471  ">

< href="zhiq.zhaopin.com/moment/82696533  "&g

        <option value="today">今日</option>

        <option value="week">本周</option>

        <option value="month">本月</option>

      </select>

    </div>

    <!-- 图表组合:折线图+柱状图 -->

    <div class="chart-wrapper">

      <!-- 折线图:销售额趋势 -->

      <div class="chart-card">

        <h4>日销售额趋势(元)</h4>

        <div class="chart-container">

          <canvas id="lineChart"></canvas>

          <div class="loading" id="lineChartLoading">加载中...</div>

        </div>

      </div>

      <!-- 柱状图:商品销售额占比 -->

      <div class="chart-card">

        <h4>各商品销售额(元)</h4>

        <div class="chart-container">

          <canvas id="barChart"></canvas>

          <div class="loading" id="barChartLoading">加载中...</div>

        </div>

      </div>

    </div>

  </div>

  <script>

    $(function() {

      // 1. 初始化图表实例(全局变量,便于后续更新)

      let lineChart = null; // 折线图实例

      let barChart = null; // 柱状图实例

      // 2. 获取DOM元素

      const $timeFilter = $('#timeFilter');

      const $lineChartLoading = $('#lineChartLoading');

      const $barChartLoading = $('#barChartLoading');

      // 3. 模拟接口:根据时间范围获取图表数据

      function fetchChartData(timeRange) {

        // 显示loading

        $lineChartLoading.show();

        $barChartLoading.show();

        // 模拟接口延迟(实际项目替换为$.ajax)

        return new Promise((resolve) => {

          setTimeout(() => {

            let lineData, barData;

            // 根据时间范围返回不同数据

            switch(timeRange) {

              case 'today':

                lineData = {

                  labels: ['09:00', '12:00', '15:00', '18:00', '21:00'],

                  datasets: [{

                    label: '今日销售额',

                    data: [1200, 2500, 1800, 3200, 2800],

                    borderColor: '#3b82f6',

                    backgroundColor: 'rgba(59, 130, 246, 0.1)',

                    fill: true,

                    tension: 0.4

                  }]

                };

                barData = {

                  labels: ['商品A', '商品B', '商品C', '商品D'],

                  datasets: [{

                    label: '今日销售额',

                    data: [1800, 2200, 1500, 1200],

                    backgroundColor: ['#3b82f6', '#10b981', '#f59e0b', '#ef4444']

                  }]

                };

                break;

              case 'week':

                lineData = {

                  labels: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],

                  datasets: [{

                    label: '本周销售额',

                    data: [8000, 12000, 9500, 15000, 11000, 18000, 13000],

                    borderColor: '#3b82f6',

                    backgroundColor: 'rgba(59, 130, 246, 0.1)',

                    fill: true,

                    tension: 0.4

                  }]

                };

                barData = {

                  labels: ['商品A', '商品B', '商品C', '商品D'],

                  datasets: [{

                    label: '本周销售额',

                    data: [15000, 22000, 18000, 12000],

                    backgroundColor: ['#3b82f6', '#10b981', '#f59e0b', '#ef4444']

                  }]

                };

                break;

              case 'month':

                lineData = {

                  labels: ['第1周', '第2周', '第3周', '第4周'],

                  datasets: [{

                    label: '本月销售额',

                    data: [45000, 52000, 68000, 58000],

                    borderColor: '#3b82f6',

                    backgroundColor: 'rgba(59, 130, 246, 0.1)',

                    fill: true,

                    tension: 0.4

                  }]

                };

                barData = {

                  labels: ['商品A', '商品B', '商品C', '商品D'],

                  datasets: [{

                    label: '本月销售额',

                    data: [65000, 82000, 75000, 58000],

                    backgroundColor: ['#3b82f6', '#10b981', '#f59e0b', '#ef4444']

                  }]

                };

                break;

            }

            // 隐藏loading

            $lineChartLoading.hide();

            $barChartLoading.hide();

            resolve({ lineData, barData });

          }, 800);

        });

      }

      // 4. 更新折线图

      function updateLineChart(data) {

        const ctx = document.getElementById('lineChart').getContext('2d');

        if (lineChart) {

          // 已存在实例,更新数据

          lineChart.data = data;

          lineChart.update();

        } else {

          // 初始化折线图

          lineChart = new Chart(ctx, {

            type: 'line',

            data: data,

            options: {

              responsive: true,

              maintainAspectRatio: false,

              interaction: {

                mode: 'index',

                intersect: false,

              },

              plugins: {

                tooltip: {

                  callbacks: {

                    label: function(context) {

                      return `销售额:${context.raw} 元`;

                    }

                  }

                }

              },

              scales: {

                y: {

                  beginAtZero: true,

                  title: {

                    display: true,

                    text: '销售额(元)'

                  }

                }

              }

            }

          });

          // 绑定折线图点击事件:实现图表联动

          lineChart.options.onClick = function(e, elements) {

            if (elements.length > 0) {

              const index = elements[0].index;

              const label = lineChart.data.labels[index];

              const value = lineChart.data.datasets[0].data[index];

              // 模拟“点击折线图点,更新柱状图为该点明细”

              updateBarChart({

                labels: ['商品A', '商品B', '商品C', '商品D'],

                datasets: [{

                  label: `${label}销售额`,

                  data: [

                    Math.round(value * 0.3),

                    Math.round(value * 0.25),

                    Math.round(value * 0.25),

                    Math.round(value * 0.2)

                  ],

                  backgroundColor: ['#3b82f6', '#10b981', '#f59e0b', '#ef4444']

                }]

              });

            }

          };

        }

      }

      // 5. 更新柱状图

      function updateBarChart(data) {

        const ctx = document.getElementById('barChart').getContext('2d');

        if (barData) {

          // 已存在实例,更新数据

          barChart.data = data;

          barChart.update();

        } else {

          // 初始化柱状图

          barChart = new Chart(ctx, {

            type: 'bar',

            data: data,

            options: {

              responsive: true,

              maintainAspectRatio: false,

              plugins: {

                tooltip: {

                  callbacks: {

                    label: function(context) {

                      return `销售额:${context.raw} 元`;

                    }

                  }

                }

              },

              scales: {

                y: {

                  beginAtZero: true,

                  title: {

                    display: true,

                    text: '销售额(元)'

                  }

                }

              }

            }

          });

        }

      }

      // 6. 初始化图表:加载默认时间范围(今日)

      async function initCharts() {

        const initialTimeRange = $timeFilter.val();

        const { lineData, barData } = await fetchChartData(initialTimeRange);

        updateLineChart(lineData);

        updateBarChart(barData);

      }

      // 7. 时间筛选下拉框变化:更新图表数据

      $timeFilter.on('change', async function() {

        const timeRange = $(this).val();

        const { lineData, barData } = await fetchChartData(timeRange);

        updateLineChart(lineData);

        updateBarChart(barData);

      });

      // 8. 页面加载完成后初始化图表

      initCharts();

    });

  </script>

</body>

</html>

核心要点

图表联动:通过lineChart.options.onClick绑定折线图点击事件,触发柱状图数据更新,实现多图表交互;

数据复用:封装fetchChartData统一获取数据,避免重复请求逻辑;

状态管理:加载状态统一控制,提升用户体验;

响应式适配:Chart.js 结合responsive: true和maintainAspectRatio: false,确保图表在不同屏幕尺寸下正常显示。

三、场景 3:高级可编辑表格(后台数据管理高频需求)

业务痛点:后台管理系统中,“数据表格” 常需支持 “行内编辑”(无需跳转页面,直接在表格中修改数据)、“批量删除”、“数据排序”、“分页加载”,传统静态表格无法满足高效数据操作需求。

需求拆解

实现行内编辑:点击表格单元格(除操作列),切换为输入框 / 下拉框进行编辑,失去焦点后保存数据;

实现批量删除:勾选多行数据,点击 “批量删除” 按钮删除选中行;

实现数据排序:点击表头(如 “金额”),切换升序 / 降序排序;

实现分页加载:模拟分页,点击 “上一页 / 下一页” 加载不同页数据。

技术方案

用jQuery动态生成表格 HTML,封装renderTable(data)函数统一渲染表格;

事件委托绑定 “单元格点击编辑”“批量删除”“表头排序” 事件;

封装toggleEditMode($cell)函数,实现单元格编辑模式切换;

用数组存储表格数据,排序和分页操作直接操作数据数组,再重新渲染表格。

完整代码

<!DOCTYPE html>

<html lang="zh-CN">

<head>

  <meta charset="UTF-8">

  <title>jQuery进阶:高级可编辑表格</title>

  <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.7.1/jquery.min.js"></script>

  <style>

    .table-container { max-width: 1000px; margin: 40px auto; padding: 0 20px; }

    .table-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }

    .btn {

      padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer;

      font-size: 14px; transition: background 0.2s;

    }

    .btn-danger { background: #ef4444; color: white; }

    .btn-primary { background: #3b82f6; color: white; }

    .data-table {

      width: 100%; border-collapse: collapse; margin-bottom: 16px;

      border: 1px solid #e5e7eb;

    }

    .data-table th, .data-table td {

      padding: 12px 16px; border: 1px solid #e5e7eb; text-align: left;

    }

    .data-table th {

      background: #f9fafb; font-weight: 500; cursor: pointer;

      user-select: none;

    }

    .data-table th.sortable::after {

      content: ' ▼'; font-size: 12px; color: #6b7280; margin-left: 4px;

    }

    .data-table th.sortable.asc::after {

      content: ' ▲';

    }

    .table-checkbox { width: 18px; height: 18px; cursor: pointer; }

    .edit-input, .edit-select {

      width: 100%; padding: 6px 8px; border: 1px solid #3b82f6; border-radius: 4px;

      box-sizing: border-box;

    }

    .pagination { display: flex; gap: 8px; align-items: center; justify-content: center; }

    .page-btn { padding: 4px 12px; border: 1px solid #e5e7eb; border-radius: 4px; cursor: pointer; }

    .page-btn:disabled { background: #f9fafb; color: #9ca3af; cursor: not-allowed; }

    .page-info { margin: 0 8px; }

    .selected-count { color: #ef4444; font-weight: 500; }

  </style>

</head>

<body>

  <div class="table-container">

    <div class="table-header">

      <button class="btn btn-danger" id="batchDeleteBtn">批量删除(选中:<span class="selected-count">0</span>)</button>

      <div>

        <input type="checkbox" id="selectAll" class="table-checkbox">

        <label for="selectAll">全选</label>

      </div>

    </div>

    <!-- 可编辑表格 -->

    <table class="data-table" id="editableTable">

      <thead>

        <tr>

          <th style="width: 50px;">选择</th>

          <th class="sortable" data-field="id">ID</th>

          <th class="sortable" data-field="name">商品名称</th>

          <th class="sortable" data-field="price">单价(元)</th>

          <th class="sortable" data-field="stock">库存</th>

          <th class="sortable" data-field="status">状态</th>

        </tr>

      </thead>

      <tbody id="tableBody">

        <!-- 表格内容动态生成 -->

      </tbody>

    </table>

    <!-- 分页控件 -->

    <div class="pagination">

      <button class="page-btn" id="prevPage" disabled>上一页</button>

      <span class="page-info">第 <span id="currentPage">1</span> 页 / 共 <span id="totalPages">3</span> 页</span>

      <button class="page-btn" id="nextPage">下一页</button>

    </div>

  </div>

  <script>

    $(function() {

      // 1. 模拟表格数据(实际项目从接口获取)

      const mockData = [

        { id: 1, name: '商品A', price: 199.99, stock: 100, status: 1 },

        { id: 2, name: '商品B', price: 299.50, stock: 50, status: 0 },

        { id: 3, name: '商品C', price: 89.90, stock: 200, status: 1 },

        { id: 4, name: '商品D', price: 399.00, stock: 30, status: 1 },

        { id: 5, name: '商品E', price: 129.99, stock: 80, status: 0 },

        { id: 6, name: '商品F', price: 499.99, stock: 150, status: 1 },

        { id: 7, name: '商品G', price: 79.90, stock: 0, status: 0 },

        { id: 8, name: '商品H', price: 249.50, stock: 60, status: 1 },

        { id: 9, name: '商品I', price: 329.00, stock: 90, status: 1 }

      ];

      // 2. 分页与排序配置

      let pageConfig = {

        currentPage: 1,

        pageSize: 3, // 每页显示3条

        totalPages: Math.ceil(mockData.length / 3),

        sortField: 'id', // 默认排序字段

        sortOrder: 'asc' // 默认升序(asc/desc)

      };

      // 3. 获取DOM元素

      const $tableBody = $('#tableBody');

      const $batchDeleteBtn = $('#batchDeleteBtn');

      const $selectAll = $('#selectAll');

      const $selectedCount = $('.selected-count');

      const $currentPage = $('#currentPage');

      const $totalPages = $('#totalPages');

      const $prevPage = $('#prevPage');

      const $nextPage = $('#nextPage');

      // 4. 渲染表格(核心函数:数据→DOM)

      function renderTable() {

        // 1. 排序数据

        const sortedData = [...mockData].sort((a, b) => {

          if (pageConfig.sortOrder === 'asc') {

            return a[pageConfig.sortField] - b[pageConfig.sortField];

          } else {

            return b[pageConfig.sortField] - a[pageConfig.sortField];

          }

        });

        // 2. 分页数据

        const startIndex = (pageConfig.currentPage - 1) * pageConfig.pageSize;

        const pageData = sortedData.slice(startIndex, startIndex + pageConfig.pageSize);

        // 3. 生成表格HTML

        let html = '';

        pageData.forEach(item => {

          // 状态文本:1=启用,0=禁用

          const statusText = item.status === 1 ? '启用' : '禁用';

          const statusClass = item.status === 1 ? 'color: #10b981' : 'color: #ef4444';

          html += `

            <tr data-id="${item.id}">

              <td><input type="checkbox" class="table-checkbox row-select" data-id="${item.id}"></td>

              <td data-field="id" data-value="${item.id}">${item.id}</td>

              <td data-field="name" data-value="${item.name}">${item.name}</td>

              <td data-field="price" data-value="${item.price}">${item.price.toFixed(2)}</td>

              <td data-field="stock" data-value="${item.stock}">${item.stock}</td>

              <td data-field="status" data-value="${item.status}" style="${statusClass}">${statusText}</td>

            </tr>

          `;

        });

        // 4. 更新表格内容

        $tableBody.html(html);

        // 5. 更新分页信息

        $currentPage.text(pageConfig.currentPage);

        $totalPages.text(pageConfig.totalPages);

        $prevPage.prop('disabled', pageConfig.currentPage === 1);

        $nextPage.prop('disabled', pageConfig.currentPage === pageConfig.totalPages);

        // 6. 更新全选状态

        updateSelectAllStatus();

      }

      // 5. 切换单元格编辑模式

      function toggleEditMode($cell) {

        const field = $cell.data('field');

        const currentValue = $cell.data('value');

        const $tr = $cell.closest('tr');

        const rowId = $tr.data('id');

        // 根据字段类型生成不同编辑控件

        let editControl = '';

        switch(field) {

          case 'name':

            // 文本输入框

            editControl = `<input type="text" class="edit-input" value="${currentValue}">`;

            break;

          case 'price':

            // 数字输入框(保留2位小数)

            editControl = `<input type="number" class="edit-input" value="${currentValue}" min="0" step="0.01">`;

            break;

          case 'stock':

            // 数字输入框(整数)

            editControl = `<input type="number" class="edit-input" value="${currentValue}" min="0" step="1">`;

            break;

          case 'status':

            // 下拉框

            editControl = `

              <select class="edit-select">

                <option value="1" ${currentValue === 1 ? 'selected' : ''}>启用</option>

                <option value="0" ${currentValue === 0 ? 'selected' : ''}>禁用</option>

              </select>

            `;

            break;

          default:

            // ID等不可编辑字段,直接返回

            return;

        }

        // 保存原始内容,切换为编辑控件

        const originalHtml = $cell.html();

        $cell.data('original-html', originalHtml);

        $cell.html(editControl);

        // 获取编辑控件,聚焦

        const $editControl = $cell.find('.edit-input, .edit-select');

        $editControl.focus();

        // 绑定失去焦点事件:保存编辑

        $editControl.on('blur', function() {

          const newValue = $(this).val();

          // 校验新值(根据字段类型)

          let isValid = true;

          let formattedValue = newValue;

          switch(field) {

            case 'price':

              formattedValue = parseFloat(newValue) || 0;

              if (formattedValue < 0) isValid = false;

              break;

            case 'stock':

              formattedValue = parseInt(newValue) || 0;

              if (formattedValue < 0) isValid = false;

              break;

            case 'status':

              formattedValue = parseInt(newValue);

              break;

          }

          if (!isValid) {

            alert(`请输入有效的${field === 'price' ? '单价' : field === 'stock' ? '库存' : '值'}`);

            // 恢复原始内容

            $cell.html($cell.data('original-html'));

            return;

          }

          // 保存数据到mockData

          const dataIndex = mockData.findIndex(item => item.id === rowId);

          if (dataIndex !== -1) {

            mockData[dataIndex][field] = formattedValue;

          }

          // 更新单元格显示(格式化显示)

          let displayValue = formattedValue;

          if (field === 'price') displayValue = formattedValue.toFixed(2);

          if (field === 'status') {

            displayValue = formattedValue === 1 ? '启用' : '禁用';

            const statusClass = formattedValue === 1 ? 'color: #10b981' : 'color: #ef4444';

            $cell.css('color', statusClass);

          }

          // 更新单元格数据和显示

          $cell.data('value', formattedValue);

          $cell.html(displayValue);

        });

        // 绑定回车键:触发失去焦点

        $editControl.on('keydown', function(e) {

          if (e.key === 'Enter') {

            $(this).blur();

          }

        });

      }

      // 6. 事件委托:单元格点击编辑(排除选择列和操作列)

      $tableBody.on('click', 'td:not(:first-child)', function() {

        const $cell = $(this);

        // 若已处于编辑模式,不重复触发

        if (!$cell.find('.edit-input, .edit-select').length) {

          toggleEditMode($cell);

        }

      });

      // 7. 全选/取消全选

      $selectAll.on('click', function() {

        const isChecked = $(this).prop('checked');

        $tableBody.find('.row-select').prop('checked', isChecked);

        updateSelectAllStatus();

      });

      // 8. 行选择:更新选中计数

      $tableBody.on('click', '.row-select', function() {

        updateSelectAllStatus();

      });

      // 9. 更新全选状态和选中计数

      function updateSelectAllStatus() {

        const $allRows = $tableBody.find('.row-select');

        const $checkedRows = $allRows.filter(':checked');

        const checkedCount = $checkedRows.length;

        // 更新选中计数

        $selectedCount.text(checkedCount);

        // 更新全选状态(所有行都选中时才勾选全选)

        $selectAll.prop('checked', checkedCount > 0 && checkedCount === $allRows.length);

      }

      // 10. 批量删除

      $batchDeleteBtn.on('click', function() {

        const $checkedRows = $tableBody.find('.row-select:checked');

        if ($checkedRows.length === 0) {

          alert('请选择要删除的行');

          return;

        }

        if (confirm(`确定要删除选中的 ${$checkedRows.length} 条数据吗?`)) {

          // 获取选中行的ID

          const checkedIds = $checkedRows.map((_, el) => parseInt($(el).data('id'))).get();

          // 从mockData中删除数据

          mockData = mockData.filter(item => !checkedIds.includes(item.id));

          // 更新分页总页数

          pageConfig.totalPages = Math.ceil(mockData.length / pageConfig.pageSize);

          // 若当前页无数据,跳转到上一页

          if (pageConfig.currentPage > pageConfig.totalPages && pageConfig.totalPages > 0) {

            pageConfig.currentPage = pageConfig.totalPages;

          }

          // 重新渲染表格

          renderTable();

        }

      });

      // 11. 表头排序:点击表头切换排序

      $('#editableTable thead').on('click', '.sortable', function() {

        const $th = $(this);

        const field = $th.data('field');

        // 若点击当前排序字段,切换排序方向;否则默认升序

        if (field === pageConfig.sortField) {

          pageConfig.sortOrder = pageConfig.sortOrder === 'asc' ? 'desc' : 'asc';

        } else {

          pageConfig.sortField = field;

          pageConfig.sortOrder = 'asc';

        }

        // 更新表头排序样式

        $('#editableTable thead .sortable').removeClass('asc');

        $th.addClass(pageConfig.sortOrder);

        // 重新渲染表格

        renderTable();

      });

      // 12. 分页控制:上一页/下一页

      $prevPage.on('click', function() {

        if (pageConfig.currentPage > 1) {

          pageConfig.currentPage--;

          renderTable();

        }

      });

      $nextPage.on('click', function() {

        if (pageConfig.currentPage < pageConfig.totalPages) {

          pageConfig.currentPage++;

          renderTable();

        }

      });

      // 13. 初始化渲染表格

      renderTable();

    });

  </script>

</body>

</html>

核心要点

行内编辑:通过toggleEditMode函数动态切换单元格为编辑控件,失去焦点后保存数据,避免页面跳转;

批量操作:用 “全选 + 行选择” 实现批量删除,通过updateSelectAllStatus统一管理选中状态;

数据驱动:表格渲染基于数据数组,排序和分页操作数据后重新渲染,确保数据与 DOM 同步;

用户体验:编辑时聚焦控件、支持回车键保存、删除前确认,提升操作流畅度。

四、场景 4:jQuery 与现代框架混合使用(老项目迁移场景)

业务痛点:企业中常存在 “老项目(jQuery)+ 新功能(Vue/React)” 的混合开发场景,需解决 “jQuery 与现代框架的 DOM 交互”“数据同步”“事件通信” 问题,避免重构整个老项目。

需求拆解

老项目(jQuery)页面中嵌入 Vue 组件(如 “商品选择器”);

jQuery 页面向 Vue 组件传递初始数据(如已选商品 ID);

Vue 组件向 jQuery 页面传递数据(如用户选择的商品信息);

jQuery 页面触发 Vue 组件的方法(如 “清空选择”),Vue 组件触发 jQuery 页面的方法(如 “选择完成”)。

技术方案

用Vue的mount选项将 Vue 组件挂载到 jQuery 管理的 DOM 节点上;

通过props向 Vue 组件传递初始数据,通过$emit实现 Vue 向 jQuery 的事件通信;

在 Vue 组件中通过$refs或document.querySelector操作 jQuery 生成的 DOM(谨慎使用);

在 jQuery 中通过 Vue 实例的methods调用 Vue 组件方法,实现跨框架方法调用。

完整代码

<!DOCTYPE html>

<html lang="zh-CN">

<head>

  <meta charset="UTF-8">

  <title>jQuery进阶:与Vue混合使用</title>

  <!-- 引入jQuery -->

  <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.7.1/jquery.min.js"></script>

  <!-- 引入Vue 2(适合老项目兼容性) -->

  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.14/dist/vue.js"></script>

  <style>

    .container { max-width: 1000px; margin: 40px auto; padding: 0 20px; }

    .old-system {

      padding: 20px; border: 1px solid #e5e7eb; border-radius: 8px;

      margin-bottom: 24px; background: #f9fafb;

    }

    .vue-component {

      padding: 20px; border: 1px solid #3b82f6; border-radius: 8px;

    }

    .btn {

      padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer;

      font-size: 14px; transition: background 0.2s;

      margin-right: 8px;

    }

    .btn-primary { background: #3b82f6; color: white; }

    .btn-success { background: #10b981; color: white; }

    .selected-items { margin-top: 16px; padding: 12px; background: white; border-radius: 4px; }

    .vue-selector { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }

    .vue-selector-item {

      padding: 8px 12px; border: 1px solid #e5e7eb; border-radius: 4px;

      cursor: pointer;

    }

    .vue-selector-item.selected {

      border-color: #3b82f6; background: rgba(59, 130, 246, 0.1);

      color: #3b82f6;

    }

  </style>

</head>

<body>

  <div class="container">

    <!-- 老项目(jQuery管理区域) -->

    <div class="old-system">

      <h3>老系统(jQuery)区域</h3>

      <p>已选商品ID:<span id="selectedIds">1,3</span></p>

      <button class="btn btn-primary" id="passToVueBtn">向Vue组件传递初始数据</button>

      <button class="btn btn-success" id="callVueMethodBtn">调用Vue组件方法(清空选择)</button>

      <div class="selected-items">

        <h4>Vue组件返回的选择结果:</h4>

        <pre id="vueResult">未选择任何商品</pre>

      </div>

    </div>

    <!-- Vue组件挂载点(由jQuery管理DOM,Vue负责内部逻辑) -->

    <div class="vue-component">

      <h3>新功能(Vue组件):商品选择器</h3>

      <div id="productSelector"></div>

    </div>

  </div>

  <script>

    $(function() {

      // 1. 老系统(jQuery)逻辑

      const $selectedIds = $('#selectedIds');

      const $vueResult = $('#vueResult');

      const $passToVueBtn = $('#passToVueBtn');

      const $callVueMethodBtn = $('#callVueMethodBtn');

      // 2. Vue组件定义(新功能逻辑)

      const ProductSelector = Vue.extend({

        props: {

          // 接收jQuery传递的初始已选商品ID(数组)

          initialSelectedIds: {

            type: Array,

            default: () => []

          }

        },

        data() {

          return {

            // 商品列表(模拟数据)

            products: [

              { id: 1, name: '商品A', price: 199.99 },

              { id: 2, name: '商品B', price: 299.50 },

              { id: 3, name: '商品C', price: 89.90 },

              { id: 4, name: '商品D', price: 399.00 },

              { id: 5, name: '商品E', price: 129.99 }

            ],

            // 当前选中的商品ID

            selectedIds: []

          };

        },

        template: `

          <div>

            <div class="vue-selector">

              <div

                v-for="product in products"

                :key="product.id"

                class="vue-selector-item"

                :class="{ selected: selectedIds.includes(product.id) }"

                @click="toggleSelect(product.id)"

              >

                {{ product.name }}(¥{{ product.price.toFixed(2) }})

              </div>

            </div>

            <button

              class="btn btn-primary"

              style="margin-top: 16px;"

              @click="emitResult"

            >

              确认选择并返回给jQuery

            </button>

          </div>

        `,

        methods: {

          // 切换商品选择状态

          toggleSelect(productId) {

            if (this.selectedIds.includes(productId)) {

              this.selectedIds = this.selectedIds.filter(id => id !== productId);

            } else {

              this.selectedIds.push(productId);

            }

          },

          // 清空选择(供jQuery调用)

          clearSelection() {

            this.selectedIds = [];

            // 清空后同步通知jQuery

            this.emitResult();

          },

          // 向jQuery传递选择结果(通过事件)

          emitResult() {

            const selectedProducts = this.products.filter(p => this.selectedIds.includes(p.id));

            // 通过window全局事件向jQuery传递数据(避免直接操作DOM)

            $(window).trigger('vue:selectionChange', [selectedProducts]);

          }

        },

        watch: {

          // 监听初始数据变化,更新选中状态

          initialSelectedIds: {

            immediate: true, // 初始加载时执行

            handler(newVal) {

              this.selectedIds = [...newVal];

              // 初始数据更新后,同步通知jQuery

              this.emitResult();

            }

          }

        }

      });

      // 3. 挂载Vue组件到jQuery管理的DOM节点

      const vueInstance = new ProductSelector({

        propsData: {

          // 初始传递老系统中的已选商品ID(从DOM中解析)

          initialSelectedIds: $selectedIds.text().split(',').map(Number)

        }

      }).$mount('#productSelector');

      // 4. jQuery监听Vue组件的事件(接收Vue传递的数据)

      $(window).on('vue:selectionChange', function(e, selectedProducts) {

        if (selectedProducts.length === 0) {

          $vueResult.text('未选择任何商品');

        } else {

          const resultText = `

选择商品数量:${selectedProducts.length}

商品详情:

${JSON.stringify(selectedProducts, null, 2)}

          `;

          $vueResult.text(resultText);

        }

      });

      // 5. jQuery向Vue组件传递新的初始数据

      $passToVueBtn.on('click', function() {

        // 模拟新的初始数据(如从老系统接口获取)

        const newInitialIds = [2, 4, 5];

        $selectedIds.text(newInitialIds.join(','));

        // 更新Vue组件的props数据(触发watch更新)

        vueInstance.initialSelectedIds = newInitialIds;

      });

      // 6. jQuery调用Vue组件的方法(清空选择)

      $callVueMethodBtn.on('click', function() {

        // 直接调用Vue实例的方法

        vueInstance.clearSelection();

        alert('已调用Vue组件的清空方法');

      });

    });

  </script>

</body>

</html>

核心要点

跨框架通信:通过window全局事件(vue:selectionChange)实现 Vue 向 jQuery 的数据传递,避免直接 DOM 操作;

数据同步:Vue 组件通过props接收 jQuery 传递的初始数据,watch监听props变化并更新内部状态;

方法调用:jQuery 通过 Vue 实例直接调用 Vue 组件的methods(如vueInstance.clearSelection()),实现跨框架方法触发;

渐进式迁移:无需重构整个老项目,只需在新功能区域嵌入 Vue 组件,降低迁移成本。

五、场景 5:jQuery 实现断点续传文件上传(大文件上传场景)

业务痛点:大文件(如 100MB+)上传时,若网络中断需重新上传,用户体验差。需实现 “断点续传”—— 将文件分片上传,记录已上传分片,网络恢复后继续上传未完成分片。

需求拆解

将大文件按固定大小(如 5MB)分片,计算每个分片的 MD5(用于断点标识);

上传前请求后端,查询已上传的分片,仅上传未完成分片;

所有分片上传完成后,请求后端合并分片为完整文件;

显示上传进度(整体进度 + 当前分片进度),支持暂停 / 继续上传。

技术方案

用FileReader和SparkMD5(轻量级 MD5 库)计算文件 MD5 和分片 MD5;

用 jQuery 的$.ajax发送分片上传请求,支持暂停(xhr.abort())和继续;

用localStorage记录已上传分片信息,实现页面刷新后仍能恢复上传;

封装FileUploader类,统一管理分片、上传、暂停、继续逻辑。

完整代码

<!DOCTYPE html>

<html lang="zh-CN">

<head>

  <meta charset="UTF-8">

  <title>jQuery进阶:断点续传文件上传</title>

  <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.7.1/jquery.min.js"></script>

  <!-- 引入SparkMD5(计算文件MD5) -->

  <script src="https://cdn.jsdelivr.net/npm/spark-md5@3.0.2/spark-md5.min.js"></script>

  <style>

    .upload-container { max-width: 800px; margin: 40px auto; padding: 0 20px; }

    .file-input { margin-bottom: 20px; }

    .upload-card {

      padding: 24px; border: 1px solid #e5e7eb; border-radius: 8px;

      background: #f9fafb;

    }

    .upload-info { margin-bottom: 16px; }

    .progress-container {

      width: 100%; height: 8px; background: #e5e7eb; border-radius: 4px;

      margin-bottom: 16px; overflow: hidden;

    }

    .progress-bar {

      height: 100%; background: #3b82f6; width: 0%; transition: width 0.3s;

    }

    .progress-text { display: flex; justify-content: space-between; margin-bottom: 24px; }

    .btn {

      padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer;

      font-size: 14px; transition: background 0.2s;

      margin-right: 12px;

    }

    .btn-primary { background: #3b82f6; color: white; }

    .btn-danger { background: #ef4444; color: white; }

    .btn:disabled { background: #9ca3af; cursor: not-allowed; }

    .upload-log {

      margin-top: 24px; padding: 16px; background: white; border-radius: 4px;

      height: 150px; overflow-y: auto; font-size: 12px; color: #6b7280;

    }

  </style>

</head>

<body>

  <div class="upload-container">

    <div class="upload-card">

      <h3>断点续传文件上传(支持大文件)</h3>

      <input type="file" class="file-input" id="fileInput" accept=".zip,.rar,.pdf,.mp4">


      <div class="upload-info">

        <p>选择文件:<span id="fileName">未选择文件</span></p>

        <p>文件大小:<span id="fileSize">0MB</span></p>

        <p>文件MD5:<span id="fileMd5">计算中...</span></p>

      </div>

      <div class="progress-container">

        <div class="progress-bar" id="progressBar"></div>

      </div>

      <div class="progress-text">

        <span>上传进度:<span id="progressPercent">0%</span></span>

        <span>状态:<span id="uploadStatus">等待选择文件</span></span>

      </div>

      <button class="btn btn-primary" id="uploadBtn" disabled>开始上传</button>

      <button class="btn btn-danger" id="pauseBtn" disabled>暂停上传</button>

      <div class="upload-log" id="uploadLog">

        <p>上传日志:</p>

      </div>

    </div>

  </div>

  <script>

    $(function() {

      // 1. 上传配置

      const UPLOAD_CONFIG = {

        chunkSize: 5 * 1024 * 1024, // 分片大小:5MB

        api: {

          check: '/api/upload/check', // 检查已上传分片

          upload: '/api/upload/chunk', // 上传分片

          merge: '/api/upload/merge' // 合并分片

        }

      };

      // 2. 获取DOM元素

      const $fileInput = $('#fileInput');

      const $fileName = $('#fileName');

      const $fileSize = $('#fileSize');

      const $fileMd5 = $('#fileMd5');

      const $progressBar = $('#progressBar');

      const $progressPercent = $('#progressPercent');

      const $uploadStatus = $('#uploadStatus');

      const $uploadBtn = $('#uploadBtn');

      const $pauseBtn = $('#pauseBtn');

      const $uploadLog = $('#uploadLog');

      // 3. 断点续传上传类(封装核心逻辑)

      class FileUploader {

        constructor() {

          this.file = null; // 当前选择的文件

          this.fileMd5 = ''; // 文件MD5(唯一标识)

          this.chunks = []; // 分片列表

          this.uploadedChunks = []; // 已上传分片索引

          this.currentChunkIndex = 0; // 当前正在上传的分片索引

          this.xhr = null; // 当前上传的XHR对象(用于暂停)

          this.isPaused = false; // 是否暂停

          this.totalProgress = 0; // 总上传进度(0-100)

        }

        // 选择文件:初始化文件信息

        selectFile(file) {

          if (!file) return;

          this.file = file;

          // 更新文件基本信息

          $fileName.text(file.name);

          $fileSize.text((file.size / (1024 * 1024)).toFixed(2) + 'MB');

          $uploadStatus.text('正在计算文件MD5...');

          $uploadLog.append(`<p>[${this.getCurrentTime()}] 已选择文件:${file.name}</p>`);

          // 计算文件MD5(用于断点标识)

          this.calculateFileMd5(file);

        }

        // 计算文件MD5(基于SparkMD5)

        calculateFileMd5(file) {

          const fileReader = new FileReader();

          const spark = new SparkMD5.ArrayBuffer();

          let chunkIndex = 0;

          let chunkSize = 2 * 1024 * 1024; // 计算MD5的分片大小:2MB

          // 分块读取文件计算MD5

          fileReader.onload = (e) => {

            spark.append(e.target.result);

            chunkIndex++;

            if (chunkIndex * chunkSize < file.size) {

              // 继续读取下一分片

              const nextChunk = file.slice(chunkIndex * chunkSize, (chunkIndex + 1) * chunkSize);

              fileReader.readAsArrayBuffer(nextChunk);

            } else {

              // 计算完成,获取文件MD5

              this.fileMd5 = spark.end();

              $fileMd5.text(this.fileMd5);

              $uploadStatus.text('MD5计算完成,可开始上传');

              $uploadLog.append(`<p>[${this.getCurrentTime()}] 文件MD5计算完成:${this.fileMd5}</p>`);

              // 初始化分片

              this.initChunks();

              // 检查本地存储的已上传分片

              this.checkLocalUploadedChunks();

              // 启用上传按钮

              $uploadBtn.prop('disabled', false);

            }

          };

          // 读取第一分片

          const firstChunk = file.slice(0, chunkSize);

          fileReader.readAsArrayBuffer(firstChunk);

        }

        // 初始化分片列表

        initChunks() {

          this.chunks = [];

          const totalChunks = Math.ceil(this.file.size / UPLOAD_CONFIG.chunkSize);

          for (let i = 0; i < totalChunks; i++) {

            const start = i * UPLOAD_CONFIG.chunkSize;

            const end = Math.min(start + UPLOAD_CONFIG.chunkSize, this.file.size);

            this.chunks.push({

              index: i,

              start: start,

              end: end,

              size: end - start

            });

          }

          $uploadLog.append(`<p>[${this.getCurrentTime()}] 文件分片完成,共${totalChunks}个分片(每个${UPLOAD_CONFIG.chunkSize / (1024 * 1024)}MB)</p>`);

        }

        // 检查本地存储的已上传分片(localStorage)

        checkLocalUploadedChunks() {

          const localKey = `upload_${this.fileMd5}`;

          const localData = localStorage.getItem(localKey);

          if (localData) {

            this.uploadedChunks = JSON.parse(localData);

            this.currentChunkIndex = this.uploadedChunks.length > 0 ? Math.max(...this.uploadedChunks) + 1 : 0;

            // 计算已上传进度

            this.totalProgress = (this.uploadedChunks.length / this.chunks.length) * 100;

            $progressBar.css('width', this.totalProgress + '%');

            $progressPercent.text(this.totalProgress.toFixed(2) + '%');

            $uploadLog.append(`<p>[${this.getCurrentTime()}] 从本地缓存发现已上传${this.uploadedChunks.length}个分片,可继续上传</p>`);

            $uploadStatus.text(`已上传${this.totalProgress.toFixed(2)}%,可继续上传`);

          } else {

            $uploadLog.append(`<p>[${this.getCurrentTime()}] 未发现本地缓存的上传记录,将从头上传</p>`);

          }

        }

        // 开始/继续上传

        startUpload() {

          if (!this.file || !this.fileMd5) return;

          if (this.currentChunkIndex >= this.chunks.length) {

            $uploadStatus.text('文件已上传完成,无需重复上传');

            return;

          }

          this.isPaused = false;

          $uploadBtn.prop('disabled', true);

          $pauseBtn.prop('disabled', false);

          $uploadStatus.text(`正在上传分片 ${this.currentChunkIndex + 1}/${this.chunks.length}`);

          // 上传当前分片

          this.uploadChunk(this.currentChunkIndex);

        }

        // 上传单个分片

        uploadChunk(chunkIndex) {

          if (this.isPaused) return;

          if (chunkIndex >= this.chunks.length) {

            // 所有分片上传完成,请求合并

            this.requestMerge();

            return;

          }

          const chunk = this.chunks[chunkIndex];

          // 截取当前分片的文件数据

          const chunkBlob = this.file.slice(chunk.start, chunk.end);

          const formData = new FormData();

          // 构建表单数据

          formData.append('fileMd5', this.fileMd5);

          formData.append('chunkIndex', chunkIndex);

          formData.append('totalChunks', this.chunks.length);

          formData.append('fileName', this.file.name);

          formData.append('chunk', chunkBlob);

          // 发送分片上传请求(模拟接口,实际项目替换为真实API)

          this.xhr = $.ajax({

            url: UPLOAD_CONFIG.api.upload,

            type: 'POST',

            data: formData,

            processData: false,

            contentType: false,

            xhr: () => {

              // 监听上传进度

              const xhr = $.ajaxSettings.xhr();

              xhr.upload.addEventListener('progress', (e) => {

                if (e.lengthComputable) {

                  // 当前分片的上传进度

                  const chunkProgress = (e.loaded / e.total) * 100;

                  // 总进度 = 已上传分片进度 + 当前分片进度

                  const totalProgress = (

                    (this.uploadedChunks.length / this.chunks.length) +

                    (chunkProgress / 100 / this.chunks.length)

                  ) * 100;

                  this.totalProgress = totalProgress;

                  $progressBar.css('width', totalProgress + '%');

                  $progressPercent.text(totalProgress.toFixed(2) + '%');

                }

              });

              return xhr;

            },

            success: (res) => {

              // 模拟接口成功响应(实际项目根据接口返回判断)

              if (res.code === 200) {

                // 记录已上传分片

                this.uploadedChunks.push(chunkIndex);

                // 保存到本地存储

                this.saveUploadedChunksToLocal();

                // 日志记录

                $uploadLog.append(`<p>[${this.getCurrentTime()}] 分片 ${chunkIndex + 1} 上传成功</p>`);

                // 上传下一分片

                this.currentChunkIndex = chunkIndex + 1;

                this.uploadChunk(this.currentChunkIndex);

              } else {

                $uploadLog.append(`<p>[${this.getCurrentTime()}] 分片 ${chunkIndex + 1} 上传失败:${res.msg},将重试...</p>`);

                // 重试当前分片(延迟1秒)

                setTimeout</doubaocanvas>

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容