mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
fix(ui): clipboard copy fails in non-HTTPS context
Use textarea + execCommand fallback when navigator.clipboard is unavailable. Consolidates 5 call sites into a single helper.
This commit is contained in:
parent
023f0cfb06
commit
a92f76ff87
@ -424,6 +424,7 @@ import { ref, computed, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { CHANNEL_FIELD_DEFS } from '@/types'
|
||||
import { copyToClipboard } from '@/utils/clipboard'
|
||||
import type { Agent, Channel, ChannelFieldDef } from '@/types'
|
||||
import {
|
||||
buildConfigJson,
|
||||
@ -629,7 +630,7 @@ const copyLabel = ref(t('channels.webhook.copy'))
|
||||
|
||||
async function copyWebhookUrl() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(webhookUrl.value)
|
||||
await copyToClipboard(webhookUrl.value)
|
||||
copyLabel.value = t('channels.webhook.copied')
|
||||
setTimeout(() => { copyLabel.value = t('channels.webhook.copy') }, 2000)
|
||||
} catch {
|
||||
@ -639,7 +640,7 @@ async function copyWebhookUrl() {
|
||||
|
||||
async function copyText(text: string) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
await copyToClipboard(text)
|
||||
ElMessage.success(t('common.copied'))
|
||||
} catch {
|
||||
ElMessage.warning(t('channels.webhook.copyFailed'))
|
||||
|
||||
@ -366,6 +366,7 @@ import { useMarkdownRenderer } from '@/composables/useMarkdownRenderer'
|
||||
import { useAuthenticatedAttachment } from '@/composables/useAuthenticatedAttachment'
|
||||
import { useToolLabel } from '@/composables/useToolLabel'
|
||||
import { http } from '@/api'
|
||||
import { copyToClipboard } from '@/utils/clipboard'
|
||||
import TypingCursor from './TypingCursor.vue'
|
||||
import BrowserTimeline from './BrowserTimeline.vue'
|
||||
import ToolCallSegment from './ToolCallSegment.vue'
|
||||
@ -563,7 +564,7 @@ let copyTimer: ReturnType<typeof setTimeout> | null = null
|
||||
function copyMessage() {
|
||||
const text = displayContent.value || props.message.content || ''
|
||||
if (!text) return
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
copyToClipboard(text).then(() => {
|
||||
copyState.value = 'copied'
|
||||
if (copyTimer) clearTimeout(copyTimer)
|
||||
copyTimer = setTimeout(() => { copyState.value = 'idle' }, 2000)
|
||||
|
||||
18
mateclaw-ui/src/utils/clipboard.ts
Normal file
18
mateclaw-ui/src/utils/clipboard.ts
Normal file
@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Copy text to clipboard with fallback for non-HTTPS contexts
|
||||
* where navigator.clipboard is undefined.
|
||||
*/
|
||||
export async function copyToClipboard(text: string): Promise<void> {
|
||||
if (navigator.clipboard) {
|
||||
return navigator.clipboard.writeText(text)
|
||||
}
|
||||
// Fallback: textarea + execCommand('copy')
|
||||
const ta = document.createElement('textarea')
|
||||
ta.value = text
|
||||
ta.style.position = 'fixed'
|
||||
ta.style.left = '-9999px'
|
||||
document.body.appendChild(ta)
|
||||
ta.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(ta)
|
||||
}
|
||||
@ -208,6 +208,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { mcConfirm } from '@/components/common/useConfirm'
|
||||
import { agentApi, agentContextApi } from '@/api/index'
|
||||
import { copyToClipboard } from '@/utils/clipboard'
|
||||
import type { Agent, WorkspaceFile } from '@/types/index'
|
||||
import { useMarkdownRenderer } from '@/composables/useMarkdownRenderer'
|
||||
import { plainTextIcon } from '@/composables/usePixelarticons'
|
||||
@ -330,7 +331,7 @@ function handlePreviewClick(e: MouseEvent) {
|
||||
const encoded = btn.getAttribute('data-code')
|
||||
if (!encoded) return
|
||||
const code = decodeURIComponent(encoded)
|
||||
navigator.clipboard.writeText(code).then(() => {
|
||||
copyToClipboard(code).then(() => {
|
||||
btn.classList.add('copied')
|
||||
const textEl = btn.querySelector('.code-block__copy-text')
|
||||
if (textEl) textEl.textContent = t('chat.copied') || 'Copied'
|
||||
|
||||
@ -317,6 +317,7 @@ import { mcConfirm } from '@/components/common/useConfirm'
|
||||
import { ChatDotRound, Delete, Plus, Setting, UploadFilled } from '@element-plus/icons-vue'
|
||||
import { conversationApi, agentApi, modelApi, chatApi, cronJobApi } from '@/api/index'
|
||||
import { channelIconUrl } from '@/utils/channelSource'
|
||||
import { copyToClipboard } from '@/utils/clipboard'
|
||||
import { useChat } from '@/composables/chat/useChat'
|
||||
import { reconstructErrorInfo } from '@/types/chatError'
|
||||
import { reconcileMessages, extractMessages } from '@/utils/messageReconcile'
|
||||
@ -1741,7 +1742,7 @@ function handleCodeCopy(e: MouseEvent) {
|
||||
const encoded = btn.getAttribute('data-code')
|
||||
if (!encoded) return
|
||||
const code = decodeURIComponent(encoded)
|
||||
navigator.clipboard.writeText(code).then(() => {
|
||||
copyToClipboard(code).then(() => {
|
||||
btn.classList.add('copied')
|
||||
const textEl = btn.querySelector('.code-block__copy-text')
|
||||
if (textEl) textEl.textContent = t('chat.copied')
|
||||
|
||||
@ -41,6 +41,7 @@
|
||||
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { copyToClipboard } from '@/utils/clipboard'
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
@ -83,7 +84,7 @@ onBeforeUnmount(() => {
|
||||
|
||||
async function copyCode() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(props.userCode)
|
||||
await copyToClipboard(props.userCode)
|
||||
copied.value = true
|
||||
setTimeout(() => { copied.value = false }, 2000)
|
||||
} catch {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user