在实际开发中,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>