feat(md-deck): Add fallback copy dialog for clipboard failure

This commit is contained in:
hyper
2026-08-07 12:13:40 +08:00
parent e3daac3080
commit 9e081891df
2 changed files with 94 additions and 31 deletions
+86 -27
View File
@@ -1,3 +1,5 @@
import { Show, createSignal, onMount } from "solid-js";
import { Portal } from "solid-js/web";
import type { DeckStore } from "./hooks/deckStore"; import type { DeckStore } from "./hooks/deckStore";
export interface DeckHeaderProps { export interface DeckHeaderProps {
@@ -9,36 +11,93 @@ export interface DeckHeaderProps {
*/ */
export function DeckHeader(props: DeckHeaderProps) { export function DeckHeader(props: DeckHeaderProps) {
const { store } = props; const { store } = props;
const [showCopyFallback, setShowCopyFallback] = createSignal(false);
const [fallbackCode, setFallbackCode] = createSignal("");
let textareaRef: HTMLTextAreaElement | undefined;
onMount(() => {
if (showCopyFallback() && textareaRef) {
textareaRef.focus();
textareaRef.select();
}
});
const handleCopy = () => {
store.actions.copyCode((code) => {
setFallbackCode(code);
setShowCopyFallback(true);
});
};
const closeFallback = () => setShowCopyFallback(false);
return ( return (
<div class="flex items-center gap-2 border-b border-gray-200 pb-2 mb-4"> <>
{/* 编辑按钮 */} <div class="flex items-center gap-2 border-b border-gray-200 pb-2 mb-4">
<button {/* 编辑按钮 */}
onClick={() => store.actions.setIsEditing(!store.state.isEditing)} <button
class={`px-3 py-1 rounded text-sm font-medium transition-colors cursor-pointer ${ onClick={() => store.actions.setIsEditing(!store.state.isEditing)}
store.state.isEditing && !store.state.fixed class={`px-3 py-1 rounded text-sm font-medium transition-colors cursor-pointer ${
? "bg-blue-100 text-blue-600" store.state.isEditing && !store.state.fixed
: "bg-gray-100 text-gray-600 hover:bg-gray-200" ? "bg-blue-100 text-blue-600"
}`} : "bg-gray-100 text-gray-600 hover:bg-gray-200"
> }`}
{store.state.isEditing ? "✓ 编辑中" : "✏️ 编辑"} >
</button> {store.state.isEditing ? "✓ 编辑中" : "✏️ 编辑"}
</button>
{/* 导出 PDF 按钮 */} {/* 导出 PDF 按钮 */}
<button <button
onClick={() => store.actions.exportDeck()} onClick={() => store.actions.exportDeck()}
class="px-2 py-1 rounded text-xs font-medium transition-colors cursor-pointer bg-green-100 text-green-600 hover:bg-green-200" class="px-2 py-1 rounded text-xs font-medium transition-colors cursor-pointer bg-green-100 text-green-600 hover:bg-green-200"
> >
📥 PDF 📥 PDF
</button> </button>
{/* 复制代码按钮 */} {/* 复制代码按钮 */}
<button <button
onClick={() => store.actions.copyCode()} onClick={handleCopy}
class="px-2 py-1 rounded text-xs font-medium transition-colors cursor-pointer bg-purple-100 text-purple-600 hover:bg-purple-200" class="px-2 py-1 rounded text-xs font-medium transition-colors cursor-pointer bg-purple-100 text-purple-600 hover:bg-purple-200"
> >
📋 📋
</button> </button>
</div> </div>
{/* 剪贴板不可用时的手动复制弹窗 */}
<Show when={showCopyFallback()}>
<Portal>
<div
class="fixed inset-0 bg-black/50 z-60 flex items-center justify-center p-4"
onClick={closeFallback}
>
<div
class="bg-white rounded-lg shadow-xl w-full max-w-lg p-4"
onClick={(e) => e.stopPropagation()}
>
<h3 class="font-bold mb-2"></h3>
<p class="text-sm text-gray-600 mb-3">
HTTPS
</p>
<textarea
ref={textareaRef}
readonly
value={fallbackCode()}
class="w-full h-40 border border-gray-300 rounded px-3 py-2 text-xs font-mono bg-gray-50 resize-none"
onFocus={(e) => e.currentTarget.select()}
onClick={(e) => e.currentTarget.select()}
/>
<div class="flex justify-end gap-2 mt-3">
<button
onClick={closeFallback}
class="px-3 py-1.5 rounded text-sm font-medium cursor-pointer bg-gray-100 text-gray-700 hover:bg-gray-200"
>
</button>
</div>
</div>
</div>
</Portal>
</Show>
</>
); );
} }
+8 -4
View File
@@ -145,7 +145,7 @@ export interface DeckActions {
clearError: () => void; clearError: () => void;
generateCode: (backLayersStr?: string) => string; generateCode: (backLayersStr?: string) => string;
copyCode: (backLayersStr?: string) => Promise<void>; copyCode: (fallback?: (code: string) => void) => Promise<void>;
setExporting: (exporting: boolean) => void; setExporting: (exporting: boolean) => void;
exportDeck: () => void; exportDeck: () => void;
@@ -514,14 +514,18 @@ export function createDeckStore(initialSrc: string = ""): DeckStore {
return parts.join(""); return parts.join("");
}; };
const copyCode = async (backLayersStr?: string) => { const copyCode = async (fallback?: (code: string) => void) => {
const code = generateCode(backLayersStr); const code = generateCode();
try { try {
await navigator.clipboard.writeText(code); await navigator.clipboard.writeText(code);
alert("已复制到剪贴板!"); alert("已复制到剪贴板!");
} catch (err) { } catch (err) {
console.error("复制失败:", err); console.error("复制失败:", err);
alert("复制失败,请手动复制"); if (fallback) {
fallback(code);
} else {
alert("复制失败,请手动复制");
}
} }
}; };