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 { type AgentToken, api } from '@/api'
import { toast } from 'vue-sonner'
import { copyToClipboard } from '@/utils/clipboard'
const props = defineProps<{
tokens: AgentToken[]
@@ -25,8 +26,9 @@ function isTokenExhausted(token: AgentToken) {
}
function copyToken(token: string) {
navigator.clipboard.writeText(token)
toast.success('已复制')
copyToClipboard(token).then((success) => {
if (success) toast.success('已复制')
})
}
function openTokenDialog() {
+7 -6
View File
@@ -11,6 +11,7 @@ import EventBinding from './components/EventBinding.vue'
import ApiUsage from './components/ApiUsage.vue'
import ChannelDialog from './components/ChannelDialog.vue'
import TemplateSettings from './components/TemplateSettings.vue'
import { copyToClipboard } from '@/utils/clipboard'
const activeTab = ref('channels')
@@ -291,10 +292,10 @@ async function copyApiToken() {
toast.error('请先生成 Token')
return
}
try {
await navigator.clipboard.writeText(apiToken.value)
const success = await copyToClipboard(apiToken.value)
if (success) {
toast.success('Token 已复制到剪贴板')
} catch {
} else {
toast.error('复制失败')
}
}
@@ -311,10 +312,10 @@ async function copyApiExample() {
-H "Content-Type: application/json" \\
-H "notify-token: ${token}" \\
-d '{"channel_id": "${ch.id}", "title": "测试通知", "text": "来自脚本的通知"}'`
try {
await navigator.clipboard.writeText(example)
const success = await copyToClipboard(example)
if (success) {
toast.success('API 调用示例已复制到剪贴板')
} catch {
} else {
toast.error('复制失败')
}
}
+16 -13
View File
@@ -19,6 +19,7 @@ import {
import type { NotifyChannel, ChannelType } from '@/api'
import { ref, computed } from 'vue'
import { toast } from 'vue-sonner'
import { copyToClipboard } from '@/utils/clipboard'
const props = defineProps<{
channels: NotifyChannel[]
@@ -49,13 +50,15 @@ function handleConfirmGenerate() {
emit('generateToken')
}
function copyToClipboard(text: string, blockId: string) {
navigator.clipboard.writeText(text).then(() => {
copiedBlock.value = blockId
toast.success('已复制到剪贴板')
setTimeout(() => {
copiedBlock.value = null
}, 2000)
function handleCopy(text: string, blockId: string) {
copyToClipboard(text).then((success) => {
if (success) {
copiedBlock.value = blockId
toast.success('已复制到剪贴板')
setTimeout(() => {
copiedBlock.value = null
}, 2000)
}
})
}
@@ -264,7 +267,7 @@ const currentExample = computed(() => {
<div class="relative flex-1 group">
<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" />
<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"
title="复制 Token">
<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>
<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"
@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" />
<Copy v-else class="w-3.5 h-3.5" />
</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>
<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"
@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" />
<Copy v-else class="w-3.5 h-3.5" />
</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>
<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"
@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" />
<Copy v-else class="w-3.5 h-3.5" />
</Button>
@@ -452,7 +455,7 @@ const currentExample = computed(() => {
</div>
<Button variant="outline" size="sm"
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" />
<Copy v-else class="w-3.5 h-3.5 sm:mr-1.5" />
<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>
<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"
@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" />
<Copy v-else class="w-2.5 h-2.5" />
</Button>
@@ -6,6 +6,7 @@ import { Plus, Trash2, TestTube, Pencil, Bell, Calendar, Copy, Check } from 'luc
import type { NotifyChannel, ChannelType } from '@/api'
import { ref } from 'vue'
import { toast } from 'vue-sonner'
import { copyToClipboard } from '@/utils/clipboard'
defineProps<{
channels: NotifyChannel[]
@@ -26,13 +27,15 @@ function getChannelTypeName(type: string, channelTypes: ChannelType[]): string {
const copiedBlock = ref<string | null>(null)
function copyToClipboard(text: string, blockId: string) {
navigator.clipboard.writeText(text).then(() => {
copiedBlock.value = blockId
toast.success('已复制长 ID')
setTimeout(() => {
copiedBlock.value = null
}, 2000)
function handleCopy(text: string, blockId: string) {
copyToClipboard(text).then((success) => {
if (success) {
copiedBlock.value = blockId
toast.success('已复制长 ID')
setTimeout(() => {
copiedBlock.value = null
}, 2000)
}
})
}
</script>
@@ -94,7 +97,7 @@ function copyToClipboard(text: string, blockId: string) {
<div class="flex items-center gap-1">
<Button variant="ghost" size="icon"
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" />
<Copy v-else class="w-4 h-4" />
</Button>
+4 -3
View File
@@ -6,6 +6,7 @@ import { Button } from '@/components/ui/button'
import { api, type SiteSettings } from '@/api'
import { toast } from 'vue-sonner'
import { useSiteSettings } from '@/composables/useSiteSettings'
import { copyToClipboard as copyTextToClipboard } from '@/utils/clipboard'
import { Badge } from '@/components/ui/badge'
import { Switch } from '@/components/ui/switch'
import { RefreshCw, Copy, AlertTriangle, ExternalLink, Info, Clock } from 'lucide-vue-next'
@@ -107,10 +108,10 @@ async function generateOpenapiToken() {
async function copyOpenapiToken() {
if (!form.value.openapi_token) return
try {
await navigator.clipboard.writeText(form.value.openapi_token)
const success = await copyTextToClipboard(form.value.openapi_token)
if (success) {
toast.success('Token 已复制到剪贴板')
} catch {
} else {
toast.error('复制失败,请手动复制')
}
}
+4 -2
View File
@@ -16,6 +16,7 @@ import { toast } from 'vue-sonner'
import { cn } from '@/lib/utils'
import { parseBaihuCommand, parseQlCommand } from '@/utils/repo-parser'
import { copyToClipboard } from '@/utils/clipboard'
import TaskNotificationConfig from './components/TaskNotificationConfig.vue'
import TaskAdvancedConfig from './components/TaskAdvancedConfig.vue'
import TaskCronConfig from './components/TaskCronConfig.vue'
@@ -120,8 +121,9 @@ function exportBaihuCommand() {
}
const cmd = parts.join(' ')
navigator.clipboard.writeText(cmd)
toast.success('baihu 指令已复制到剪贴板')
copyToClipboard(cmd).then((success) => {
if (success) toast.success('baihu 指令已复制到剪贴板')
})
}
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 { getCronDescription } from '@/utils/cron'
import { generateBaihuCommand } from '@/utils/repo-parser'
import { copyToClipboard } from '@/utils/clipboard'
const router = useRouter()
@@ -186,11 +187,13 @@ function openExportDialog(task: Task) {
function copyCommandText() {
if (!exportCommandText.value) return
navigator.clipboard.writeText(exportCommandText.value).then(() => {
toast.success('同步指令已复制到剪贴板')
showExportDialog.value = false
}).catch(() => {
toast.error('复制失败,请手动选择复制')
copyToClipboard(exportCommandText.value).then((success) => {
if (success) {
toast.success('同步指令已复制到剪贴板')
showExportDialog.value = false
} else {
toast.error('复制失败,请手动选择复制')
}
})
}
+6 -22
View File
@@ -47,34 +47,18 @@ function handleStatusChange(status: any) {
statusState.value = status
}
import { copyToClipboard } from '@/utils/clipboard'
const copiedCommand = ref<string | null>(null)
const copyToClipboard = async (text: string) => {
try {
if (navigator.clipboard && window.isSecureContext) {
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()
}
}
const handleCopy = async (text: string) => {
const success = await copyToClipboard(text)
if (success) {
copiedCommand.value = text
setTimeout(() => {
if (copiedCommand.value === text) {
copiedCommand.value = null
}
}, 2000)
} catch (err) {
console.error('Failed to copy', err)
}
}
</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="text-gray-400 text-[11px] leading-tight mt-0.5">{{ cmd.description }}</span>
</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" />
<Copy v-else class="h-3 w-3" />
</button>