refactor: use fallback clipboard for non-https env (#111)

This commit is contained in:
duorameng
2026-06-09 16:12:55 +08:00
parent fc04bd9b0c
commit da4f08e9a1
8 changed files with 60 additions and 61 deletions
@@ -3,6 +3,7 @@ import { Button } from '@/components/ui/button'
import { Plus, Ticket, Check, X, Copy, Pencil, Trash2 } from 'lucide-vue-next' import { Plus, Ticket, Check, X, Copy, Pencil, Trash2 } from 'lucide-vue-next'
import { type AgentToken, api } from '@/api' import { type AgentToken, api } from '@/api'
import { toast } from 'vue-sonner' import { toast } from 'vue-sonner'
import { copyToClipboard } from '@/utils/clipboard'
const props = defineProps<{ const props = defineProps<{
tokens: AgentToken[] tokens: AgentToken[]
@@ -25,8 +26,9 @@ function isTokenExhausted(token: AgentToken) {
} }
function copyToken(token: string) { function copyToken(token: string) {
navigator.clipboard.writeText(token) copyToClipboard(token).then((success) => {
toast.success('已复制') if (success) toast.success('已复制')
})
} }
function openTokenDialog() { function openTokenDialog() {
+7 -6
View File
@@ -11,6 +11,7 @@ import EventBinding from './components/EventBinding.vue'
import ApiUsage from './components/ApiUsage.vue' import ApiUsage from './components/ApiUsage.vue'
import ChannelDialog from './components/ChannelDialog.vue' import ChannelDialog from './components/ChannelDialog.vue'
import TemplateSettings from './components/TemplateSettings.vue' import TemplateSettings from './components/TemplateSettings.vue'
import { copyToClipboard } from '@/utils/clipboard'
const activeTab = ref('channels') const activeTab = ref('channels')
@@ -291,10 +292,10 @@ async function copyApiToken() {
toast.error('请先生成 Token') toast.error('请先生成 Token')
return return
} }
try { const success = await copyToClipboard(apiToken.value)
await navigator.clipboard.writeText(apiToken.value) if (success) {
toast.success('Token 已复制到剪贴板') toast.success('Token 已复制到剪贴板')
} catch { } else {
toast.error('复制失败') toast.error('复制失败')
} }
} }
@@ -311,10 +312,10 @@ async function copyApiExample() {
-H "Content-Type: application/json" \\ -H "Content-Type: application/json" \\
-H "notify-token: ${token}" \\ -H "notify-token: ${token}" \\
-d '{"channel_id": "${ch.id}", "title": "测试通知", "text": "来自脚本的通知"}'` -d '{"channel_id": "${ch.id}", "title": "测试通知", "text": "来自脚本的通知"}'`
try { const success = await copyToClipboard(example)
await navigator.clipboard.writeText(example) if (success) {
toast.success('API 调用示例已复制到剪贴板') toast.success('API 调用示例已复制到剪贴板')
} catch { } else {
toast.error('复制失败') toast.error('复制失败')
} }
} }
+16 -13
View File
@@ -19,6 +19,7 @@ import {
import type { NotifyChannel, ChannelType } from '@/api' import type { NotifyChannel, ChannelType } from '@/api'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { toast } from 'vue-sonner' import { toast } from 'vue-sonner'
import { copyToClipboard } from '@/utils/clipboard'
const props = defineProps<{ const props = defineProps<{
channels: NotifyChannel[] channels: NotifyChannel[]
@@ -49,13 +50,15 @@ function handleConfirmGenerate() {
emit('generateToken') emit('generateToken')
} }
function copyToClipboard(text: string, blockId: string) { function handleCopy(text: string, blockId: string) {
navigator.clipboard.writeText(text).then(() => { copyToClipboard(text).then((success) => {
copiedBlock.value = blockId if (success) {
toast.success('已复制到剪贴板') copiedBlock.value = blockId
setTimeout(() => { toast.success('已复制到剪贴板')
copiedBlock.value = null setTimeout(() => {
}, 2000) copiedBlock.value = null
}, 2000)
}
}) })
} }
@@ -264,7 +267,7 @@ const currentExample = computed(() => {
<div class="relative flex-1 group"> <div class="relative flex-1 group">
<Input :model-value="apiToken" readonly placeholder="尚未生成 Token" <Input :model-value="apiToken" readonly placeholder="尚未生成 Token"
class="h-10 pr-10 bg-muted/30 border-muted-foreground/20 focus-visible:ring-primary/30 font-code text-sm tracking-tight" /> class="h-10 pr-10 bg-muted/30 border-muted-foreground/20 focus-visible:ring-primary/30 font-code text-sm tracking-tight" />
<div v-if="apiToken" @click="copyToClipboard(apiToken, 'token')" <div v-if="apiToken" @click="handleCopy(apiToken, 'token')"
class="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-primary cursor-pointer transition-colors p-1 rounded-md hover:bg-muted" class="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-primary cursor-pointer transition-colors p-1 rounded-md hover:bg-muted"
title="复制 Token"> title="复制 Token">
<Check v-if="copiedBlock === 'token'" class="w-4 h-4 text-emerald-500 animate-in zoom-in" /> <Check v-if="copiedBlock === 'token'" class="w-4 h-4 text-emerald-500 animate-in zoom-in" />
@@ -361,7 +364,7 @@ const currentExample = computed(() => {
</div> </div>
<Button variant="ghost" size="icon" <Button variant="ghost" size="icon"
class="absolute right-2 top-1/2 -translate-y-1/2 h-7 w-7 text-zinc-500 hover:text-white hover:bg-white/10 opacity-0 group-hover:opacity-100 transition-all" class="absolute right-2 top-1/2 -translate-y-1/2 h-7 w-7 text-zinc-500 hover:text-white hover:bg-white/10 opacity-0 group-hover:opacity-100 transition-all"
@click="copyToClipboard('baihu builtininstall', 'install-cmd')"> @click="handleCopy('baihu builtininstall', 'install-cmd')">
<Check v-if="copiedBlock === 'install-cmd'" class="w-3.5 h-3.5 text-emerald-500" /> <Check v-if="copiedBlock === 'install-cmd'" class="w-3.5 h-3.5 text-emerald-500" />
<Copy v-else class="w-3.5 h-3.5" /> <Copy v-else class="w-3.5 h-3.5" />
</Button> </Button>
@@ -389,7 +392,7 @@ const currentExample = computed(() => {
<pre class="text-[11px] leading-snug"><span class="text-violet-500">import</span> baihu<br/>baihu.notify(<span class="text-emerald-600">"标题"</span>, <span class="text-emerald-600">"内容"</span>)</pre> <pre class="text-[11px] leading-snug"><span class="text-violet-500">import</span> baihu<br/>baihu.notify(<span class="text-emerald-600">"标题"</span>, <span class="text-emerald-600">"内容"</span>)</pre>
<Button variant="ghost" size="icon" <Button variant="ghost" size="icon"
class="absolute right-2 top-2 h-6 w-6 text-zinc-400 opacity-0 group-hover:opacity-100 transition-all" class="absolute right-2 top-2 h-6 w-6 text-zinc-400 opacity-0 group-hover:opacity-100 transition-all"
@click="copyToClipboard('import baihu\nbaihu.notify(\'标题\', \'内容\')', 'py-builtin')"> @click="handleCopy('import baihu\nbaihu.notify(\'标题\', \'内容\')', 'py-builtin')">
<Check v-if="copiedBlock === 'py-builtin'" class="w-3.5 h-3.5 text-emerald-500" /> <Check v-if="copiedBlock === 'py-builtin'" class="w-3.5 h-3.5 text-emerald-500" />
<Copy v-else class="w-3.5 h-3.5" /> <Copy v-else class="w-3.5 h-3.5" />
</Button> </Button>
@@ -406,7 +409,7 @@ const currentExample = computed(() => {
<pre class="text-[11px] leading-snug"><span class="text-violet-500">const</span> baihu = require(<span class="text-emerald-600">'baihu'</span>);<br/>baihu.notify(<span class="text-emerald-600">"标题"</span>, <span class="text-emerald-600">"内容"</span>);</pre> <pre class="text-[11px] leading-snug"><span class="text-violet-500">const</span> baihu = require(<span class="text-emerald-600">'baihu'</span>);<br/>baihu.notify(<span class="text-emerald-600">"标题"</span>, <span class="text-emerald-600">"内容"</span>);</pre>
<Button variant="ghost" size="icon" <Button variant="ghost" size="icon"
class="absolute right-2 top-2 h-6 w-6 text-zinc-400 opacity-0 group-hover:opacity-100 transition-all" class="absolute right-2 top-2 h-6 w-6 text-zinc-400 opacity-0 group-hover:opacity-100 transition-all"
@click="copyToClipboard('const baihu = require(\'baihu\');\nbaihu.notify(\'标题\', \'内容\');', 'js-builtin')"> @click="handleCopy('const baihu = require(\'baihu\');\nbaihu.notify(\'标题\', \'内容\');', 'js-builtin')">
<Check v-if="copiedBlock === 'js-builtin'" class="w-3.5 h-3.5 text-emerald-500" /> <Check v-if="copiedBlock === 'js-builtin'" class="w-3.5 h-3.5 text-emerald-500" />
<Copy v-else class="w-3.5 h-3.5" /> <Copy v-else class="w-3.5 h-3.5" />
</Button> </Button>
@@ -452,7 +455,7 @@ const currentExample = computed(() => {
</div> </div>
<Button variant="outline" size="sm" <Button variant="outline" size="sm"
class="h-8 px-2.5 text-[11px] border-muted-foreground/30 hover:bg-muted transition-all shrink-0" class="h-8 px-2.5 text-[11px] border-muted-foreground/30 hover:bg-muted transition-all shrink-0"
@click="copyToClipboard(currentExample, 'example')"> @click="handleCopy(currentExample, 'example')">
<Check v-if="copiedBlock === 'example'" class="w-3.5 h-3.5 text-emerald-500 sm:mr-1.5" /> <Check v-if="copiedBlock === 'example'" class="w-3.5 h-3.5 text-emerald-500 sm:mr-1.5" />
<Copy v-else class="w-3.5 h-3.5 sm:mr-1.5" /> <Copy v-else class="w-3.5 h-3.5 sm:mr-1.5" />
<span class="hidden sm:inline">复制代码</span> <span class="hidden sm:inline">复制代码</span>
@@ -481,7 +484,7 @@ const currentExample = computed(() => {
<span class="text-zinc-600 dark:text-zinc-500 truncate max-w-[100px]">{{ ch.name }}</span> <span class="text-zinc-600 dark:text-zinc-500 truncate max-w-[100px]">{{ ch.name }}</span>
<Button variant="ghost" size="icon" <Button variant="ghost" size="icon"
class="h-5 w-5 ml-auto text-zinc-400 hover:text-zinc-800 dark:hover:text-zinc-200 hover:bg-zinc-200 dark:hover:bg-zinc-800 opacity-0 group-hover:opacity-100 transition-all rounded" class="h-5 w-5 ml-auto text-zinc-400 hover:text-zinc-800 dark:hover:text-zinc-200 hover:bg-zinc-200 dark:hover:bg-zinc-800 opacity-0 group-hover:opacity-100 transition-all rounded"
@click="copyToClipboard(ch.id, 'channel-' + ch.id)"> @click="handleCopy(ch.id, 'channel-' + ch.id)">
<Check v-if="copiedBlock === 'channel-' + ch.id" class="w-2.5 h-2.5 text-emerald-500" /> <Check v-if="copiedBlock === 'channel-' + ch.id" class="w-2.5 h-2.5 text-emerald-500" />
<Copy v-else class="w-2.5 h-2.5" /> <Copy v-else class="w-2.5 h-2.5" />
</Button> </Button>
@@ -6,6 +6,7 @@ import { Plus, Trash2, TestTube, Pencil, Bell, Calendar, Copy, Check } from 'luc
import type { NotifyChannel, ChannelType } from '@/api' import type { NotifyChannel, ChannelType } from '@/api'
import { ref } from 'vue' import { ref } from 'vue'
import { toast } from 'vue-sonner' import { toast } from 'vue-sonner'
import { copyToClipboard } from '@/utils/clipboard'
defineProps<{ defineProps<{
channels: NotifyChannel[] channels: NotifyChannel[]
@@ -26,13 +27,15 @@ function getChannelTypeName(type: string, channelTypes: ChannelType[]): string {
const copiedBlock = ref<string | null>(null) const copiedBlock = ref<string | null>(null)
function copyToClipboard(text: string, blockId: string) { function handleCopy(text: string, blockId: string) {
navigator.clipboard.writeText(text).then(() => { copyToClipboard(text).then((success) => {
copiedBlock.value = blockId if (success) {
toast.success('已复制长 ID') copiedBlock.value = blockId
setTimeout(() => { toast.success('已复制长 ID')
copiedBlock.value = null setTimeout(() => {
}, 2000) copiedBlock.value = null
}, 2000)
}
}) })
} }
</script> </script>
@@ -94,7 +97,7 @@ function copyToClipboard(text: string, blockId: string) {
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<Button variant="ghost" size="icon" <Button variant="ghost" size="icon"
class="h-8 w-8 rounded-full hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors" class="h-8 w-8 rounded-full hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors"
@click="copyToClipboard(ch.id, 'channel-' + ch.id)" title="复制 ID"> @click="handleCopy(ch.id, 'channel-' + ch.id)" title="复制 ID">
<Check v-if="copiedBlock === 'channel-' + ch.id" class="w-4 h-4 text-emerald-500" /> <Check v-if="copiedBlock === 'channel-' + ch.id" class="w-4 h-4 text-emerald-500" />
<Copy v-else class="w-4 h-4" /> <Copy v-else class="w-4 h-4" />
</Button> </Button>
+4 -3
View File
@@ -6,6 +6,7 @@ import { Button } from '@/components/ui/button'
import { api, type SiteSettings } from '@/api' import { api, type SiteSettings } from '@/api'
import { toast } from 'vue-sonner' import { toast } from 'vue-sonner'
import { useSiteSettings } from '@/composables/useSiteSettings' import { useSiteSettings } from '@/composables/useSiteSettings'
import { copyToClipboard as copyTextToClipboard } from '@/utils/clipboard'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { Switch } from '@/components/ui/switch' import { Switch } from '@/components/ui/switch'
import { RefreshCw, Copy, AlertTriangle, ExternalLink, Info, Clock } from 'lucide-vue-next' import { RefreshCw, Copy, AlertTriangle, ExternalLink, Info, Clock } from 'lucide-vue-next'
@@ -107,10 +108,10 @@ async function generateOpenapiToken() {
async function copyOpenapiToken() { async function copyOpenapiToken() {
if (!form.value.openapi_token) return if (!form.value.openapi_token) return
try { const success = await copyTextToClipboard(form.value.openapi_token)
await navigator.clipboard.writeText(form.value.openapi_token) if (success) {
toast.success('Token 已复制到剪贴板') toast.success('Token 已复制到剪贴板')
} catch { } else {
toast.error('复制失败,请手动复制') toast.error('复制失败,请手动复制')
} }
} }
+4 -2
View File
@@ -16,6 +16,7 @@ import { toast } from 'vue-sonner'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { parseBaihuCommand, parseQlCommand } from '@/utils/repo-parser' import { parseBaihuCommand, parseQlCommand } from '@/utils/repo-parser'
import { copyToClipboard } from '@/utils/clipboard'
import TaskNotificationConfig from './components/TaskNotificationConfig.vue' import TaskNotificationConfig from './components/TaskNotificationConfig.vue'
import TaskAdvancedConfig from './components/TaskAdvancedConfig.vue' import TaskAdvancedConfig from './components/TaskAdvancedConfig.vue'
import TaskCronConfig from './components/TaskCronConfig.vue' import TaskCronConfig from './components/TaskCronConfig.vue'
@@ -120,8 +121,9 @@ function exportBaihuCommand() {
} }
const cmd = parts.join(' ') const cmd = parts.join(' ')
navigator.clipboard.writeText(cmd) copyToClipboard(cmd).then((success) => {
toast.success('baihu 指令已复制到剪贴板') if (success) toast.success('baihu 指令已复制到剪贴板')
})
} }
function importFromBaihu() { function importFromBaihu() {
+8 -5
View File
@@ -31,6 +31,7 @@ import { TASK_TYPE, AGENT_STATUS, TRIGGER_TYPE, TASK_STATUS } from '@/constants'
import TextOverflow from '@/components/TextOverflow.vue' import TextOverflow from '@/components/TextOverflow.vue'
import { getCronDescription } from '@/utils/cron' import { getCronDescription } from '@/utils/cron'
import { generateBaihuCommand } from '@/utils/repo-parser' import { generateBaihuCommand } from '@/utils/repo-parser'
import { copyToClipboard } from '@/utils/clipboard'
const router = useRouter() const router = useRouter()
@@ -186,11 +187,13 @@ function openExportDialog(task: Task) {
function copyCommandText() { function copyCommandText() {
if (!exportCommandText.value) return if (!exportCommandText.value) return
navigator.clipboard.writeText(exportCommandText.value).then(() => { copyToClipboard(exportCommandText.value).then((success) => {
toast.success('同步指令已复制到剪贴板') if (success) {
showExportDialog.value = false toast.success('同步指令已复制到剪贴板')
}).catch(() => { showExportDialog.value = false
toast.error('复制失败,请手动选择复制') } else {
toast.error('复制失败,请手动选择复制')
}
}) })
} }
+6 -22
View File
@@ -47,34 +47,18 @@ function handleStatusChange(status: any) {
statusState.value = status statusState.value = status
} }
import { copyToClipboard } from '@/utils/clipboard'
const copiedCommand = ref<string | null>(null) const copiedCommand = ref<string | null>(null)
const copyToClipboard = async (text: string) => { const handleCopy = async (text: string) => {
try { const success = await copyToClipboard(text)
if (navigator.clipboard && window.isSecureContext) { if (success) {
await navigator.clipboard.writeText(text)
} else {
const textArea = document.createElement("textarea")
textArea.value = text
textArea.style.position = "absolute"
textArea.style.opacity = "0"
document.body.prepend(textArea)
textArea.select()
try {
document.execCommand('copy')
} catch (err) {
console.error('Fallback copy failed', err)
} finally {
textArea.remove()
}
}
copiedCommand.value = text copiedCommand.value = text
setTimeout(() => { setTimeout(() => {
if (copiedCommand.value === text) { if (copiedCommand.value === text) {
copiedCommand.value = null copiedCommand.value = null
} }
}, 2000) }, 2000)
} catch (err) {
console.error('Failed to copy', err)
} }
} }
</script> </script>
@@ -105,7 +89,7 @@ const copyToClipboard = async (text: string) => {
<span class="font-bold text-blue-400 text-xs">baihu {{ cmd.name }}</span> <span class="font-bold text-blue-400 text-xs">baihu {{ cmd.name }}</span>
<span class="text-gray-400 text-[11px] leading-tight mt-0.5">{{ cmd.description }}</span> <span class="text-gray-400 text-[11px] leading-tight mt-0.5">{{ cmd.description }}</span>
</div> </div>
<button @click.stop.prevent="copyToClipboard(`baihu ${cmd.name}`)" class="opacity-0 group-hover:opacity-100 transition-opacity p-1 hover:bg-[#3c3c3c] rounded text-gray-400 hover:text-white focus:outline-none focus:ring-0 shrink-0 mt-0.5" :title="copiedCommand === `baihu ${cmd.name}` ? '已复制' : '复制命令'"> <button @click.stop.prevent="handleCopy(`baihu ${cmd.name}`)" class="opacity-0 group-hover:opacity-100 transition-opacity p-1 hover:bg-[#3c3c3c] rounded text-gray-400 hover:text-white focus:outline-none focus:ring-0 shrink-0 mt-0.5" :title="copiedCommand === `baihu ${cmd.name}` ? '已复制' : '复制命令'">
<Check v-if="copiedCommand === `baihu ${cmd.name}`" class="h-3 w-3 text-green-500" /> <Check v-if="copiedCommand === `baihu ${cmd.name}`" class="h-3 w-3 text-green-500" />
<Copy v-else class="h-3 w-3" /> <Copy v-else class="h-3 w-3" />
</button> </button>