PC端实现自定义右键菜单与图片"复制图片并加水印"功能的完整实践(兼容H5)
背景
在Web应用开发中,我们常常需要自定义右键菜单,以提升用户体验或实现更高级的交互功能。另一方面,针对图片内容的操作(如加水印并复制)也是内容安全和品牌营销常用的需求。如果要实现一个 兼容PC与H5端的右键自定义菜单 ,并在图片上集成"复制图片(并加水印)"的功能(支持文字/图片水印、可自定义水印位置和尺寸),应该如何设计?本文将从技术分享者的角度,详细介绍实现思路、关键代码和插件化/兼容性优化建议。

一、需求分解与技术选型
- 自定义右键菜单 :原生浏览器右键菜单无法完全控制,需拦截
contextmenu事件,渲染自定义菜单。 - 菜单项"复制图片(并加水印)" :
- 图片渲染至
canvas后添加水印(文字或图片方式均可,位置/尺寸可配置)。 - 将处理后的图片写入剪切板(PC端首选Clipboard API,H5端兼容降级为图片下载等)。
- 图片渲染至
- 兼容性要求:兼容主流桌面浏览器和移动端H5浏览器(触摸设备右键替代方案)。
- 扩展性/优雅性:代码结构应支持插件化,便于自定义菜单样式、扩展新功能。
二、实现流程详解
1. 右键菜单的实现
Step1: 拦截并阻止默认右键菜单
js
document.addEventListener('contextmenu', function(e) {
// 判断目标为目标图片或容器
if (e.target.classList.contains('custom-context-img')) {
e.preventDefault();
showCustomMenu(e);
}
});
Step2: 渲染自定义菜单并定位
js
function showCustomMenu(e) {
// 动态插入菜单DOM(可优化为组件)
const menu = document.createElement('div');
menu.className = 'custom-context-menu';
menu.style.top = `${e.clientY}px`;
menu.style.left = `${e.clientX}px`;
menu.innerHTML = `
<div class="custom-context-item" data-action="copy-watermark">复制图片(并加水印)</div>
<div class="custom-context-item" data-action="download">下载图片</div>
<!-- 其他菜单项 -->
`;
document.body.appendChild(menu);
// 点击菜单项事件
menu.addEventListener('click', (ev) => {
const action = ev.target.dataset.action;
handleMenuAction(action, e.target); // e.target为右击图片
removeCustomMenu();
});
// 点击其他区域关闭菜单
setTimeout(() => {
document.addEventListener('click', removeCustomMenu, { once: true });
}, 0);
}
function removeCustomMenu() {
const menu = document.querySelector('.custom-context-menu');
menu && menu.remove();
}
Step3: 兼容移动端(H5)
移动端无右键,通常可用"长按"事件模拟:
js
let touchTimer = null;
document.addEventListener('touchstart', function(e) {
if (e.target.classList.contains('custom-context-img')) {
touchTimer = setTimeout(() => {
showCustomMenu(e.touches[0]);
}, 600); // 长按600ms触发
}
});
document.addEventListener('touchend', () => { clearTimeout(touchTimer); });
2. "复制图片并加水印"功能详解
Step1: 将图片绘制到Canvas
js
function drawImageToCanvas(img, width, height) {
const canvas = document.createElement('canvas');
canvas.width = width || img.naturalWidth || 300;
canvas.height = height || img.naturalHeight || 150;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
return { canvas, ctx };
}
Step2: 添加文本或图片水印
js
function addTextWatermark(ctx, text, opts) {
ctx.save();
ctx.globalAlpha = opts.opacity ?? 0.5;
ctx.font = `${opts.fontWeight || 'bold'} ${opts.fontSize || 24}px ${opts.fontFamily || 'sans-serif'}`;
ctx.fillStyle = opts.color || 'rgba(0,0,0,0.2)';
ctx.textAlign = opts.align || 'right';
ctx.textBaseline = opts.baseline || 'bottom';
// 支持自定义位置
const x = opts.x || ctx.canvas.width - 20;
const y = opts.y || ctx.canvas.height - 20;
ctx.rotate((opts.angle || 0) * Math.PI / 180);
ctx.fillText(text, x, y);
ctx.restore();
}
function addImageWatermark(ctx, watermarkImg, opts) {
ctx.save();
ctx.globalAlpha = opts.opacity ?? 0.5;
// 尺寸自定义
const w = opts.width || watermarkImg.width;
const h = opts.height || watermarkImg.height;
// 位置自定义
const x = opts.x !== undefined ? opts.x : (ctx.canvas.width - w - 20);
const y = opts.y !== undefined ? opts.y : (ctx.canvas.height - h - 20);
ctx.drawImage(watermarkImg, x, y, w, h);
ctx.restore();
}
Step3: 写入剪切板(PC端)或兼容处理(H5)
PC端 Clipboard API:
js
async function copyCanvasToClipboard(canvas) {
try {
// 仅安全环境支持
canvas.toBlob(async (blob) => {
await navigator.clipboard.write([
new ClipboardItem({ [blob.type]: blob })
]);
alert('已复制到剪贴板!');
});
} catch (err) {
alert('复制失败,请使用图片下载');
}
}
H5端降级:下载保存图片
js
function saveCanvasAsImage(canvas) {
const url = canvas.toDataURL('image/png');
const link = document.createElement('a');
link.href = url;
link.download = 'watermark.png';
link.click();
}
Step4: 菜单动作处理
js
function handleMenuAction(action, imgElement) {
if (action === 'copy-watermark') {
const { canvas, ctx } = drawImageToCanvas(imgElement);
// 示例:加文字水印
addTextWatermark(ctx, 'Demo Watermark', {
x: canvas.width - 120, y: canvas.height - 20,
color: 'rgba(255,0,0,0.3)', fontSize: 28, angle: -15
});
// 示例:加图片水印
/*
const watermarkImg = new Image();
watermarkImg.src = 'url_to_watermark.png';
watermarkImg.onload = () => {
addImageWatermark(ctx, watermarkImg, { x: 10, y: 10, width: 100, height: 60, opacity: 0.5 });
}
*/
// 兼容性处理
if (navigator.clipboard && window.ClipboardItem) {
copyCanvasToClipboard(canvas);
} else {
saveCanvasAsImage(canvas);
}
}
// 其他action: download等
}
三、样式与易用性优化
可用CSS美化菜单外观:
css
.custom-context-menu {
position: fixed;
z-index: 999999;
min-width: 180px;
background: #fff;
box-shadow: 0 2px 10px rgba(0,0,0,0.15);
border-radius: 8px;
overflow: hidden;
font-family: sans-serif;
}
.custom-context-item {
padding: 12px 18px;
cursor: pointer;
transition: background 0.15s;
}
.custom-context-item:hover {
background: #4170ea;
color: #fff;
}
四、兼容性&扩展性说明
- 跨平台兼容:基于DOM事件的右键/长按兼容绝大多数主流浏览器;Clipboard API自动降级到图片下载。
- 水印方式灵活:支持文字/图片任意组合、自定义参数,便于嵌入企业Logo或时效标记。
- 扩展性方案:将菜单渲染逻辑、图片处理逻辑模块解耦,便于后续增加菜单项或迁移到Vue/React等主流前端框架中。
- 安全性提示:部分Clipboard API特性需HTTPS和权限,移动端请适当引导用户下载图片。
五、完整Demo工程&总结
完整Demo代码:
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>PC端自定义右键菜单与图片复制加水印 Demo</title>
<style>
body { font-family: "Helvetica Neue", Helvetica, Arial, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif; background: #f6f8fa; min-height: 100vh; margin: 0; padding: 0; }
.img-container { margin: 32px auto; text-align: center; }
.custom-context-img { border-radius: 10px; box-shadow: 0 4px 12px #0001; margin: 12px 0; max-width: 100%; }
.custom-context-menu {
position: fixed;
z-index: 99999;
min-width: 200px;
background: #fff;
border-radius: 7px;
box-shadow: 0 4px 18px rgba(0,0,0,0.18);
padding: 7px 0;
transition: opacity 0.12s;
user-select: none;
font-family: inherit;
}
.custom-context-menu .custom-context-item {
padding: 11px 22px;
cursor: pointer;
font-size: 16px;
color: #434658;
transition: background 0.12s, color 0.12s;
}
.custom-context-menu .custom-context-item:hover {
background: #007aff;
color: #fff;
}
@media (max-width: 600px) {
.img-container { padding: 12px; }
}
</style>
</head>
<body>
<div class="img-container">
<h2>自定义右键菜单图片演示</h2>
<img
class="custom-context-img"
src="https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=600"
width="420"
alt="演示图片"
>
<br>
<img
class="custom-context-img"
src="https://images.unsplash.com/photo-1465101162946-4377e57745c3?w=600"
width="420"
alt="演示图片2"
>
<p style="color:#888;margin-top:24px;font-size:15px;">支持PC右键,也可移动端长按图片</p>
</div>
<script>
// -- 1. 显示自定义右键菜单 (兼容PC和H5端) --
(function(){
let menuEl;
let longPressTimer = null;
function removeMenu() {
if (menuEl) {
menuEl.remove();
menuEl = null;
}
}
// 主入口:在图片上右键
document.addEventListener('contextmenu', function(e) {
if (e.target.classList.contains('custom-context-img')) {
e.preventDefault();
showMenuAt(e.clientX, e.clientY, e.target);
}
});
// 主入口:长按移动端图片
document.addEventListener('touchstart', function(e) {
const t = e.target;
if (t.classList.contains('custom-context-img')) {
longPressTimer = setTimeout(() => {
const touch = e.touches[0];
showMenuAt(touch.clientX, touch.clientY, t);
}, 660);
}
});
document.addEventListener('touchend', function(){ clearTimeout(longPressTimer); });
document.addEventListener('touchmove', function(){ clearTimeout(longPressTimer); });
// 关闭菜单
document.addEventListener('click', removeMenu);
document.addEventListener('scroll', removeMenu, true);
function showMenuAt(x, y, imgEl) {
removeMenu();
menuEl = document.createElement('div');
menuEl.className = 'custom-context-menu';
menuEl.innerHTML = `
<div class="custom-context-item" data-act="copy-wm">复制图片(并加水印)</div>
<div class="custom-context-item" data-act="download-wm">下载加水印图片</div>
<div class="custom-context-item" data-act="normal-download">直接下载原图片</div>
`;
menuEl.style.top = y + 'px';
menuEl.style.left = x + 'px';
// 溢出窗口自动适配
setTimeout(() => {
const rect = menuEl.getBoundingClientRect();
if (rect.right > window.innerWidth) {
menuEl.style.left = (window.innerWidth - rect.width - 14) + 'px';
}
if (rect.bottom > window.innerHeight) {
menuEl.style.top = (window.innerHeight - rect.height - 8) + 'px';
}
}, 8);
menuEl.onclick = (ev) => {
const act = ev.target.dataset.act;
if (act) {
handleMenuAction(act, imgEl);
removeMenu();
}
ev.stopPropagation();
};
document.body.appendChild(menuEl);
}
// -- 2. 菜单项操作逻辑 --
async function handleMenuAction(action, img) {
if (action === "copy-wm" || action === "download-wm") {
// 加水印
const {canvas} = await drawImageWithWatermark(img, {
type: 'text', // 'text' or 'image'
text: 'Copilot Demo', // 水印内容
color: 'rgba(255,255,255,1)', // 水印颜色
fontSize: 32, // 水印字体大小
angle: 0, // 角度
pos: 'br', // 'br'(右下),'tl'(左上)... 可自定义
opacity: 0.9,
offsetX: 24, offsetY: 24
// 如为图片形式,加 { type:'image', watermarkUrl:..., ...}
});
if (action === "copy-wm") {
if (navigator.clipboard && window.ClipboardItem) {
canvas.toBlob(async (blob) => {
try {
await navigator.clipboard.write([new window.ClipboardItem({ [blob.type]: blob })]);
// 复制成功弹窗预览
showImagePreview(canvas.toDataURL('image/png'), '已复制带水印的图片!你可以Ctrl+V粘贴到聊天窗口或文档里查看。');
} catch {
alert("复制失败,建议使用下载保存方式");
}
}, 'image/png');
} else {
saveCanvas(canvas, 'watermarked.png');
}
}
if (action === "download-wm") {
saveCanvas(canvas, 'watermarked.png');
}
}
if (action === "normal-download") {
const a = document.createElement("a");
a.href = img.src;
a.download = "origin.png";
a.click();
}
}
// -- 3. Canvas 绘制和加水印支持自定义位置/字体等 --
function drawImageWithWatermark(imgEl, options) {
return new Promise((resolve) => {
// 支持跨域图片
const img = new window.Image();
img.crossOrigin = "anonymous";
img.onload = () => {
const w = img.width, h = img.height;
const canvas = document.createElement("canvas");
canvas.width = w; canvas.height = h;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, w, h);
if (options.type === 'text') {
const text = options.text || "Watermark";
ctx.save();
ctx.globalAlpha = options.opacity ?? 0.8;
ctx.font = `bold ${options.fontSize||26}px sans-serif`;
ctx.fillStyle = options.color || "rgba(255,0,0,0.25)";
ctx.textAlign = "right";
ctx.textBaseline = "bottom";
// 计算位置
let x=0, y=0;
const marginX=options.offsetX||20, marginY=options.offsetY||20;
if (options.pos === 'br') { x = w - marginX; y = h - marginY; }
else if (options.pos === 'tr') { x = w - marginX; y = marginY + (options.fontSize||26);}
else if (options.pos === 'tl') { x = marginX; y = (options.fontSize||26) + marginY; }
else if (options.pos === 'bl') { x = marginX; y = h - marginY; }
else { x = w - marginX; y = h - marginY; }
// 旋转角度
if (options.angle) {
ctx.translate(x, y);
ctx.rotate(options.angle * Math.PI / 180);
ctx.translate(-x, -y);
}
ctx.fillText(text, x, y);
ctx.restore();
}
if (options.type === 'image' && options.watermarkUrl) {
const wm = new window.Image();
wm.crossOrigin = "anonymous";
wm.onload = () => {
ctx.save();
ctx.globalAlpha = options.opacity||0.5;
// 尺寸
const wmw = options.wmWidth||Math.floor(w*0.18), wmh = options.wmHeight||Math.floor(h*0.15);
// 位置
const wx = options.offsetX ? options.offsetX : (w - wmw - 18);
const wy = options.offsetY ? options.offsetY : (h - wmh - 16);
ctx.drawImage(wm, wx, wy, wmw, wmh);
ctx.restore();
resolve({canvas, ctx});
};
wm.src = options.watermarkUrl;
return; // 异步resolve
}
resolve({canvas, ctx});
};
img.src = imgEl.src;
});
}
// 保存canvas图片
function saveCanvas(canvas, filename) {
const url = canvas.toDataURL('image/png');
const a = document.createElement("a");
a.href = url;
a.download = filename||"图片.png";
a.click();
}
// 4. 复制成功后弹窗预览图片
function showImagePreview(imgUrl, tip) {
const div = document.createElement('div');
div.className = 'image-preview-mask';
div.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;z-index:999999;background:rgba(0,0,0,0.27);display:flex;flex-direction:column;align-items:center;justify-content:center;';
div.innerHTML = `
<div style="background:#fff;padding:22px 22px 15px 22px;border-radius:8px;box-shadow:0 4px 24px #0002;max-width:92vw;">
<div style="font-size:16px;color:#222;margin-bottom:10px;">${tip||'预览'}</div>
<img src="${imgUrl}" style="max-width:460px;max-height:48vh;box-shadow:0 2px 10px #0001;border-radius:6px;" />
<div style="text-align:center;margin-top:12px;">
<button onclick="this.closest('.image-preview-mask').remove()" style="padding:4px 21px;font-size:15px;background:#007aff;color:#fff;border:none;border-radius:4px;cursor:pointer;">关闭</button>
</div>
</div>
`;
document.body.appendChild(div);
}
})();
</script>
</body>
</html>
总结
本文系统分享了跨端自定义右键菜单与**图片"复制并加水印"**的实现思路与完整代码,解决了常见的兼容性与可扩展性痛点。核心思路为Canvas图片处理+剪贴板API按需降级,并通过优雅的事件解耦和样式设计确保良好用户体验。相关功能不只适用于日常项目,也适合二次封装输出为通用插件,提高团队开发效率。
react组件版本
js
// 组件封装
import React, { useRef, useEffect, useState } from "react";
/**
* WatermarkContextMenu
* 可复用的图片右键自定义菜单 + 水印复制/保存组件
*
* Props:
* - imgSrc: 图片url(必传)
* - render?: 传入自定义图片节点(可选,优先渲染)
* - watermark: {
* type: "text" | "image",
* text?: string,
* color?: string,
* fontSize?: number,
* angle?: number,
* pos?: "br" | "bl" | "tr" | "tl",
* opacity?: number,
* offsetX?: number,
* offsetY?: number,
* imageUrl?: string, // type=image 时的水印图片
* wmWidth?: number,
* wmHeight?: number,
* }
* - menuItems?: 追加自定义菜单项(数组: { label: string, onClick: ()=>void }[])
*/
const menuBase = [
{
id: "copy-wm",
label: "复制图片(并加水印)",
},
{
id: "download-wm",
label: "下载加水印图片",
},
{
id: "normal-download",
label: "直接下载原图片",
},
];
/** 工具方法,将图片绘制到canvas并加水印 */
function drawImageWithWatermark({ img, options }) {
return new Promise((resolve, reject) => {
const maybeLoad = img.complete
? Promise.resolve()
: new Promise((res, rej) => {
img.onload = () => res();
img.onerror = rej;
});
maybeLoad
.then(() => {
const w = img.naturalWidth || img.width,
h = img.naturalHeight || img.height;
const canvas = document.createElement("canvas");
canvas.width = w;
canvas.height = h;
const ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0, w, h);
if (options.type === "text") {
const text = options.text || "Watermark";
ctx.save();
ctx.globalAlpha = options.opacity ?? 0.38;
ctx.font = `bold ${options.fontSize || 26}px sans-serif`;
ctx.fillStyle = options.color || "rgba(255,0,0,0.25)";
ctx.textAlign = "right";
ctx.textBaseline = "bottom";
let x = 0,
y = 0;
const marginX = options.offsetX || 20,
marginY = options.offsetY || 20;
if (options.pos === "br") {
x = w - marginX;
y = h - marginY;
} else if (options.pos === "tr") {
x = w - marginX;
y = marginY + (options.fontSize || 26);
} else if (options.pos === "tl") {
x = marginX;
y = (options.fontSize || 26) + marginY;
} else if (options.pos === "bl") {
x = marginX;
y = h - marginY;
} else {
x = w - marginX;
y = h - marginY;
}
if (options.angle) {
ctx.translate(x, y);
ctx.rotate((options.angle * Math.PI) / 180);
ctx.translate(-x, -y);
}
ctx.fillText(text, x, y);
ctx.restore();
resolve({ canvas, ctx });
} else if (
options.type === "image" &&
options.imageUrl &&
options.imageUrl.length > 0
) {
const wm = new window.Image();
wm.crossOrigin = "anonymous";
wm.onload = () => {
ctx.save();
ctx.globalAlpha = options.opacity || 0.5;
const wmw = options.wmWidth || Math.floor(w * 0.18),
wmh = options.wmHeight || Math.floor(h * 0.15);
const wx =
typeof options.offsetX === "number"
? options.offsetX
: w - wmw - 18;
const wy =
typeof options.offsetY === "number"
? options.offsetY
: h - wmh - 16;
ctx.drawImage(wm, wx, wy, wmw, wmh);
ctx.restore();
resolve({ canvas, ctx });
};
wm.onerror = reject;
wm.src = options.imageUrl;
} else {
resolve({ canvas, ctx });
}
})
.catch(reject);
});
}
function saveCanvas(canvas, filename) {
const url = canvas.toDataURL("image/png");
const a = document.createElement("a");
a.href = url;
a.download = filename || "图片.png";
a.click();
}
/** 复制成功弹窗预览 */
function PreviewModal({ imgUrl, tip, onClose }) {
useEffect(() => {
// 支持键盘 Esc 关闭
function handle(e) {
if (e.key === "Escape") onClose();
}
window.addEventListener("keydown", handle);
return () => window.removeEventListener("keydown", handle);
}, [onClose]);
return (
<div
className="wm-preview-mask"
style={{
position: "fixed",
zIndex: 999999,
left: 0,
top: 0,
right: 0,
bottom: 0,
background: "rgba(0,0,0,0.27)",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
}}
tabIndex={-1}
onClick={onClose}
>
<div
style={{
background: "#fff",
padding: 22,
borderRadius: 8,
boxShadow: "0 4px 24px #0002",
maxWidth: "92vw",
}}
onClick={e => e.stopPropagation()}
>
<div
style={{
fontSize: 16,
color: "#222",
marginBottom: 10,
textAlign: "center",
}}
>
{tip || "预览"}
</div>
<img
src={imgUrl}
alt="预览"
style={{
maxWidth: 460,
maxHeight: "48vh",
boxShadow: "0 2px 10px #0001",
borderRadius: 6,
}}
/>
<div style={{ textAlign: "center", marginTop: 12 }}>
<button
onClick={onClose}
style={{
padding: "4px 21px",
fontSize: 15,
background: "#007aff",
color: "#fff",
border: "none",
borderRadius: 4,
cursor: "pointer",
}}
>
关闭
</button>
</div>
</div>
</div>
);
}
/** 主组件 */
export default function WatermarkContextMenu({
imgSrc,
render,
watermark = {
type: "text",
text: "Copilot Demo",
color: "rgba(0,0,0,0.25)",
fontSize: 32,
angle: -22,
pos: "br",
opacity: 0.55,
offsetX: 24,
offsetY: 24,
},
menuItems = [],
}) {
const imgRef = useRef();
const menuRef = useRef();
const [menu, setMenu] = useState(null); // {x, y, visible}
const [preview, setPreview] = useState(null); // {imgUrl, tip}
// 弹菜单
function openMenu(e) {
e.preventDefault?.();
const x = e.clientX || (e.touches && e.touches[0].clientX) || 100;
const y = e.clientY || (e.touches && e.touches[0].clientY) || 100;
setMenu({ x, y });
}
useEffect(() => {
if (!menu) return;
function close() {
setMenu(null);
}
window.addEventListener("click", close);
window.addEventListener("scroll", close, true);
return () => {
window.removeEventListener("click", close);
window.removeEventListener("scroll", close, true);
};
}, [menu]);
// 长按兼容H5
useEffect(() => {
let timer;
const node = imgRef.current;
if (!node) return;
function onTouchStart(e) {
timer = setTimeout(() => {
openMenu(e);
}, 660);
}
function clear() {
clearTimeout(timer);
}
node.addEventListener("touchstart", onTouchStart);
node.addEventListener("touchend", clear);
node.addEventListener("touchmove", clear);
return () => {
node.removeEventListener("touchstart", onTouchStart);
node.removeEventListener("touchend", clear);
node.removeEventListener("touchmove", clear);
};
}, []);
async function doMenuAction(id) {
setMenu(null);
if (id === "copy-wm" || id === "download-wm") {
const img = imgRef.current;
try {
const { canvas } = await drawImageWithWatermark({
img,
options: watermark,
});
if (id === "copy-wm") {
if (navigator.clipboard && window.ClipboardItem) {
canvas.toBlob(async (blob) => {
try {
await navigator.clipboard.write([
new window.ClipboardItem({ [blob.type]: blob }),
]);
setPreview({
imgUrl: canvas.toDataURL("image/png"),
tip:
"已复制带水印的图片!你可以Ctrl+V粘贴到聊天窗口或文档里验证。",
});
} catch {
saveCanvas(canvas, "watermarked.png");
}
}, "image/png");
} else {
saveCanvas(canvas, "watermarked.png");
}
} else if (id === "download-wm") {
saveCanvas(canvas, "watermarked.png");
}
} catch (err) {
alert("处理失败: " + err);
}
} else if (id === "normal-download") {
const img = imgRef.current;
const a = document.createElement("a");
a.href = img.src;
a.download = "origin.png";
a.click();
}
}
// 右键菜单样式
const menuStyle =
menu &&
{
position: "fixed",
zIndex: 99999,
minWidth: 200,
background: "#fff",
borderRadius: 7,
boxShadow: "0 4px 18px rgba(0,0,0,0.18)",
padding: "7px 0",
userSelect: "none",
fontFamily: "inherit",
left: menu.x,
top: menu.y,
};
return (
<>
<span
style={{ display: "inline-block", cursor: "pointer", position: "relative" }}
onContextMenu={openMenu}
tabIndex={0}
>
{render ? (
// 用户自定义子节点,自动绑定ref
React.cloneElement(render, { ref: imgRef })
) : (
<img
ref={imgRef}
className="custom-context-img"
src={imgSrc}
alt="可自定义右键菜单图片"
style={{
borderRadius: 10,
boxShadow: "0 4px 12px #0001",
margin: "12px 0",
maxWidth: "100%",
cursor: "pointer",
}}
/>
)}
{/* 菜单 */}
{menu && (
<div ref={menuRef} style={menuStyle}>
{menuBase.map((item) => (
<div
key={item.id}
style={{
padding: "11px 22px",
fontSize: 16,
color: "#434658",
cursor: "pointer",
transition: "background 0.12s, color 0.12s",
borderRadius: 3,
marginBottom: 2,
}}
onClick={() => doMenuAction(item.id)}
onTouchEnd={e => { e.preventDefault(); doMenuAction(item.id); }}
>
{item.label}
</div>
))}
{menuItems.map((item, i) => (
<div
key={`cus-${i}`}
style={{
padding: "11px 22px",
fontSize: 16,
color: "#434658",
cursor: "pointer",
borderRadius: 3,
transition: "background 0.12s, color 0.12s",
}}
onClick={() => {
setMenu(null);
item.onClick && item.onClick();
}}
onTouchEnd={e => {
e.preventDefault();
setMenu(null);
item.onClick && item.onClick();
}}
>
{item.label}
</div>
))}
</div>
)}
</span>
{/* 复制/保存弹窗 */}
{preview && (
<PreviewModal
imgUrl={preview.imgUrl}
tip={preview.tip}
onClose={() => setPreview(null)}
/>
)}
</>
);
}
js
// 组件使用示例
import React from "react";
import WatermarkContextMenu from "./WatermarkContextMenu";
export default function App() {
return (
<div style={{ textAlign: "center", margin: 40 }}>
<h2>自定义右键菜单图片演示 - React 组件</h2>
<WatermarkContextMenu
imgSrc="https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=600"
watermark={{
type: "text",
text: "Copilot Demo",
color: "rgba(255,0,0,0.22)",
fontSize: 34,
angle: -20,
pos: "br",
opacity: 0.58,
offsetX: 22,
offsetY: 22,
}}
menuItems={[
{
label: "自定义菜单动作",
onClick: () => alert("你点击了自定义菜单~"),
},
]}
/>
<br />
<WatermarkContextMenu
imgSrc="https://images.unsplash.com/photo-1465101162946-4377e57745c3?w=600"
/>
<p style={{ color: "#999" }}>
支持PC右键,也可移动端长按图片弹出菜单
</p>
</div>
);
}