热门角色不仅是灵感来源,更是你的效率助手。通过精挑细选的角色提示词,你可以快速生成高质量内容、提升创作灵感,并找到最契合你需求的解决方案。让创作更轻松,让价值更直接!
我们根据不同用户需求,持续更新角色库,让你总能找到合适的灵感入口。
本提示词专为前端开发场景设计,能够根据用户指定的HTML元素、事件类型和回调需求,生成符合JavaScript最佳实践的事件监听代码。通过结构化参数输入,自动处理事件绑定、事件对象处理和错误捕获等核心功能,支持多种常见事件类型和元素选择方式,帮助开发者快速实现网页交互功能,提升开发效率和代码质量。输出结果包含完整代码实现、技术原理说明和实际应用指导。
"use strict";
/**
* 事件委托方式绑定“加入购物车”点击事件。
* - 目标元素:.product-card .buy-btn
* - 事件类型:click
* - 回调逻辑:
* 1) 将当前商品加入购物车
* 2) 更新右上角购物车数量徽标
* 3) 按钮进入短暂禁用与加载态防止重复点击
* 4) 记录一次埋点事件
* 5) 异常时轻提示且恢复按钮状态
*
* 返回值:一个用于解绑事件的函数。
*/
function attachBuyButtonListener(options = {}) {
const cfg = normalizeConfig(options);
// 用 WeakSet 记录处于“加载态”的按钮,避免重复点击并防止内存泄漏
const busyButtons = new WeakSet();
function clickHandler(event) {
// 事件委托:只处理 .buy-btn 上的点击
const btn = event.target.closest(cfg.selectors.button);
if (!btn) return;
const card = btn.closest(cfg.selectors.card);
if (!card) return;
// 如果是 <a>,阻止默认跳转
if (btn.tagName === "A") {
event.preventDefault();
}
// 已处于忙碌态或被禁用时直接返回
if (busyButtons.has(btn) || btn.disabled) {
return;
}
// 获取商品数据
let product;
try {
product = cfg.getProductFromCard(card);
} catch (ex) {
console.error("[buy-btn] getProductFromCard error:", ex);
cfg.toast("无法获取商品信息,请稍后重试");
return;
}
if (!product || !product.id) {
cfg.toast("商品信息缺失,无法加入购物车");
return;
}
// 设置按钮加载态
busyButtons.add(btn);
setButtonLoading(btn, true, cfg.loadingText, cfg.loadingClass);
// 预埋点(不阻塞主流程)
try {
cfg.services.track("buy_click", {
productId: product.id,
productName: product.name,
price: product.price,
});
} catch (trackErr) {
// 埋点失败不影响主流程
console.warn("[buy-btn] track buy_click error:", trackErr);
}
// 加入购物车
(async () => {
try {
const result = await cfg.services.addItem(product);
// 更新徽标
const cartCount =
result && typeof result.cartCount !== "undefined"
? Number(result.cartCount)
: null;
updateCartBadge(cfg.root, cfg.selectors.badge, cartCount);
// 成功埋点
try {
cfg.services.track("add_to_cart_success", {
productId: product.id,
productName: product.name,
price: product.price,
cartCount,
});
} catch (trackErr) {
console.warn("[buy-btn] track add_to_cart_success error:", trackErr);
}
} catch (err) {
console.error("[buy-btn] addItem error:", err);
cfg.toast("加入购物车失败,请稍后重试");
// 失败埋点
try {
cfg.services.track("add_to_cart_error", {
productId: product.id,
message: err && err.message,
});
} catch (trackErr) {
console.warn("[buy-btn] track add_to_cart_error error:", trackErr);
}
} finally {
// 恢复按钮状态
setButtonLoading(btn, false, cfg.loadingText, cfg.loadingClass);
busyButtons.delete(btn);
}
})();
}
// 绑定事件(冒泡阶段,非被动监听,因为需要调用 preventDefault)
cfg.root.addEventListener("click", clickHandler, { capture: false });
// 返回解绑函数,避免内存泄漏
return function detachBuyButtonListener() {
cfg.root.removeEventListener("click", clickHandler, { capture: false });
};
}
/* ---------- 工具函数与默认配置 ---------- */
function normalizeConfig(options) {
const cfg = {
root:
options.root && isNodeLike(options.root)
? options.root
: document, // 默认在 document 上委托
selectors: {
card: options.selectors?.card || ".product-card",
button: options.selectors?.button || ".buy-btn",
badge: options.selectors?.badge || ".cart-badge",
},
services: {
addItem:
typeof options.services?.addItem === "function"
? options.services.addItem
: async () => {
throw new Error("services.addItem 未提供");
},
track:
typeof options.services?.track === "function"
? options.services.track
: () => {},
},
toast:
typeof options.toast === "function"
? options.toast
: (msg) => console.warn("[toast]", msg),
getProductFromCard:
typeof options.getProductFromCard === "function"
? options.getProductFromCard
: defaultGetProductFromCard,
loadingText: options.loadingText || "处理中...",
loadingClass: options.loadingClass || "is-loading",
};
return cfg;
}
function isNodeLike(node) {
return (
node &&
(node === document ||
node === window ||
(typeof node.nodeType === "number" && (node.nodeType === 1 || node.nodeType === 9)))
);
}
function defaultGetProductFromCard(cardEl) {
// 读取常见的数据来源:data-* 或子元素
const id =
cardEl.dataset.productId ||
cardEl.querySelector("[data-product-id]")?.dataset.productId;
const name =
cardEl.dataset.productName ||
cardEl.querySelector("[data-product-name]")?.dataset.productName ||
cardEl.querySelector(".title")?.textContent?.trim() ||
"";
const priceStr =
cardEl.dataset.productPrice ||
cardEl.querySelector("[data-product-price]")?.dataset.productPrice;
const price =
priceStr !== undefined && priceStr !== null && priceStr !== ""
? Number(priceStr)
: undefined;
return { id, name, price };
}
function setButtonLoading(btn, loading, loadingText, loadingClass) {
if (loading) {
if (!btn.dataset.originalText) {
btn.dataset.originalText = (btn.textContent || "").trim();
}
btn.disabled = true;
btn.setAttribute("aria-disabled", "true");
btn.setAttribute("aria-busy", "true");
if (loadingClass) btn.classList.add(loadingClass);
// 简单替换文案。若按钮内有图标/自定义内容,可用更细粒度的处理。
btn.textContent = loadingText;
} else {
btn.disabled = false;
btn.removeAttribute("aria-disabled");
btn.removeAttribute("aria-busy");
if (loadingClass) btn.classList.remove(loadingClass);
if (btn.dataset.originalText) {
btn.textContent = btn.dataset.originalText;
delete btn.dataset.originalText;
}
}
}
function updateCartBadge(root, badgeSelector, cartCount) {
const badge = root.querySelector(badgeSelector);
if (!badge) return;
// 有明确数量则直接设置,否则尝试 +1
if (cartCount !== null && !Number.isNaN(Number(cartCount))) {
badge.textContent = String(cartCount);
} else {
const current = Number(badge.textContent) || 0;
badge.textContent = String(current + 1);
}
// 轻微动效提示更新(依赖 .pulse 的 CSS,可选)
badge.classList.add("pulse");
setTimeout(() => badge.classList.remove("pulse"), 400);
// 辅助无障碍:动态区域可设置为 polite
if (!badge.hasAttribute("aria-live")) {
badge.setAttribute("aria-live", "polite");
}
}
/* ---------- 可选:一个简易的 Toast 实现(生产环境可替换为成熟组件) ---------- */
function createToast(message, duration = 2000) {
const containerId = "toast-container";
let container = document.getElementById(containerId);
if (!container) {
container = document.createElement("div");
container.id = containerId;
container.style.position = "fixed";
container.style.top = "16px";
container.style.right = "16px";
container.style.zIndex = "10000";
container.style.display = "flex";
container.style.flexDirection = "column";
container.style.gap = "8px";
container.setAttribute("role", "status");
container.setAttribute("aria-live", "polite");
document.body.appendChild(container);
}
const toast = document.createElement("div");
toast.textContent = message;
toast.style.background = "rgba(0,0,0,0.8)";
toast.style.color = "#fff";
toast.style.padding = "8px 12px";
toast.style.borderRadius = "6px";
toast.style.fontSize = "14px";
toast.style.boxShadow = "0 2px 8px rgba(0,0,0,0.25)";
container.appendChild(toast);
setTimeout(() => {
toast.remove();
if (container.childElementCount === 0) {
container.remove();
}
}, duration);
}
<!-- 示例 HTML -->
<div class="header">
<span class="cart-badge" aria-live="polite">0</span>
</div>
<div class="product-list">
<div class="product-card" data-product-id="sku-1001" data-product-name="示例商品A" data-product-price="199">
<h3 class="title">示例商品A</h3>
<button class="buy-btn">加入购物车</button>
</div>
<div class="product-card" data-product-id="sku-1002" data-product-name="示例商品B" data-product-price="299">
<h3 class="title">示例商品B</h3>
<a class="buy-btn" href="/buy/sku-1002">加入购物车</a>
</div>
</div>
<script>
// 模拟服务实现(实际项目中调用后端 API)
const services = {
addItem: async (product) => {
// 这里可替换为真实接口调用:
// const resp = await fetch('/api/cart', { method: 'POST', body: JSON.stringify(product) });
// const data = await resp.json();
// return { cartCount: data.count };
await new Promise((r) => setTimeout(r, 600)); // 模拟网络延迟
window.__cartCount = (window.__cartCount || 0) + 1;
// 随机制造一次失败演示
if (Math.random() < 0.1) throw new Error("网络异常");
return { cartCount: window.__cartCount };
},
track: (eventName, payload) => {
// 这里可替换为真实埋点 SDK
console.log("[track]", eventName, payload);
},
};
// 使用自定义 Toast
const toast = (msg) => createToast(msg);
// 绑定事件
const detach = attachBuyButtonListener({
root: document,
selectors: {
card: ".product-card",
button: ".buy-btn",
badge: ".cart-badge",
},
services,
toast,
loadingText: "处理中...",
loadingClass: "is-loading",
});
// 测试:3秒后可选择解除绑定
// setTimeout(() => {
// detach();
// console.log("已解除买按钮事件绑定");
// }, 3000);
</script>
<style>
/* 可选的徽标动效与按钮加载样式 */
.cart-badge.pulse { animation: pulse 0.4s ease; }
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.2); }
100% { transform: scale(1); }
}
.buy-btn.is-loading { opacity: 0.7; cursor: not-allowed; }
</style>
测试建议:
/**
* 专业事件监听器:表单提交(submit)
* 需求:
* - 阻止默认提交
* - 校验邮箱格式与密码强度
* - 逐项高亮错误并显示提示
* - 提交时禁用表单与按钮
* - 使用 fetch 异步提交
* - 成功后清空并显示成功条
* - 失败则聚焦首个错误
*/
(function initSignupForm(selector = 'form#signup') {
const form = document.querySelector(selector);
if (!form) {
console.warn(`[signup] 未找到表单元素:${selector}`);
return;
}
// 防止重复绑定同一个表单(避免内存泄漏/事件冲突)
if (form.dataset.bound === 'true') return;
form.dataset.bound = 'true';
// 可配置:服务端接口(默认使用 form.action / form.method)
const submitURL = form.action || '/api/signup';
const submitMethod = (form.method || 'POST').toUpperCase();
// 提升可访问性:禁用原生HTML5校验,由脚本接管
form.noValidate = true;
// 常用字段引用(可按需扩展)
const fields = {
email: form.querySelector('input[name="email"]'),
password: form.querySelector('input[name="password"]'),
};
// 提交过程的中断控制(防止重复请求、支持取消)
let currentAbortController = null;
// 安全的 CSS 选择器转义(兼容性处理)
const cssEscape = (name) =>
window.CSS && typeof window.CSS.escape === 'function'
? window.CSS.escape(name)
: String(name).replace(/([^\w-])/g, '\\$1');
// 状态栏:用于展示成功/错误信息(无侵入,自动创建)
function getStatusBar() {
let bar = form.querySelector('.form-status');
if (!bar) {
bar = document.createElement('div');
bar.className = 'form-status';
bar.setAttribute('role', 'status');
bar.setAttribute('aria-live', 'polite');
bar.hidden = true;
form.prepend(bar);
}
return bar;
}
function showStatus(type, message) {
const bar = getStatusBar();
bar.textContent = message; // 使用 textContent 防止 XSS
bar.dataset.type = type; // 可用于样式区分:success/error/info
bar.hidden = false;
}
// 错误消息容器:优先使用同一字段附近的 .error-msg,没有则自动创建
function getErrorContainer(input) {
const inField = input.closest('.field');
let msg = inField?.querySelector('.error-msg');
if (!msg) {
const next = input.nextElementSibling;
if (next && next.classList && next.classList.contains('error-msg')) {
msg = next;
}
}
if (!msg) {
msg = document.createElement('div');
msg.className = 'error-msg';
msg.hidden = true;
input.after(msg);
}
return msg;
}
function setFieldError(input, message) {
if (!input) return;
const msg = getErrorContainer(input);
input.classList.add('is-invalid');
input.setAttribute('aria-invalid', 'true');
// 确保错误消息有可被关联的ID
if (!msg.id) {
msg.id = `${input.name || input.id || 'field'}-error`;
}
input.setAttribute('aria-describedby', msg.id);
// 显示错误文本
msg.textContent = message;
msg.hidden = false;
msg.setAttribute('role', 'alert');
}
function clearFieldError(input) {
if (!input) return;
input.classList.remove('is-invalid');
input.removeAttribute('aria-invalid');
input.removeAttribute('aria-describedby');
const msg = getErrorContainer(input);
msg.textContent = '';
msg.hidden = true;
msg.removeAttribute('role');
}
// 基础校验:邮箱与密码
function isValidEmail(email) {
// 简洁实用的邮箱校验(避免过度严格导致合法邮箱无法通过)
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return re.test(email);
}
function assessPasswordStrength(pw) {
const categories = [
/[A-Z]/.test(pw), // 大写
/[a-z]/.test(pw), // 小写
/[0-9]/.test(pw), // 数字
/[^A-Za-z0-9]/.test(pw), // 特殊字符
];
const count = categories.filter(Boolean).length;
const strong = pw.length >= 8 && count >= 3; // 至少8位,四类中满足任意三类
return { strong, count, length: pw.length };
}
function validateFields() {
const errors = [];
if (fields.email) {
clearFieldError(fields.email);
const val = fields.email.value.trim();
if (!val) {
setFieldError(fields.email, '邮箱不能为空');
errors.push(fields.email);
} else if (!isValidEmail(val)) {
setFieldError(fields.email, '邮箱格式不正确');
errors.push(fields.email);
}
}
if (fields.password) {
clearFieldError(fields.password);
const val = fields.password.value;
const res = assessPasswordStrength(val);
if (!val) {
setFieldError(fields.password, '密码不能为空');
errors.push(fields.password);
} else if (!res.strong) {
setFieldError(
fields.password,
'密码需至少 8 位,并包含大小写、数字或特殊字符中的任意 3 类'
);
errors.push(fields.password);
}
}
return errors;
}
// 禁用/启用表单控件(提交阶段防止重复操作)
function setFormDisabled(disabled) {
const controls = form.querySelectorAll('input, select, textarea, button');
controls.forEach((el) => {
// 不改变隐性不可用状态的元素(例如有 data-skip-disable 时)
if (el.dataset.skipDisable === 'true') return;
el.disabled = disabled;
});
form.setAttribute('aria-busy', String(disabled));
form.dataset.disabled = String(disabled);
}
// 序列化表单为对象(FormData -> Object)
function serializeFormToObject() {
const fd = new FormData(form);
const obj = Object.create(null);
for (const [key, value] of fd.entries()) {
// 处理同名字段为数组
if (obj[key] !== undefined) {
Array.isArray(obj[key]) ? obj[key].push(value) : (obj[key] = [obj[key], value]);
} else {
obj[key] = value;
}
}
return obj;
}
// 提交到服务端(fetch)
async function postJSON(url, data, signal) {
const resp = await fetch(url, {
method: submitMethod,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify(data),
signal,
credentials: 'same-origin',
});
const contentType = resp.headers.get('content-type') || '';
let payload = null;
if (contentType.includes('application/json')) {
payload = await resp.json();
} else {
// 非JSON情况也尽量解析(并避免使用 innerHTML)
payload = await resp.text();
}
if (!resp.ok) {
const err = new Error('请求失败');
err.status = resp.status;
err.payload = payload;
throw err;
}
return payload;
}
function focusFirstError(inputs) {
const target = inputs.find((el) => el && typeof el.focus === 'function');
if (target) target.focus();
}
function clearFormAndErrors() {
form.reset();
Object.values(fields).forEach(clearFieldError);
}
// 提交事件处理函数
async function onSubmit(e) {
e.preventDefault();
// 若当前正在提交,忽略重复触发
if (form.dataset.disabled === 'true') {
return;
}
// 客户端校验
const invalids = validateFields();
if (invalids.length > 0) {
showStatus('error', '请修正标注的错误后再提交');
focusFirstError(invalids);
return;
}
// 开始提交:禁用表单,显示状态
setFormDisabled(true);
showStatus('info', '正在提交...');
// 确保只存在一个进行中的请求
if (currentAbortController) currentAbortController.abort();
currentAbortController = new AbortController();
try {
const data = serializeFormToObject();
const result = await postJSON(submitURL, data, currentAbortController.signal);
// 成功:清空表单,清理错误,显示成功条
clearFormAndErrors();
showStatus('success', '注册成功');
} catch (err) {
// 服务端返回字段级错误:尝试逐项显示
let focused = false;
if (err.payload && typeof err.payload === 'object' && err.payload.errors) {
const serverErrors = err.payload.errors;
const erroredInputs = [];
Object.keys(serverErrors).forEach((name) => {
const input = form.querySelector(`[name="${cssEscape(name)}"]`);
if (input) {
setFieldError(input, String(serverErrors[name]));
erroredInputs.push(input);
}
});
showStatus('error', err.payload.message || '提交失败,请检查表单内容');
if (erroredInputs.length) {
focusFirstError(erroredInputs);
focused = true;
}
}
// 其他错误:网络问题/非结构化错误
if (!focused) {
showStatus('error', '网络或服务器错误,请稍后重试');
focusFirstError([fields.email, fields.password].filter(Boolean));
}
} finally {
setFormDisabled(false);
}
}
// 输入时即时反馈(事件委托,提升性能与可维护性)
function onInput(e) {
const t = e.target;
if (!t || !t.name) return;
// 只对表单控件处理
if (!(t instanceof HTMLInputElement || t instanceof HTMLTextAreaElement || t instanceof HTMLSelectElement)) {
return;
}
// 即时清理错误并给出轻量校验提示
if (t.name === 'email') {
clearFieldError(t);
const val = t.value.trim();
if (val && !isValidEmail(val)) {
setFieldError(t, '邮箱格式不正确');
}
} else if (t.name === 'password') {
clearFieldError(t);
const val = t.value;
const res = assessPasswordStrength(val);
if (val && !res.strong) {
setFieldError(t, '密码强度不足(至少8位,含大小写/数字/特殊字符中的任意3类)');
}
}
}
// 绑定事件监听器(遵循现代标准,避免废弃API)
form.addEventListener('submit', onSubmit, { capture: false });
form.addEventListener('input', onInput, { capture: false });
})('form#signup');
<form id="signup" action="/api/signup" method="POST">
<div class="field">
<label for="email">邮箱</label>
<input id="email" name="email" type="email" autocomplete="email" required />
<div class="error-msg" hidden></div>
</div>
<div class="field">
<label for="password">密码</label>
<input id="password" name="password" type="password" autocomplete="new-password" required />
<div class="error-msg" hidden></div>
</div>
<button type="submit">注册</button>
<!-- 状态条会自动插入为 .form-status(如果不存在) -->
</form>
<script src="signup-listener.js"></script>
/**
* 搜索输入框联想下拉(可取消请求 + 300ms 防抖 + ARIA 无障碍)
* - 目标元素:#searchInput
* - 事件类型:input
* - 需求:
* 1) 300ms 防抖
* 2) 输入长度 >= 2 才发请求
* 3) 调用候选接口获取结果
* 4) 渲染下拉列表并使用 ARIA 属性
* 5) 请求中显示 loading
* 6) 支持取消上次请求
* 7) 清空输入时移除结果
*/
(function initSearchAutocomplete() {
const config = {
selector: '#searchInput',
minLength: 2,
debounceMs: 300,
// 建议替换为你的真实接口地址
endpoint: '/api/suggest?q=',
// 将接口返回的结果映射为 { id, label } 结构
mapResultItem: (item) => ({
id: item.id ?? String(item.value ?? item.id ?? item),
label: item.label ?? item.name ?? String(item.label ?? item),
raw: item
}),
// 可选:最多显示条数(前端裁剪)
maxItems: 10
};
const input = document.querySelector(config.selector);
if (!input) {
console.error(`[autocomplete] element not found for selector: ${config.selector}`);
return;
}
// ARIA & DOM 结构
const listboxId = `${input.id || 'searchInput'}-listbox`;
const listbox = document.createElement('ul');
listbox.id = listboxId;
listbox.role = 'listbox';
listbox.className = 'autocomplete-listbox';
listbox.hidden = true;
// 让容器紧随输入框,便于定位
input.insertAdjacentElement('afterend', listbox);
// 输入框 ARIA 属性(简化 combobox 模式)
input.setAttribute('role', 'combobox');
input.setAttribute('aria-autocomplete', 'list');
input.setAttribute('aria-expanded', 'false');
input.setAttribute('aria-controls', listboxId);
input.setAttribute('aria-haspopup', 'listbox');
// SR(屏幕阅读器)状态区域,用于播报加载与结果数
const srStatus = document.createElement('div');
srStatus.setAttribute('aria-live', 'polite');
srStatus.setAttribute('aria-atomic', 'true');
srStatus.className = 'sr-only'; // 你可在 CSS 中设置 .sr-only 进行隐藏但可读
input.insertAdjacentElement('afterend', srStatus);
// 状态管理
let debounceTimer = null;
let abortController = null;
let requestSeq = 0; // 防竞态
let isComposing = false; // 输入法合成中(中文/日文等)
// 工具函数
const setExpanded = (expanded) => {
input.setAttribute('aria-expanded', String(expanded));
listbox.hidden = !expanded;
};
const clearList = () => {
listbox.innerHTML = '';
listbox.hidden = true;
input.setAttribute('aria-expanded', 'false');
input.setAttribute('aria-busy', 'false');
srStatus.textContent = '';
};
const renderLoading = () => {
listbox.innerHTML = '';
const li = document.createElement('li');
li.className = 'autocomplete-loading';
li.setAttribute('role', 'presentation');
li.textContent = 'Loading...';
listbox.appendChild(li);
input.setAttribute('aria-busy', 'true');
setExpanded(true);
srStatus.textContent = 'Loading';
};
const renderError = (message = 'Network error') => {
listbox.innerHTML = '';
const li = document.createElement('li');
li.className = 'autocomplete-error';
li.setAttribute('role', 'presentation');
li.textContent = message;
listbox.appendChild(li);
input.setAttribute('aria-busy', 'false');
setExpanded(true);
srStatus.textContent = message;
};
const renderResults = (items) => {
listbox.innerHTML = '';
input.setAttribute('aria-busy', 'false');
if (!items || items.length === 0) {
setExpanded(false);
srStatus.textContent = 'No results';
return;
}
const frag = document.createDocumentFragment();
items.slice(0, config.maxItems).forEach((item, idx) => {
const li = document.createElement('li');
li.role = 'option';
li.tabIndex = -1;
li.dataset.id = item.id;
// 使用 textContent 防止 XSS
li.textContent = item.label;
// 唯一 id 便于 aria-activedescendant(如后续扩展键盘导航)
li.id = `${listboxId}-opt-${idx}`;
frag.appendChild(li);
});
listbox.appendChild(frag);
setExpanded(true);
const countText = `${items.length} result${items.length > 1 ? 's' : ''}`;
srStatus.textContent = countText;
};
const cancelOngoing = () => {
if (abortController) {
abortController.abort();
abortController = null;
}
};
// 具体请求函数(支持取消)
const fetchSuggestions = async (query, seq, signal) => {
const url = `${config.endpoint}${encodeURIComponent(query)}`;
const res = await fetch(url, { signal, headers: { 'Accept': 'application/json' } });
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
const data = await res.json();
// 容错:允许 Array<string> 或 Array<object>
const items = Array.isArray(data) ? data : (data.items || data.results || []);
return items.map(config.mapResultItem);
};
// 输入处理(防抖调度)
const scheduleSearch = (query) => {
if (debounceTimer) clearTimeout(debounceTimer);
debounceTimer = setTimeout(async () => {
debounceTimer = null;
const trimmed = query.trim();
if (trimmed.length === 0) {
cancelOngoing();
clearList();
return;
}
if (trimmed.length < config.minLength) {
cancelOngoing();
clearList();
return;
}
// 取消上一次请求
cancelOngoing();
abortController = new AbortController();
const signal = abortController.signal;
const seq = ++requestSeq;
renderLoading();
try {
const items = await fetchSuggestions(trimmed, seq, signal);
// 如果期间又发起了新请求,丢弃旧结果
if (seq !== requestSeq) return;
renderResults(items);
} catch (err) {
if (err.name === 'AbortError') {
// 被主动取消,忽略
return;
}
renderError('Failed to load results');
console.error('[autocomplete] request error:', err);
}
}, config.debounceMs);
};
// 事件处理
const onInput = (e) => {
if (isComposing) return; // 输入法合成中,不触发检索
const value = e.target.value || '';
scheduleSearch(value);
};
const onCompositionStart = () => { isComposing = true; };
const onCompositionEnd = (e) => {
isComposing = false;
// 合成结束后再触发一次
scheduleSearch(e.target.value || '');
};
const onListClick = (e) => {
const option = e.target.closest('[role="option"]');
if (!option) return;
const label = option.textContent || '';
const id = option.dataset.id;
input.value = label;
clearList();
// 向外派发事件,携带选中项数据
const detail = { id, label };
input.dispatchEvent(new CustomEvent('autocomplete:select', { detail, bubbles: true }));
};
const onEscape = (e) => {
if (e.key === 'Escape') {
clearList();
}
};
const onDocumentClick = (e) => {
if (e.target === input) return;
if (listbox.contains(e.target)) return;
clearList();
};
// 绑定监听
input.addEventListener('input', onInput, { passive: true });
input.addEventListener('compositionstart', onCompositionStart);
input.addEventListener('compositionend', onCompositionEnd);
input.addEventListener('keydown', onEscape);
listbox.addEventListener('click', onListClick);
document.addEventListener('click', onDocumentClick);
// 可选:暴露销毁函数(防止内存泄漏,若需要动态卸载)
input.autocompleteDestroy = () => {
cancelOngoing();
if (debounceTimer) clearTimeout(debounceTimer);
input.removeEventListener('input', onInput);
input.removeEventListener('compositionstart', onCompositionStart);
input.removeEventListener('compositionend', onCompositionEnd);
input.removeEventListener('keydown', onEscape);
listbox.removeEventListener('click', onListClick);
document.removeEventListener('click', onDocumentClick);
listbox.remove();
srStatus.remove();
input.removeAttribute('role');
input.removeAttribute('aria-autocomplete');
input.removeAttribute('aria-expanded');
input.removeAttribute('aria-controls');
input.removeAttribute('aria-haspopup');
input.removeAttribute('aria-busy');
};
})();
可选的最简样式建议(非必需):
/* 建议样式(示意) */
.autocomplete-listbox {
position: absolute;
z-index: 1000;
margin-top: 4px;
width: 100%;
max-height: 240px;
overflow: auto;
background: #fff;
border: 1px solid #ddd;
border-radius: 6px;
box-shadow: 0 6px 18px rgba(0,0,0,0.08);
padding: 4px 0;
}
.autocomplete-listbox li {
padding: 8px 12px;
cursor: pointer;
}
.autocomplete-listbox li:hover {
background: #f5f5f5;
}
.autocomplete-loading,
.autocomplete-error {
color: #666;
cursor: default;
}
.sr-only {
position: absolute !important;
width: 1px; height: 1px;
margin: -1px; padding: 0; overflow: hidden;
clip: rect(0 0 0 0); border: 0;
}
内部状态与函数:
<input id="searchInput" type="text" placeholder="Search..." autocomplete="off" />
<script src="/path/to/autocomplete.js"></script>
<script>
// 如需监听选中事件
document.getElementById('searchInput').addEventListener('autocomplete:select', (e) => {
console.log('Selected:', e.detail); // { id, label }
});
</script>
<input id="searchInput" type="text" placeholder="Search..." autocomplete="off" />
<script>
// 替换示例中的 endpoint 和 fetch 函数(简单模拟)
// 方案1:本地服务返回 /api/suggest?q=foo -> ["foo1","foo2",...]
// 方案2:在 Service Worker 或 dev server 做 mock
</script>
// 若接口返回 { items: [{id: 1, name: 'Apple'}, ...] }
// 上面的 mapResultItem 已默认兼容 name/label。若字段更复杂可重写:
// config.mapResultItem = item => ({ id: item.id, label: `${item.name} (${item.category})`, raw: item });
让任何从事前端相关工作的同学,基于少量自然语言与结构化输入,快速生成可直接投入项目的 JavaScript 事件监听方案:- 一次填写,自动产出高质量事件监听代码、详尽注释、使用示例与注意事项,复制即用;- 内置健壮性与可维护性思路(元素校验、合理绑定、边界处理、冲突预防),显著降低返工与线上隐患;- 用统一模板把“需求-代码-说明-示例”打包输出,缩短评审与协作成本;- 覆盖点击、表单、键鼠、滚动、可视化交互等高频场景,既适合新人提效,也能让资深同学把重复工作自动化;- 以可读、可扩展为目标,帮助团队建立一致的编码风格与最佳实践沉淀,提升交付速度与质量。
在复杂页面中快速为按钮、表单、列表等绑定交互,一键生成可靠监听器与回调,减少重复手写,专注业务逻辑。
通过带注释的代码与原理说明学习事件机制,边用边学,快速补齐知识短板,完成可上线的交互开发任务。
在低保真或高保真原型中快速接入可运行的交互脚本,快速验证体验与漏斗假设,缩短评审与迭代周期。
将模板生成的提示词复制粘贴到您常用的 Chat 应用(如 ChatGPT、Claude 等),即可直接对话使用,无需额外开发。适合个人快速体验和轻量使用场景。
把提示词模板转化为 API,您的程序可任意修改模板参数,通过接口直接调用,轻松实现自动化与批量处理。适合开发者集成与业务系统嵌入。
在 MCP client 中配置对应的 server 地址,让您的 AI 应用自动调用提示词模板。适合高级用户和团队协作,让提示词在不同 AI 工具间无缝衔接。
半价获取高级提示词-优惠即将到期