网站右下角弹窗广告是一种常见的广告形式,之前访问网页时经常能看到,直到今天接到产品的需求,要在我们的网站上也加入一个右下角弹窗,折腾了一番成功满足产品需求,完整代码如下:
// popup.js - 只需引入此文件并在顶部修改 CFG 即可
(function () {
// ========== 配置区(只修改这里) ==========
var CFG = {
imageUrl: 'https://www.02405.com/popup.jpg', // 必填:图片 URL
linkUrl: 'https://www.02405.com/, // 必填:点击图片打开的链接(新窗口)
delay: 3000, // 毫秒,页面打开后延迟多少显示弹窗(默认 3000)
width: 400, // PC 端宽度(px)
height: 368, // PC 端高度(px)
once: false, // true => 只显示一次(使用 localStorage 记录)
// 点击图片是否关闭弹窗(默认 false 不关闭)
closeOnImageClick: false,
// 抖动相关(可调)
shakeIntervalMs: 10000, // 每隔多少毫秒触发一次抖动(设为 0 则关闭抖动)
shakesPerBurst: 2, // 每次触发抖几个抖动(两次)
shakeDelayBetweenMs: 350, // 每次抖动之间的间隔(ms)
shakeDurationMs: 300, // 单次抖动动画时长(ms)
shakeTranslatePx: 12 // 抖动位移幅度(像素)
};
// ============================================
// 校验配置(所有 return 均在函数内部,避免顶层 return 错误)
if (!CFG || !CFG.imageUrl || !CFG.linkUrl) {
if (typeof console !== 'undefined') console.warn('popup.js: missing imageUrl or linkUrl in CFG');
return;
}
// 新增:仅在 PC(宽度 > 600px)显示弹窗,移动端不显示
try {
var isMobileViewport = false;
if (typeof window !== 'undefined') {
if (typeof window.matchMedia === 'function') {
// 与 CSS 媒体查询保持一致:移动端规则使用 max-width:600px
isMobileViewport = window.matchMedia('(max-width:600px)').matches;
} else if (typeof window.innerWidth === 'number') {
isMobileViewport = window.innerWidth <= 600;
}
}
if (isMobileViewport) {
if (typeof console !== 'undefined') console.info('popup.js: mobile viewport detected (<=600px) — popup will not be shown on mobile.');
return;
}
} catch (e) {
// 若检测抛错则忽略并继续(默认显示)
}
if (window._sitePopupInjected) return;
window._sitePopupInjected = true;
var storageKey = 'site-popup-shown:' + encodeURIComponent(CFG.imageUrl + '|' + CFG.linkUrl);
try {
if (CFG.once && window.localStorage && localStorage.getItem(storageKey)) {
window._sitePopupInjected = false;
return;
}
} catch (e) {
// localStorage 访问异常则忽略
}
function buildPopup() {
var container = document.createElement('div');
container.id = 'site-popup-container';
container.setAttribute('aria-hidden', 'true');
// 动态生成抖动 keyframes,使用配置的位移和时长
var shakeKeyframes = '\
@keyframes site-popup-shake {\
0% { transform: translateX(0); }\
10% { transform: translateX(-' + CFG.shakeTranslatePx + 'px); }\
30% { transform: translateX(' + CFG.shakeTranslatePx + 'px); }\
50% { transform: translateX(-' + Math.round(CFG.shakeTranslatePx / 2) + 'px); }\
70% { transform: translateX(' + Math.round(CFG.shakeTranslatePx / 2) + 'px); }\
90% { transform: translateX(-' + Math.round(CFG.shakeTranslatePx / 4) + 'px); }\
100% { transform: translateX(0); }\
}';
var css = '\
#site-popup-container{position:fixed;z-index:2147483646;opacity:0;pointer-events:none;transition:opacity .22s ease}\
#site-popup-container.show{opacity:1;pointer-events:auto}\
#site-popup{position:relative;overflow:hidden;border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,.25);background:#fff}\
#site-popup img{display:block;width:100%;height:100%;object-fit:cover;cursor:pointer;border:0;vertical-align:middle}\
#site-popup .close-btn{position:absolute;top:8px;right:8px;width:34px;height:34px;border-radius:50%;background:rgba(0,0,0,.6);color:#fff;border:0;display:flex;align-items:center;justify-content:center;font-size:16px;line-height:1;cursor:pointer}\
/* 抖动类(通过 JS 动态添加/移除) */\
#site-popup.shake { animation-name: site-popup-shake; animation-timing-function: ease-in-out; animation-fill-mode: both; }\
@media (min-width:601px){#site-popup-container{right:20px;bottom:20px;left:auto;top:auto;transform:none}#site-popup{width:' + CFG.width + 'px;height:' + CFG.height + 'px}}\
@media (max-width:600px){#site-popup-container{left:50%;top:50%;transform:translate(-50%,-50%)}#site-popup{width:90vw;max-width:' + CFG.width + 'px;height:auto;border-radius:10px}#site-popup .close-btn{width:30px;height:30px;font-size:14px;top:6px;right:6px}}';
var style = document.createElement('style');
// 合并 shake keyframes 和其余 css
style.textContent = shakeKeyframes + css;
container.appendChild(style);
var box = document.createElement('div');
box.id = 'site-popup';
var img = document.createElement('img');
img.src = CFG.imageUrl;
img.alt = '';
// 点击图片:仅在新窗口打开链接,但不关闭弹窗
img.addEventListener('click', function (e) {
try { window.open(CFG.linkUrl, '_blank', 'noopener,noreferrer'); } catch (err) {}
// 仅在配置明确要求关闭时才关闭(默认 false)
if (CFG.closeOnImageClick) {
closePopup();
}
}, { passive: true });
var btn = document.createElement('button');
btn.className = 'close-btn';
btn.type = 'button';
btn.setAttribute('aria-label', '关闭');
btn.textContent = '✕';
btn.addEventListener('click', function (e) {
e.stopPropagation();
closePopup();
}, false);
box.appendChild(img);
box.appendChild(btn);
container.appendChild(box);
// 控制抖动循环句柄和 state
var shakeIntervalId = null;
var shakeTimeoutIds = [];
// 启动抖动循环(如果启用)
function startShakeLoop() {
if (!CFG.shakeIntervalMs || CFG.shakeIntervalMs <= 0) return;
// 每次触发两次抖动(可配置)
function doBurst() {
for (var i = 0; i < CFG.shakesPerBurst; i++) {
(function (idx) {
var t = setTimeout(function () {
doShakeOnce();
}, idx * CFG.shakeDelayBetweenMs);
shakeTimeoutIds.push(t);
})(i);
}
}
// 延迟第一次触发为 shakeIntervalMs 后(保持周期感),并周期性触发
shakeIntervalId = setInterval(doBurst, CFG.shakeIntervalMs);
// 不立即触发,第一次在 interval 到后触发(可以修改这里让它先抖一次)
}
// 做一次抖动
var shaking = false;
function doShakeOnce() {
if (shaking) return;
shaking = true;
// 设置动画时长
box.style.animationDuration = (CFG.shakeDurationMs || 300) + 'ms';
box.classList.add('shake');
// 移除类在动画结束后
var to = setTimeout(function () {
box.classList.remove('shake');
shaking = false;
}, (CFG.shakeDurationMs || 300) + 40);
shakeTimeoutIds.push(to);
}
// Esc 关闭处理
function onKey(e) { if (e && (e.key === 'Escape' || e.key === 'Esc')) closePopup(); }
window.addEventListener('keydown', onKey);
function closePopup() {
container.classList.remove('show');
container.setAttribute('aria-hidden', 'true');
// 清理抖动定时器
if (shakeIntervalId) {
clearInterval(shakeIntervalId);
shakeIntervalId = null;
}
for (var i = 0; i < shakeTimeoutIds.length; i++) {
try { clearTimeout(shakeTimeoutIds[i]); } catch (e) {}
}
shakeTimeoutIds = [];
setTimeout(function () {
try { if (container.parentNode) container.parentNode.removeChild(container); } catch (e) {}
window._sitePopupInjected = false;
}, 240);
window.removeEventListener('keydown', onKey);
try {
if (CFG.once && window.localStorage) localStorage.setItem(storageKey, '1');
} catch (e) {}
}
return {
el: container,
startShakeLoop: startShakeLoop,
clearShakeTimers: function () {
if (shakeIntervalId) {
clearInterval(shakeIntervalId);
shakeIntervalId = null;
}
for (var i = 0; i < shakeTimeoutIds.length; i++) {
try { clearTimeout(shakeTimeoutIds[i]); } catch (e) {}
}
shakeTimeoutIds = [];
},
show: function () {
document.body.appendChild(container);
// 强制重绘后显示过渡效果
setTimeout(function () {
container.classList.add('show');
container.setAttribute('aria-hidden', 'false');
}, 20);
// 启动抖动循环(如果配置启用)
startShakeLoop();
return closePopup;
}
};
}
// 在 body 可用后延迟显示
function start() {
setTimeout(function () {
if (!document.body) {
document.addEventListener('DOMContentLoaded', function () {
var u = buildPopup();
u.show();
});
} else {
var u = buildPopup();
u.show();
}
}, Number(CFG.delay) || 3000);
}
// 启动
start();
})();





