|
实战痛点 |
解决方案 |
技术工具 |
|---|---|---|
|
输入时高频请求后端校验 |
防抖控制请求时机,前端预校验优先 |
防抖函数+正则表达式 |
|
用户连续点击提交按钮 |
状态锁+按钮置灰双重保障 |
isSubmitting状态变量 |
|
错误信息展示混乱 |
与后端约定结构化返回格式,精准渲染字段错误 |
字段级错误容器+统一渲染函数 |
<!-- HTML结构:语义化布局+错误容器 -->
<form id="registerForm" class="form-container">
<div class="form-item">
<label for="phone">手机号:</label>
<input type="tel" name="phone" id="phone" placeholder="请输入11位手机号">
<span class="error-message" id="phoneError"></span>
</div>
<div class="form-item">
<label for="code">验证码:</label>
<div class="code-group">
<input type="text" name="code" id="code" placeholder="请输入6位验证码">
<button type="button" id="getCodeBtn" class="btn-code">获取验证码</button>
</div>
<span class="error-message" id="codeError"></span>
</div>
<div class="form-item">
<label for="password">密码:</label>
<input type="password" name="password" id="password" placeholder="请输入6-16位密码">
<span class="error-message" id="passwordError"></span>
</div>
<button type="submit" id="submitBtn" class="btn-submit">注册并登录</button>
</form>
< href="it8.tlbelt.cn">
< href="it7.tlbelt.cn">
< href="ity.tlbelt.cn">
< href="itz.tlbelt.cn">
< href="it7.tlbelt.cn">
< href="it9.tlbelt.cn">
< href="itc.tlbelt.cn">
< href="itx.tlbelt.cn">
< href="ito.tlbelt.cn">
< href="itg.tlbelt.cn">
< href="it7.tlbelt.cn">
< href="itm.tlbelt.cn">
< href="ito.tlbelt.cn">
< href="it6.tlbelt.cn">
< href="itb.tlbelt.cn">
< href="itf.tlbelt.cn">
< href="itr.tlbelt.cn">
< href="itx.tlbelt.cn">
< href="itr.tlbelt.cn">
< href="ita.tlbelt.cn">
<script>
// 1. 通用工具函数:防抖(减少高频请求)
function debounce(func, delay = 500) {
let timer = null;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => func.apply(this, args), delay);
};
}
// 2. 错误处理工具:统一渲染与清除
function showError(field, message) {
const errorEl = document.getElementById(`${field}Error`);
const inputEl = document.getElementById(field);
errorEl.textContent = message;
errorEl.style.color = "#ff4d4f";
inputEl.classList.add("error-border"); // 输入框红色边框提示
}
function clearError(field) {
const errorEl = document.getElementById(`${field}Error`);
const inputEl = document.getElementById(field);
errorEl.textContent = "";
inputEl.classList.remove("error-border");
}
// 3. 手机号校验:失焦触发+前端预校验+后端查重
const phoneInput = document.getElementById("phone");
phoneInput.addEventListener("blur", debounce(async (e) => {
const phone = e.target.value.trim();
// 前端预校验:先判断格式,减少后端请求
if (!phone) {
showError("phone", "手机号不能为空");
return;
}
if (!/^1[3-9]\d{9}$/.test(phone)) {
showError("phone", "请输入正确的11位手机号");
return;
}
// 后端校验:判断是否已注册
try {
const res = await fetch("/api/user/check-phone", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ phone })
});
const data = await res.json();
if (data.code !== 200) {
showError("phone", data.message); // 后端返回:"该手机号已注册"
} else {
clearError("phone");
}
} catch (err) {
showError("phone", "网络异常,请稍后再试");
}
}));
// 4. 验证码逻辑:倒计时+防重复发送
const getCodeBtn = document.getElementById("getCodeBtn");
let isCodeSending = false; // 状态锁
getCodeBtn.addEventListener("click", async () => {
const phone = phoneInput.value.trim();
// 先校验手机号合法性
if (!/^1[3-9]\d{9}$/.test(phone)) {
showError("phone", "请先输入正确的手机号");
phoneInput.focus();
return;
}
if (isCodeSending) return; // 防止重复点击
// 按钮状态切换:置灰+倒计时
isCodeSending = true;
getCodeBtn.disabled = true;
getCodeBtn.textContent = "60s后重新获取";
let countdown = 60;
try {
// 调用验证码接口
const res = await fetch("/api/user/send-code", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ phone })
});
const data = await res.json();
if (data.code !== 200) {
throw new Error(data.message);
}
} catch (err) {
// 异常处理:恢复按钮状态
clearInterval(timer);
getCodeBtn.disabled = false;
getCodeBtn.textContent = "获取验证码";
isCodeSending = false;
showError("code", err.message || "验证码发送失败");
}
// 倒计时逻辑
const timer = setInterval(() => {
countdown--;
getCodeBtn.textContent = `${countdown}s后重新获取`;
if (countdown <= 0) {
clearInterval(timer);
getCodeBtn.disabled = false;
getCodeBtn.textContent = "获取验证码";
isCodeSending = false;
}
}, 1000);
});
// 5. 表单提交:全量校验+防重复提交+结果处理
const registerForm = document.getElementById("registerForm");
const submitBtn = document.getElementById("submitBtn");
let isSubmitting = false;
registerForm.addEventListener("submit", async (e) => {
e.preventDefault(); // 阻止默认刷新
if (isSubmitting) return;
// 1. 收集表单数据
const formData = {
phone: document.getElementById("phone").value.trim(),
code: document.getElementById("code").value.trim(),
password: document.getElementById("password").value.trim()
};
// 2. 前端全量校验
let isFormValid = true;
Object.keys(formData).forEach(field => {
if (!formData[field]) {
showError(field, `${field === 'phone' ? '手机号' : field === 'code' ? '验证码' : '密码'}不能为空`);
isFormValid = false;
} else {
clearError(field);
}
});
if (formData.password.length < 6 || formData.password.length > 16) {
showError("password", "密码长度需为6-16位");
isFormValid = false;
}
if (!isFormValid) return;
// 3. 提交状态控制
isSubmitting = true;
submitBtn.disabled = true;
submitBtn.textContent = "注册中...";
try {
// 4. 调用注册接口
const res = await fetch("/api/user/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(formData)
});
const data = await res.json();
if (data.code === 200) {
// 注册成功:存储Token+跳转登录页
localStorage.setItem("token", data.token);
alert("注册成功!即将跳转到首页");
window.location.href = "/home";
} else {
// 结构化错误:字段级错误精准提示
if (data.field) {
showError(`${data.field}Error`, data.message);
} else {
alert(data.message); // 全局错误(如系统维护)
}
}
} catch (err) {
alert("网络异常,注册失败,请稍后再试");
} finally {
// 5. 恢复状态(无论成功失败)
isSubmitting = false;
submitBtn.disabled = false;
submitBtn.textContent = "注册并登录";
}
});
</script>
<!-- HTML结构 -->
<form id="registerForm">
<div class="form-item">
<label>手机号:</label>
<input type="tel" name="phone" id="phone" placeholder="请输入手机号">
<span class="error-message" id="phoneError"></span>
</div>
<div class="form-item">
< href="itj.tlbelt.cn">
< href="itw.tlbelt.cn">
< href="itb.tlbelt.cn">
< href="itn.tlbelt.cn">
< href="it3.tlbelt.cn">
< href="itw.tlbelt.cn">
< href="itq.tlbelt.cn">
< href="itn.tlbelt.cn">
< href="itu.tlbelt.cn">
< href="it8.tlbelt.cn">
< href="itc.tlbelt.cn">
< href="it3.tlbelt.cn">
< href="it0.tlbelt.cn">
< href="itk.tlbelt.cn">
< href="itf.tlbelt.cn">
< href="itt.tlbelt.cn">
< href="it6.tlbelt.cn">
< href="it6.tlbelt.cn">
< href="it7.tlbelt.cn">
< href="it9.tlbelt.cn">
<label>验证码:</label>
<input type="text" name="code" id="code" placeholder="请输入验证码">
<button type="button" id="getCodeBtn">获取验证码</button>
<span class="error-message" id="codeError"></span>
</div>
<div class="form-item">
<label>密码:</label>
<input type="password" name="password" id="password" placeholder="请输入密码">
<span class="error-message" id="passwordError"></span>
</div>
<button type="submit" id="submitBtn">注册</button>
</form>
<script>
// 1. 通用防抖函数(避免输入时高频请求)
function debounce(func, delay = 500) {
let timer = null;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => func.apply(this, args), delay);
};
}
// 2. 错误信息渲染工具函数
function showError(elementId, message) {
const errorEl = document.getElementById(elementId);
errorEl.textContent = message;
errorEl.style.color = "#ff4d4f";
// 给输入框添加错误样式
document.getElementById(elementId.replace("Error", "")).classList.add("error-border");
}
// 3. 清除错误信息
function clearError(elementId) {
const errorEl = document.getElementById(elementId);
errorEl.textContent = "";
document.getElementById(elementId.replace("Error", "")).classList.remove("error-border");
}
// 4. 手机号实时校验(失焦+防抖)
const phoneInput = document.getElementById("phone");
phoneInput.addEventListener("blur", debounce(async (e) => {
const phone = e.target.value.trim();
if (!phone) {
showError("phoneError", "手机号不能为空");
return;
}
// 先做前端格式校验,减少后端请求
if (!/^1[3-9]\d{9}$/.test(phone)) {
showError("phoneError", "手机号格式错误");
return;
}
try {
// 调用后端校验接口(是否已注册)
const res = await fetch("/api/user/check-phone", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ phone })
});
const data = await res.json();
if (data.code !== 200) {
showError("phoneError", data.message); // 后端返回:"该手机号已注册"
} else {
clearError("phoneError");
}
} catch (err) {
showError("phoneError", "网络异常,请稍后再试");
}
}));
// 5. 获取验证码(防重复点击)
const getCodeBtn = document.getElementById("getCodeBtn");
let isCodeSending = false;
getCodeBtn.addEventListener("click", async () => {
const phone = phoneInput.value.trim();
if (!/^1[3-9]\d{9}$/.test(phone)) {
showError("phoneError", "请先输入正确的手机号");
return;
}
if (isCodeSending) return; // 状态锁防重复点击
// 按钮置灰并倒计时
isCodeSending = true;
getCodeBtn.disabled = true;
getCodeBtn.textContent = "60s后重新获取";
let countdown = 60;
try {
// 调用获取验证码接口
const res = await fetch("/api/user/send-code", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ phone })
});
const data = await res.json();
if (data.code !== 200) {
showError("codeError", data.message);
// 倒计时中断,恢复按钮状态
clearInterval(timer);
getCodeBtn.disabled = false;
getCodeBtn.textContent = "获取验证码";
isCodeSending = false;
}
} catch (err) {
showError("codeError", "验证码发送失败");
getCodeBtn.disabled = false;
getCodeBtn.textContent = "获取验证码";
isCodeSending = false;
}
// 倒计时逻辑
const timer = setInterval(() => {
countdown--;
getCodeBtn.textContent = `${countdown}s后重新获取`;
if (countdown <= 0) {
clearInterval(timer);
getCodeBtn.disabled = false;
getCodeBtn.textContent = "获取验证码";
isCodeSending = false;
}
}, 1000);
});
// 6. 表单提交(防重复提交+全量校验)
const registerForm = document.getElementById("registerForm");
const submitBtn = document.getElementById("submitBtn");
let isSubmitting = false;
registerForm.addEventListener("submit", async (e) => {
e.preventDefault(); // 阻止默认刷新行为
if (isSubmitting) return;
// 1. 收集表单数据
const formData = {
phone: document.getElementById("phone").value.trim(),
code: document.getElementById("code").value.trim(),
password: document.getElementById("password").value.trim()
};
// 2. 前端全量校验
let isFormValid = true;
if (!formData.phone) {
showError("phoneError", "手机号不能为空");
isFormValid = false;
}
if (!formData.code) {
showError("codeError", "验证码不能为空");
isFormValid = false;
}
if (formData.password.length < 6) {
showError("passwordError", "密码长度不能少于6位");
isFormValid = false;
}
if (!isFormValid) return;
// 3. 提交状态控制
isSubmitting = true;
submitBtn.disabled = true;
submitBtn.textContent = "注册中...";
try {
// 4. 调用注册接口
const res = await fetch("/api/user/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(formData)
});
const data = await res.json();
if (data.code === 200) {
// 注册成功,跳转登录页
alert("注册成功!即将跳转到登录页");
window.location.href = "/login";
} else {
// 后端返回的字段级错误(如验证码过期)
if (data.field) {
showError(`${data.field}Error`, data.message);
} else {
alert(data.message); // 全局错误(如系统维护)
}
}
} catch (err) {
alert("网络异常,注册失败,请稍后再试");
} finally {
// 5. 恢复按钮状态(无论成功失败)
isSubmitting = false;
submitBtn.disabled = false;
submitBtn.textContent = "注册";
}
});
</script>
isSubmitting等状态变量,防止通过控制台等方式绕过UI限制重复提交;{code, message, field}),实现错误信息的精准渲染。<!-- HTML结构 -->
<div class="search-container">
<input type="text" id="searchInput" placeholder="请输入商品名称搜索...">
<ul id="searchSuggest" class="suggest-list"></ul>
</div>
<script>
// 1. 初始化变量:缓存容器、请求控制器(用于中断请求)
const searchCache = new Map(); // 缓存:key=关键词,value=搜索结果
let abortController = null;
// 2. 防抖函数(搜索场景延迟300ms更合适)
function debounce(func, delay = 300) {
let timer = null;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => func.apply(this, args), delay);
};
}
// 3. 渲染搜索结果
function renderSuggest(results) {
const suggestEl = document.getElementById("searchSuggest");
if (results.length === 0) {
suggestEl.innerHTML = "<li class='empty'>暂无匹配结果</li>";
return;
}
// 拼接结果列表
const html = results.map(item => `
<li class="suggest-item" data-id="${item.id}">
<img src="${item.cover}" alt="${item.name}">
<div class="info">
<p class="name">${item.name}</p>
<p class="price">¥${item.price.toFixed(2)}</p>
</div>
</li>
`).join("");
suggestEl.innerHTML = html;
// 给结果项绑定点击事件(跳转到商品详情)
suggestEl.querySelectorAll(".suggest-item").forEach(item => {
item.addEventListener("click", () => {
window.location.href = `/product/${item.dataset.id}`;
});
});
}
// 4. 搜索核心函数
async function searchProduct(keyword) {
const suggestEl = document.getElementById("searchSuggest");
// 空关键词:清空结果和缓存
if (!keyword.trim()) {
suggestEl.innerHTML = "";
searchCache.delete(keyword);
return;
}
// 命中缓存:直接渲染
if (searchCache.has(keyword)) {
renderSuggest(searchCache.get(keyword));
return;
}
// 中断上一次未完成的请求(防止结果顺序错乱)
if (abortController) abortController.abort();
abortController = new AbortController();
const signal = abortController.signal;
try {
suggestEl.innerHTML = "<li class='loading'>搜索中...</li>";
// 发起搜索请求(携带信号用于中断)
const res = await fetch(`/api/product/search?q=${encodeURIComponent(keyword)}`, { signal });
if (!res.ok) throw new Error("搜索失败");
const data = await res.json();
if (data.code === 200) {
// 存入缓存(只缓存有效结果)
searchCache.set(keyword, data.data);
renderSuggest(data.data);
}
} catch (err) {
// 忽略中断错误,只处理网络错误
if (err.name !== "AbortError") {
suggestEl.innerHTML = "<li class='error'>搜索异常,请稍后再试</li>";
}
}
}
// 5. 绑定输入事件
const searchInput = document.getElementById("searchInput");
searchInput.addEventListener("input", debounce((e) => {
const keyword = e.target.value.trim();
searchProduct(keyword);
}));
// 6. 点击页面其他区域关闭搜索结果
document.addEventListener("click", (e) => {
const searchContainer = document.querySelector(".search-container");
if (!searchContainer.contains(e.target)) {
document.getElementById("searchSuggest").innerHTML = "";
}
});
</script>
encodeURIComponent处理关键词中的特殊字符(如空格、中文),避免请求参数错误;<!-- HTML结构 -->
<div class="dashboard">
<div class="loading-progress">
<div class="progress-bar" id="progressBar"></div>
<span id="progressText">加载中... 0%</span>
</div>
<div class="dashboard-grid">
<div class="card" id="salesCard">
<h3>今日销售额</h3>
<p class="value" id="salesValue">--</p>
<button class="retry-btn" style="display:none" data-target="sales">重试</button>
</div>
<div class="card" id="orderCard">
<h3>今日订单数</h3>
<p class="value" id="orderValue">--</p>
<button class="retry-btn" style="display:none" data-target="order">重试</button>
</div>
<div class="card" id="userCard">
<h3>新增用户数</h3>
<p class="value" id="userValue">--</p>
<button class="retry-btn" style="display:none" data-target="user">重试</button>
</div>
<div class="card" id="conversionCard">
<h3>转化率</h3>
<p class="value" id="conversionValue">--</p>
<button class="retry-btn" style="display:none" data-target="conversion">重试</button>
</div>
</div>
</div>
<script>
// 1. 定义需要加载的请求配置(关联卡片与接口)
const dashboardApis = [
{
key: "sales", // 对应卡片标识
url: "/api/dashboard/sales",
render: (data) => { // 渲染函数
document.getElementById("salesValue").textContent = `¥${data.toLocaleString()}`;
}
},
{
key: "order",
url: "/api/dashboard/orders",
render: (data) => {
document.getElementById("orderValue").textContent = data.toLocaleString();
}
},
{
key: "user",
url: "/api/dashboard/new-users",
render: (data) => {
document.getElementById("userValue").textContent = data.toLocaleString();
}
},
{
key: "conversion",
url: "/api/dashboard/conversion",
render: (data) => {
document.getElementById("conversionValue").textContent = `${(data * 100).toFixed(2)}%`;
}
}
];
// 2. 并发请求调度器(限制并发数为3)
async function requestConcurrent(requests, limit = 3) {
const results = [];
const executing = new Set();
const queue = [...requests];
let completedCount = 0; // 已完成请求数(用于计算进度)
const totalCount = requests.length; // 总请求数
// 更新进度条
function updateProgress() {
const progress = Math.round((completedCount / totalCount) * 100);
document.getElementById("progressBar").style.width = `${progress}%`;
document.getElementById("progressText").textContent = `加载中... ${progress}%`;
// 全部完成后隐藏进度条
if (progress === 100) {
setTimeout(() => {
document.querySelector(".loading-progress").style.display = "none";
}, 500);
}
}
async function dispatch() {
if (queue.length === 0 && executing.size === 0) return results;
while (executing.size < limit && queue.length > 0) {
const { key, request, render } = queue.shift();
const promise = request()
.then(data => {
render(data); // 成功后渲染数据
return { key, success: true };
})
.catch(err => {
// 失败后显示重试按钮
document.querySelector(`.retry-btn[data-target="${key}"]`).style.display = "inline-block";
return { key, success: false, error: err.message };
})
.finally(() => {
executing.delete(promise);
completedCount++;
updateProgress(); // 每完成一个请求更新进度
dispatch();
});
executing.add(promise);
results.push(promise);
}
await Promise.all(executing);
return dispatch();
}
updateProgress(); // 初始化进度
return dispatch();
}
// 3. 初始化请求函数(包装每个API请求)
function initDashboardRequests() {
return dashboardApis.map(api => ({
key: api.key,
render: api.render,
request: () => fetch(api.url)
.then(res => {
if (!res.ok) throw new Error(`请求失败: ${res.status}`);
return res.json();
})
.then(data => {
if (data.code !== 200) throw new Error(data.message);
return data.data;
})
}));
}
// 4. 重试单个请求
document.querySelectorAll(".retry-btn").forEach(btn => {
btn.addEventListener("click", async () => {
const key = btn.dataset.target;
// 找到对应的API配置
const api = dashboardApis.find(item => item.key === key);
if (!api) return;
// 按钮置为加载中状态
btn.textContent = "重试中...";
btn.disabled = true;
try {
// 重新请求
const res = await fetch(api.url);
const data = await res.json();
if (data.code === 200) {
api.render(data.data); // 重新渲染
btn.style.display = "none"; // 隐藏重试按钮
} else {
throw new Error(data.message);
}
} catch (err) {
btn.textContent = "重试失败,点击再试";
btn.disabled = false;
}
});
});
// 5. 页面加载时初始化数据看板
window.addEventListener("load", () => {
const requests = initDashboardRequests();
requestConcurrent(requests);
});
</script>
Blob.slice()将文件分割为固定大小的分片(如5MB);
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/69921553/viewspace-3102026/ ,如需转载,请注明出处,否则将追究法律责任。