feat(qrcode): 实现图片转二维码右键菜单功能
- 添加图片类型的右键菜单配置 - 修改 parseContextMenuClick 支持 srcUrl 参数 - 实现图片URL自动解析功能 - 检测图片URL自动切换到解析模式 - 更新测试用例覆盖新功能 - 新增国际化文案
This commit is contained in:
@@ -616,6 +616,10 @@
|
|||||||
"message": "解析结果将显示在此处",
|
"message": "解析结果将显示在此处",
|
||||||
"description": "Translation key: qrCode_resultPlaceholder"
|
"description": "Translation key: qrCode_resultPlaceholder"
|
||||||
},
|
},
|
||||||
|
"qrCode_imageToQr": {
|
||||||
|
"message": "图片转二维码",
|
||||||
|
"description": "Translation key: qrCode_imageToQr"
|
||||||
|
},
|
||||||
"rightClickRestorer_loading": {
|
"rightClickRestorer_loading": {
|
||||||
"message": "正在加载...",
|
"message": "正在加载...",
|
||||||
"description": "Translation key: rightClickRestorer_loading"
|
"description": "Translation key: rightClickRestorer_loading"
|
||||||
|
|||||||
@@ -25,6 +25,13 @@ function isUrl(text: string): boolean {
|
|||||||
return domainPattern.test(trimmed);
|
return domainPattern.test(trimmed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 检测URL是否为图片格式 */
|
||||||
|
function isImageUrl(url: string): boolean {
|
||||||
|
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg', '.ico'];
|
||||||
|
const lowerUrl = url.toLowerCase();
|
||||||
|
return imageExtensions.some((ext) => lowerUrl.includes(ext));
|
||||||
|
}
|
||||||
|
|
||||||
/** 生成二维码的核心逻辑 */
|
/** 生成二维码的核心逻辑 */
|
||||||
function generateQrCodeDataUrl(text: string): string {
|
function generateQrCodeDataUrl(text: string): string {
|
||||||
const trimmedText = text.trim();
|
const trimmedText = text.trim();
|
||||||
@@ -178,37 +185,90 @@ export function useQrCode(): QrCodeContextValue {
|
|||||||
}));
|
}));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
/** 右键菜单传入URL时,自动生成二维码 */
|
/** 从图片URL解析二维码 */
|
||||||
const handleContextMenuData = useCallback((payload: string) => {
|
const parseQrCodeFromUrl = useCallback(
|
||||||
setMode('generate');
|
async (imageUrl: string) => {
|
||||||
|
try {
|
||||||
|
setParserState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
parsing: true,
|
||||||
|
parseError: '',
|
||||||
|
decodedResult: '',
|
||||||
|
previewUrl: imageUrl,
|
||||||
|
selectedFile: null,
|
||||||
|
}));
|
||||||
|
|
||||||
// 直接生成二维码,无需等待
|
// 从URL获取图片并转换为File对象
|
||||||
const qrCodeDataUrl = generateQrCodeDataUrl(payload);
|
const response = await fetch(imageUrl);
|
||||||
|
const blob = await response.blob();
|
||||||
|
const file = new File([blob], 'qrcode-image.png', { type: blob.type });
|
||||||
|
|
||||||
if (qrCodeDataUrl) {
|
setParserState((prev) => ({ ...prev, selectedFile: file }));
|
||||||
// 生成成功,直接跳转到预览态
|
|
||||||
setGeneratorState((prev) => ({
|
const result = await parseQrCodeFromFile(file);
|
||||||
...prev,
|
|
||||||
step: 'preview',
|
if (result.success && result.data) {
|
||||||
textToEncode: payload,
|
setParserState((prev) => ({ ...prev, decodedResult: result.data! }));
|
||||||
savedText: payload.trim(),
|
toast.success(t('qrCode:parseSuccess'));
|
||||||
qrCodeDataUrl,
|
} else {
|
||||||
generating: false,
|
const errorMsg = result.error || t('qrCode:noQrDetected');
|
||||||
inputError: '',
|
setParserState((prev) => ({ ...prev, parseError: errorMsg }));
|
||||||
}));
|
toast.error(errorMsg);
|
||||||
} else {
|
}
|
||||||
// 生成失败,停留在输入态,显示文本供用户编辑
|
} catch (error) {
|
||||||
setGeneratorState((prev) => ({
|
console.error('解析图片二维码失败:', error);
|
||||||
...prev,
|
const errorMsg = error instanceof Error ? error.message : t('qrCode:parseError');
|
||||||
step: 'input',
|
setParserState((prev) => ({ ...prev, parseError: errorMsg }));
|
||||||
textToEncode: payload,
|
toast.error(errorMsg);
|
||||||
savedText: '',
|
} finally {
|
||||||
qrCodeDataUrl: '',
|
setParserState((prev) => ({ ...prev, parsing: false }));
|
||||||
generating: false,
|
}
|
||||||
inputError: '',
|
},
|
||||||
}));
|
[t],
|
||||||
}
|
);
|
||||||
}, []);
|
|
||||||
|
/** 右键菜单传入URL时,自动生成二维码或解析图片 */
|
||||||
|
const handleContextMenuData = useCallback(
|
||||||
|
(payload: string) => {
|
||||||
|
// 检测是否为图片URL,如果是则切换到解析模式
|
||||||
|
if (isImageUrl(payload)) {
|
||||||
|
setMode('parse');
|
||||||
|
void parseQrCodeFromUrl(payload);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 非图片URL,生成二维码
|
||||||
|
setMode('generate');
|
||||||
|
|
||||||
|
// 直接生成二维码,无需等待
|
||||||
|
const qrCodeDataUrl = generateQrCodeDataUrl(payload);
|
||||||
|
|
||||||
|
if (qrCodeDataUrl) {
|
||||||
|
// 生成成功,直接跳转到预览态
|
||||||
|
setGeneratorState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
step: 'preview',
|
||||||
|
textToEncode: payload,
|
||||||
|
savedText: payload.trim(),
|
||||||
|
qrCodeDataUrl,
|
||||||
|
generating: false,
|
||||||
|
inputError: '',
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
// 生成失败,停留在输入态,显示文本供用户编辑
|
||||||
|
setGeneratorState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
step: 'input',
|
||||||
|
textToEncode: payload,
|
||||||
|
savedText: '',
|
||||||
|
qrCodeDataUrl: '',
|
||||||
|
generating: false,
|
||||||
|
inputError: '',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[parseQrCodeFromUrl],
|
||||||
|
);
|
||||||
|
|
||||||
useContextMenuData({ featureKey: 'qrCode', onData: handleContextMenuData });
|
useContextMenuData({ featureKey: 'qrCode', onData: handleContextMenuData });
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ describe('contextMenu', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('CONTEXT_MENU_CONFIGS', () => {
|
describe('CONTEXT_MENU_CONFIGS', () => {
|
||||||
it('应该包含 7 个菜单项配置', () => {
|
it('应该包含 8 个菜单项配置', () => {
|
||||||
expect(CONTEXT_MENU_CONFIGS).toHaveLength(7);
|
expect(CONTEXT_MENU_CONFIGS).toHaveLength(8);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('应该有一个父级菜单项 Testing Tools', () => {
|
it('应该有一个父级菜单项 Testing Tools', () => {
|
||||||
@@ -43,13 +43,22 @@ describe('contextMenu', () => {
|
|||||||
expect(pageMenus).toHaveLength(2);
|
expect(pageMenus).toHaveLength(2);
|
||||||
expect(pageMenus.map((m) => m.id)).toEqual(['storageCleaner', 'qrCode-page']);
|
expect(pageMenus.map((m) => m.id)).toEqual(['storageCleaner', 'qrCode-page']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('应该有 1 个 image 上下文的子菜单', () => {
|
||||||
|
const imageMenus = CONTEXT_MENU_CONFIGS.filter(
|
||||||
|
(c) => c.contexts[0] === 'image' && c.parentId === 'testing-tools-parent',
|
||||||
|
);
|
||||||
|
expect(imageMenus).toHaveLength(1);
|
||||||
|
expect(imageMenus[0].id).toBe('qrCode-image');
|
||||||
|
expect(imageMenus[0].title).toBe('🖼️ 图片转二维码');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('createAllContextMenus', () => {
|
describe('createAllContextMenus', () => {
|
||||||
it('应该为每个配置调用 chrome.contextMenus.create', () => {
|
it('应该为每个配置调用 chrome.contextMenus.create', () => {
|
||||||
createAllContextMenus();
|
createAllContextMenus();
|
||||||
|
|
||||||
expect(chrome.contextMenus.create).toHaveBeenCalledTimes(7);
|
expect(chrome.contextMenus.create).toHaveBeenCalledTimes(8);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('应该使用正确的参数创建菜单项', () => {
|
it('应该使用正确的参数创建菜单项', () => {
|
||||||
@@ -185,5 +194,33 @@ describe('contextMenu', () => {
|
|||||||
data: { featureKey: 'textStatistics', payload: 'short text' },
|
data: { featureKey: 'textStatistics', payload: 'short text' },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('当点击 qrCode-image 菜单时应返回 qrCode 功能和图片URL', () => {
|
||||||
|
const info = createMockOnClickData({
|
||||||
|
srcUrl: 'https://example.com/image.png',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = parseContextMenuClick('qrCode-image', info);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: true,
|
||||||
|
data: { featureKey: 'qrCode', payload: 'https://example.com/image.png' },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('srcUrl 优先于 selectionText 和 pageUrl', () => {
|
||||||
|
const info = createMockOnClickData({
|
||||||
|
srcUrl: 'https://example.com/image.png',
|
||||||
|
selectionText: 'selected text',
|
||||||
|
pageUrl: 'https://example.com',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = parseContextMenuClick('qrCode-image', info);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: true,
|
||||||
|
data: { featureKey: 'qrCode', payload: 'https://example.com/image.png' },
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export const MAX_PAYLOAD_LENGTH = 10000;
|
|||||||
/** 菜单项 ID 到 PageType 的映射(仅处理非常规映射) */
|
/** 菜单项 ID 到 PageType 的映射(仅处理非常规映射) */
|
||||||
const MENU_ID_TO_PAGE_TYPE: Record<string, PageType> = {
|
const MENU_ID_TO_PAGE_TYPE: Record<string, PageType> = {
|
||||||
'qrCode-page': 'qrCode',
|
'qrCode-page': 'qrCode',
|
||||||
|
'qrCode-image': 'qrCode',
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -77,6 +78,12 @@ export const CONTEXT_MENU_CONFIGS: ContextMenuItemConfig[] = [
|
|||||||
contexts: [chrome.contextMenus.ContextType.PAGE],
|
contexts: [chrome.contextMenus.ContextType.PAGE],
|
||||||
parentId: PARENT_MENU_ID,
|
parentId: PARENT_MENU_ID,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'qrCode-image',
|
||||||
|
title: '🖼️ 图片转二维码',
|
||||||
|
contexts: [chrome.contextMenus.ContextType.IMAGE],
|
||||||
|
parentId: PARENT_MENU_ID,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function createAllContextMenus(): void {
|
export function createAllContextMenus(): void {
|
||||||
@@ -96,6 +103,14 @@ export function parseContextMenuClick(
|
|||||||
): ParseResult {
|
): ParseResult {
|
||||||
const featureKey = getMenuPageType(menuItemId);
|
const featureKey = getMenuPageType(menuItemId);
|
||||||
|
|
||||||
|
// 处理图片 URL(右键点击图片时)
|
||||||
|
if (info.srcUrl) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: { featureKey, payload: info.srcUrl },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (info.selectionText) {
|
if (info.selectionText) {
|
||||||
const text = info.selectionText;
|
const text = info.selectionText;
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import { defineWebExtConfig } from 'wxt';
|
import { defineWebExtConfig } from 'wxt';
|
||||||
|
|
||||||
export default defineWebExtConfig({
|
export default defineWebExtConfig({
|
||||||
startUrls: ['https://www.baidu.com', 'chrome://extensions/'],
|
startUrls: ['https://www.bing.com', 'chrome://extensions/'],
|
||||||
chromiumArgs: ['chrome://extensions/'],
|
chromiumArgs: ['chrome://extensions/'],
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user