refactor: use fallback clipboard for non-https env (#111)
This commit is contained in:
@@ -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() {
|
||||
|
||||
@@ -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('复制失败')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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('复制失败,请手动复制')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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('复制失败,请手动选择复制')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user