Skip to content

Commit ac1c8c2

Browse files
committed
✨ feat: 在提示词选择器中添加复制功能
1 parent 6b9ba1f commit ac1c8c2

File tree

5 files changed

+151
-13
lines changed

5 files changed

+151
-13
lines changed

entrypoints/content/components/PromptSelector.tsx

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { PromptItemWithVariables, EditableElement, Category } from "@/utils
44
import { getPromptSelectorStyles } from "../utils/styles";
55
import { extractVariables } from "../utils/variableParser";
66
import { showVariableInput } from "./VariableInput";
7-
import { isDarkMode } from "@/utils/tools";
7+
import { isDarkMode, getCopyShortcutText } from "@/utils/tools";
88
import { getCategories } from "@/utils/categoryUtils";
99
import { getGlobalSetting } from "@/utils/globalSettings";
1010
import { t } from "@/utils/i18n";
@@ -28,6 +28,7 @@ const PromptSelector: React.FC<PromptSelectorProps> = ({
2828
const [selectedCategoryId, setSelectedCategoryId] = useState<string | null>(null);
2929
const [categoriesMap, setCategoriesMap] = useState<Record<string, Category>>({});
3030
const [closeOnOutsideClick, setCloseOnOutsideClick] = useState(true);
31+
const [copiedId, setCopiedId] = useState<string | null>(null);
3132
const searchInputRef = useRef<HTMLInputElement>(null);
3233
const listRef = useRef<HTMLDivElement>(null);
3334
const modalRef = useRef<HTMLDivElement>(null);
@@ -149,6 +150,20 @@ const PromptSelector: React.FC<PromptSelectorProps> = ({
149150
setSelectedCategoryId(allOptions[nextIndex]);
150151
};
151152

153+
// 复制提示词内容
154+
const copyPrompt = async (e: React.MouseEvent, prompt: PromptItemWithVariables) => {
155+
e.stopPropagation(); // 阻止事件冒泡,避免触发选择提示词
156+
try {
157+
await navigator.clipboard.writeText(prompt.content);
158+
setCopiedId(prompt.id);
159+
setTimeout(() => {
160+
setCopiedId(null);
161+
}, 2000); // 2秒后清除复制状态
162+
} catch (err) {
163+
console.error(t('copyFailed'), err);
164+
}
165+
};
166+
152167
// 键盘导航
153168
useEffect(() => {
154169
const handleKeyDown = (e: KeyboardEvent) => {
@@ -181,6 +196,18 @@ const PromptSelector: React.FC<PromptSelectorProps> = ({
181196
// Tab键循环切换分类
182197
cycleCategorySelection(e.shiftKey ? 'prev' : 'next');
183198
break;
199+
case "c":
200+
// Ctrl+C (Windows) 或 Command+C (Mac) 复制当前选中的提示词
201+
if ((e.ctrlKey || e.metaKey) && filteredPrompts[selectedIndex]) {
202+
e.preventDefault();
203+
navigator.clipboard.writeText(filteredPrompts[selectedIndex].content)
204+
.then(() => {
205+
setCopiedId(filteredPrompts[selectedIndex].id);
206+
setTimeout(() => setCopiedId(null), 2000);
207+
})
208+
.catch(err => console.error(t('copyFailed'), err));
209+
}
210+
break;
184211
}
185212
};
186213

@@ -482,7 +509,25 @@ const PromptSelector: React.FC<PromptSelectorProps> = ({
482509
onClick={() => applyPrompt(prompt)}
483510
onMouseEnter={() => !isKeyboardNav && setSelectedIndex(index)}
484511
>
485-
<div className="qp-prompt-title">{prompt.title}</div>
512+
<div className="qp-flex qp-justify-between qp-items-center">
513+
<div className="qp-prompt-title">{prompt.title}</div>
514+
<button
515+
className={`qp-copy-button ${copiedId === prompt.id ? 'qp-copied' : ''}`}
516+
onClick={(e) => copyPrompt(e, prompt)}
517+
title={t('copyPrompt')}
518+
>
519+
{copiedId === prompt.id ? (
520+
<svg className="qp-copy-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
521+
<path d="M20 6L9 17L4 12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
522+
</svg>
523+
) : (
524+
<svg className="qp-copy-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
525+
<path d="M8 4v12a2 2 0 002 2h8a2 2 0 002-2V7.242a2 2 0 00-.602-1.43L16.083 2.57A2 2 0 0014.685 2H10a2 2 0 00-2 2z" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
526+
<path d="M16 2v3a2 2 0 002 2h3M4 8v12a2 2 0 002 2h8" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
527+
</svg>
528+
)}
529+
</button>
530+
</div>
486531
<div className="qp-prompt-preview">{prompt.content}</div>
487532
<div className="qp-prompt-meta">
488533
{category && (
@@ -543,7 +588,7 @@ const PromptSelector: React.FC<PromptSelectorProps> = ({
543588

544589
<div className="qp-modal-footer">
545590
<span>{t('totalPrompts2', [filteredPrompts.length.toString()])}</span>
546-
<span>{t('navigationHelp')}</span>
591+
<span>{t('pressCtrlCToCopy', [getCopyShortcutText()])}{t('navigationHelp')}</span>
547592
</div>
548593
</div>
549594
</div>

entrypoints/content/utils/styles.ts

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,20 @@ export function getPromptSelectorStyles(): string {
1919
--qp-bg-secondary: #f9fafb;
2020
--qp-bg-hover: #f8f5ff;
2121
--qp-bg-selected: #f3ecff;
22-
--qp-bg-tag: #eee8ff;
22+
--qp-bg-tag: #f3f4f6;
2323
--qp-text-primary: #111827;
24-
--qp-text-secondary: #6b7280;
25-
--qp-text-tag: #5b46a8;
24+
--qp-text-secondary: #4b5563;
25+
--qp-text-tag: #6b7280;
2626
--qp-border-color: #e5e7eb;
2727
--qp-focus-ring: #9d85f2;
2828
--qp-shadow-color: rgba(124, 58, 237, 0.06);
2929
--qp-green: #10b981;
30-
--qp-accent: #8674e2;
30+
--qp-accent: #6366f1;
3131
--qp-accent-light: #a495eb;
3232
--qp-gradient-start: #9f87f0;
3333
--qp-gradient-end: #8674e2;
34+
--qp-accent-hover: #4f46e5;
35+
--qp-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
3436
}
3537
3638
/* 暗黑模式变量 */
@@ -40,18 +42,19 @@ export function getPromptSelectorStyles(): string {
4042
--qp-bg-secondary: #111827;
4143
--qp-bg-hover: #2c2967;
4244
--qp-bg-selected: #3b348c;
43-
--qp-bg-tag: #2f2c6e;
45+
--qp-bg-tag: #374151;
4446
--qp-text-primary: #f9fafb;
4547
--qp-text-secondary: #9ca3af;
46-
--qp-text-tag: #c7bdfa;
48+
--qp-text-tag: #d1d5db;
4749
--qp-border-color: #374151;
4850
--qp-focus-ring: #9d85f2;
4951
--qp-shadow-color: rgba(124, 58, 237, 0.12);
5052
--qp-green: #34d399;
51-
--qp-accent: #9d85f2;
52-
--qp-accent-light: #bbadf7;
53+
--qp-accent: #6366f1;
54+
--qp-accent-hover: #818cf8;
5355
--qp-gradient-start: #7e63e3;
5456
--qp-gradient-end: #6055c5;
57+
--qp-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.18);
5558
}
5659
5760
/* 移植原来的样式 */
@@ -81,6 +84,10 @@ export function getPromptSelectorStyles(): string {
8184
.qp-justify-center {
8285
justify-content: center !important;
8386
}
87+
88+
.qp-justify-between {
89+
justify-content: space-between !important;
90+
}
8491
8592
.qp-z-50 {
8693
z-index: 2147483647 !important;
@@ -576,5 +583,33 @@ export function getPromptSelectorStyles(): string {
576583
font-weight: 500 !important;
577584
white-space: nowrap !important;
578585
}
586+
587+
/* 复制按钮样式 */
588+
.qp-copy-button {
589+
display: flex !important;
590+
align-items: center !important;
591+
justify-content: center !important;
592+
padding: 6px !important;
593+
border-radius: 6px !important;
594+
background: transparent !important;
595+
border: none !important;
596+
cursor: pointer !important;
597+
transition: all 0.2s ease !important;
598+
color: var(--qp-text-secondary) !important;
599+
}
600+
601+
.qp-copy-button:hover {
602+
background: var(--qp-bg-tag) !important;
603+
color: var(--qp-text-primary) !important;
604+
}
605+
606+
.qp-copy-button.qp-copied {
607+
color: #10B981 !important;
608+
}
609+
610+
.qp-copy-icon {
611+
width: 18px !important;
612+
height: 18px !important;
613+
}
579614
`;
580615
}

public/_locales/en/messages.json

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -762,7 +762,7 @@
762762
}
763763
},
764764
"navigationHelp": {
765-
"message": "↑↓ Navigate · Enter Select · Tab Switch Category"
765+
"message": "↑↓ Navigate · Tab Switch Category · Enter Select"
766766
},
767767
"contentScriptLoaded": {
768768
"message": "Content script (WXT): Loaded"
@@ -1157,5 +1157,19 @@
11571157
},
11581158
"ctrlEnterToConfirm": {
11591159
"message": "Ctrl+Enter to confirm"
1160+
},
1161+
"copyPrompt": {
1162+
"message": "Copy prompt"
1163+
},
1164+
"copyFailed": {
1165+
"message": "Failed to copy prompt"
1166+
},
1167+
"pressCtrlCToCopy": {
1168+
"message": "$shortcut$ to copy",
1169+
"placeholders": {
1170+
"shortcut": {
1171+
"content": "$1"
1172+
}
1173+
}
11601174
}
11611175
}

public/_locales/zh/messages.json

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -765,7 +765,7 @@
765765
}
766766
},
767767
"navigationHelp": {
768-
"message": "↑↓ 导航 · Enter 选择 · Tab 切换分类"
768+
"message": "↑↓ 导航 · Tab 切换分类 · Enter 选择"
769769
},
770770
"contentScriptLoaded": {
771771
"message": "内容脚本 (WXT): 已加载"
@@ -1157,5 +1157,19 @@
11571157
},
11581158
"ctrlEnterToConfirm": {
11591159
"message": "Ctrl+Enter 确认"
1160+
},
1161+
"copyPrompt": {
1162+
"message": "复制提示词"
1163+
},
1164+
"copyFailed": {
1165+
"message": "复制提示词失败"
1166+
},
1167+
"pressCtrlCToCopy": {
1168+
"message": "$shortcut$ 复制",
1169+
"placeholders": {
1170+
"shortcut": {
1171+
"content": "$1"
1172+
}
1173+
}
11601174
}
11611175
}

utils/tools.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,33 @@ export const isDarkMode = () => {
55
window.matchMedia("(prefers-color-scheme: dark)").matches
66
);
77
};
8+
9+
// 检测操作系统类型
10+
export const getOS = () => {
11+
const platform = navigator.platform.toLowerCase();
12+
const userAgent = navigator.userAgent.toLowerCase();
13+
14+
if (platform.includes('mac') || userAgent.includes('mac')) {
15+
return 'mac';
16+
} else if (platform.includes('win') || userAgent.includes('win')) {
17+
return 'windows';
18+
} else if (platform.includes('linux') || userAgent.includes('linux')) {
19+
return 'linux';
20+
} else {
21+
return 'unknown';
22+
}
23+
};
24+
25+
// 获取复制快捷键文本
26+
export const getCopyShortcutText = () => {
27+
const os = getOS();
28+
switch (os) {
29+
case 'mac':
30+
return '⌘+C';
31+
case 'windows':
32+
case 'linux':
33+
return 'Ctrl+C';
34+
default:
35+
return 'Ctrl+C';
36+
}
37+
};

0 commit comments

Comments
 (0)