mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-14 19:45:08 +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 { useI18n } from 'vue-i18n'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { CHANNEL_FIELD_DEFS } from '@/types'
|
import { CHANNEL_FIELD_DEFS } from '@/types'
|
||||||
|
import { copyToClipboard } from '@/utils/clipboard'
|
||||||
import type { Agent, Channel, ChannelFieldDef } from '@/types'
|
import type { Agent, Channel, ChannelFieldDef } from '@/types'
|
||||||
import {
|
import {
|
||||||
buildConfigJson,
|
buildConfigJson,
|
||||||
@ -629,7 +630,7 @@ const copyLabel = ref(t('channels.webhook.copy'))
|
|||||||
|
|
||||||
async function copyWebhookUrl() {
|
async function copyWebhookUrl() {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(webhookUrl.value)
|
await copyToClipboard(webhookUrl.value)
|
||||||
copyLabel.value = t('channels.webhook.copied')
|
copyLabel.value = t('channels.webhook.copied')
|
||||||
setTimeout(() => { copyLabel.value = t('channels.webhook.copy') }, 2000)
|
setTimeout(() => { copyLabel.value = t('channels.webhook.copy') }, 2000)
|
||||||
} catch {
|
} catch {
|
||||||
@ -639,7 +640,7 @@ async function copyWebhookUrl() {
|
|||||||
|
|
||||||
async function copyText(text: string) {
|
async function copyText(text: string) {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(text)
|
await copyToClipboard(text)
|
||||||
ElMessage.success(t('common.copied'))
|
ElMessage.success(t('common.copied'))
|
||||||
} catch {
|
} catch {
|
||||||
ElMessage.warning(t('channels.webhook.copyFailed'))
|
ElMessage.warning(t('channels.webhook.copyFailed'))
|
||||||
|
|||||||
@ -366,6 +366,7 @@ import { useMarkdownRenderer } from '@/composables/useMarkdownRenderer'
|
|||||||
import { useAuthenticatedAttachment } from '@/composables/useAuthenticatedAttachment'
|
import { useAuthenticatedAttachment } from '@/composables/useAuthenticatedAttachment'
|
||||||
import { useToolLabel } from '@/composables/useToolLabel'
|
import { useToolLabel } from '@/composables/useToolLabel'
|
||||||
import { http } from '@/api'
|
import { http } from '@/api'
|
||||||
|
import { copyToClipboard } from '@/utils/clipboard'
|
||||||
import TypingCursor from './TypingCursor.vue'
|
import TypingCursor from './TypingCursor.vue'
|
||||||
import BrowserTimeline from './BrowserTimeline.vue'
|
import BrowserTimeline from './BrowserTimeline.vue'
|
||||||
import ToolCallSegment from './ToolCallSegment.vue'
|
import ToolCallSegment from './ToolCallSegment.vue'
|
||||||
@ -563,7 +564,7 @@ let copyTimer: ReturnType<typeof setTimeout> | null = null
|
|||||||
function copyMessage() {
|
function copyMessage() {
|
||||||
const text = displayContent.value || props.message.content || ''
|
const text = displayContent.value || props.message.content || ''
|
||||||
if (!text) return
|
if (!text) return
|
||||||
navigator.clipboard.writeText(text).then(() => {
|
copyToClipboard(text).then(() => {
|
||||||
copyState.value = 'copied'
|
copyState.value = 'copied'
|
||||||
if (copyTimer) clearTimeout(copyTimer)
|
if (copyTimer) clearTimeout(copyTimer)
|
||||||
copyTimer = setTimeout(() => { copyState.value = 'idle' }, 2000)
|
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 { ElMessage } from 'element-plus'
|
||||||
import { mcConfirm } from '@/components/common/useConfirm'
|
import { mcConfirm } from '@/components/common/useConfirm'
|
||||||
import { agentApi, agentContextApi } from '@/api/index'
|
import { agentApi, agentContextApi } from '@/api/index'
|
||||||
|
import { copyToClipboard } from '@/utils/clipboard'
|
||||||
import type { Agent, WorkspaceFile } from '@/types/index'
|
import type { Agent, WorkspaceFile } from '@/types/index'
|
||||||
import { useMarkdownRenderer } from '@/composables/useMarkdownRenderer'
|
import { useMarkdownRenderer } from '@/composables/useMarkdownRenderer'
|
||||||
import { plainTextIcon } from '@/composables/usePixelarticons'
|
import { plainTextIcon } from '@/composables/usePixelarticons'
|
||||||
@ -330,7 +331,7 @@ function handlePreviewClick(e: MouseEvent) {
|
|||||||
const encoded = btn.getAttribute('data-code')
|
const encoded = btn.getAttribute('data-code')
|
||||||
if (!encoded) return
|
if (!encoded) return
|
||||||
const code = decodeURIComponent(encoded)
|
const code = decodeURIComponent(encoded)
|
||||||
navigator.clipboard.writeText(code).then(() => {
|
copyToClipboard(code).then(() => {
|
||||||
btn.classList.add('copied')
|
btn.classList.add('copied')
|
||||||
const textEl = btn.querySelector('.code-block__copy-text')
|
const textEl = btn.querySelector('.code-block__copy-text')
|
||||||
if (textEl) textEl.textContent = t('chat.copied') || 'Copied'
|
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 { ChatDotRound, Delete, Plus, Setting, UploadFilled } from '@element-plus/icons-vue'
|
||||||
import { conversationApi, agentApi, modelApi, chatApi, cronJobApi } from '@/api/index'
|
import { conversationApi, agentApi, modelApi, chatApi, cronJobApi } from '@/api/index'
|
||||||
import { channelIconUrl } from '@/utils/channelSource'
|
import { channelIconUrl } from '@/utils/channelSource'
|
||||||
|
import { copyToClipboard } from '@/utils/clipboard'
|
||||||
import { useChat } from '@/composables/chat/useChat'
|
import { useChat } from '@/composables/chat/useChat'
|
||||||
import { reconstructErrorInfo } from '@/types/chatError'
|
import { reconstructErrorInfo } from '@/types/chatError'
|
||||||
import { reconcileMessages, extractMessages } from '@/utils/messageReconcile'
|
import { reconcileMessages, extractMessages } from '@/utils/messageReconcile'
|
||||||
@ -1741,7 +1742,7 @@ function handleCodeCopy(e: MouseEvent) {
|
|||||||
const encoded = btn.getAttribute('data-code')
|
const encoded = btn.getAttribute('data-code')
|
||||||
if (!encoded) return
|
if (!encoded) return
|
||||||
const code = decodeURIComponent(encoded)
|
const code = decodeURIComponent(encoded)
|
||||||
navigator.clipboard.writeText(code).then(() => {
|
copyToClipboard(code).then(() => {
|
||||||
btn.classList.add('copied')
|
btn.classList.add('copied')
|
||||||
const textEl = btn.querySelector('.code-block__copy-text')
|
const textEl = btn.querySelector('.code-block__copy-text')
|
||||||
if (textEl) textEl.textContent = t('chat.copied')
|
if (textEl) textEl.textContent = t('chat.copied')
|
||||||
|
|||||||
@ -41,6 +41,7 @@
|
|||||||
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { copyToClipboard } from '@/utils/clipboard'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
visible: boolean
|
visible: boolean
|
||||||
@ -83,7 +84,7 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
async function copyCode() {
|
async function copyCode() {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(props.userCode)
|
await copyToClipboard(props.userCode)
|
||||||
copied.value = true
|
copied.value = true
|
||||||
setTimeout(() => { copied.value = false }, 2000)
|
setTimeout(() => { copied.value = false }, 2000)
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user