Skip to content

Commit bbcd77c

Browse files
committed
fix(chatui): Fixed textarea leak in the copy button.
1 parent 9ac696d commit bbcd77c

File tree

1 file changed

+36
-26
lines changed

1 file changed

+36
-26
lines changed

dashboard/src/components/chat/MessageList.vue

Lines changed: 36 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -501,37 +501,61 @@ export default {
501501
502502
// 复制代码到剪贴板
503503
tryExecCommandCopy(text) {
504+
let textArea = null;
504505
try {
505-
const textArea = document.createElement('textarea');
506+
textArea = document.createElement('textarea');
506507
textArea.value = text;
507508
document.body.appendChild(textArea);
508509
textArea.focus();
509510
textArea.select();
510511
const ok = document.execCommand('copy');
511-
document.body.removeChild(textArea);
512512
return ok;
513513
} catch (_) {
514514
return false;
515+
} finally {
516+
try {
517+
textArea?.remove?.();
518+
} catch (_) {
519+
// ignore cleanup errors
520+
}
515521
}
516522
},
517523
518524
async copyTextToClipboard(text) {
519525
// 优先使用同步复制,尽量保留用户手势上下文;
520526
// 在非安全来源(例如通过局域网 IP + vite --host)时成功率更高。
521527
if (this.tryExecCommandCopy(text)) {
522-
return true;
528+
return { ok: true, method: 'execCommand' };
523529
}
524530
525531
if (navigator.clipboard?.writeText) {
526532
try {
527533
await navigator.clipboard.writeText(text);
528-
return true;
529-
} catch (_) {
530-
return false;
534+
return { ok: true, method: 'clipboard' };
535+
} catch (error) {
536+
return { ok: false, method: 'clipboard', error };
531537
}
532538
}
533539
534-
return false;
540+
return { ok: false, method: 'unavailable' };
541+
},
542+
543+
async copyWithFeedback(text, messageIndex = null) {
544+
const result = await this.copyTextToClipboard(text);
545+
const ok = !!result?.ok;
546+
547+
if (messageIndex !== null && messageIndex !== undefined) {
548+
if (ok) this.showCopySuccess(messageIndex);
549+
else this.showCopyFailure(messageIndex);
550+
}
551+
552+
if (ok) {
553+
this.toast?.success?.(this.t('core.common.copied'));
554+
} else {
555+
this.toast?.error?.(this.t('core.common.copyFailed'));
556+
}
557+
558+
return result;
535559
},
536560
537561
buildCopyTextFromParts(messageParts) {
@@ -565,30 +589,15 @@ export default {
565589
566590
async copyCodeToClipboard(code) {
567591
const text = String(code ?? '');
568-
if (!text) return false;
569-
570-
const ok = await this.copyTextToClipboard(text);
571-
if (ok) {
572-
this.toast?.success?.(this.t('core.common.copied'));
573-
} else {
574-
this.toast?.error?.(this.t('core.common.copyFailed'));
575-
}
576-
return ok;
592+
if (!text) return { ok: false, method: 'empty' };
593+
return await this.copyWithFeedback(text, null);
577594
},
578595
579596
// 复制bot消息到剪贴板
580597
async copyBotMessage(messageParts, messageIndex) {
581598
let textToCopy = this.buildCopyTextFromParts(messageParts);
582599
if (!textToCopy) textToCopy = '[媒体内容]';
583-
584-
const ok = await this.copyTextToClipboard(textToCopy);
585-
if (ok) {
586-
this.showCopySuccess(messageIndex);
587-
this.toast?.success?.(this.t('core.common.copied'));
588-
} else {
589-
this.showCopyFailure(messageIndex);
590-
this.toast?.error?.(this.t('core.common.copyFailed'));
591-
}
600+
await this.copyWithFeedback(textToCopy, messageIndex);
592601
},
593602
594603
// 显示复制成功提示
@@ -673,7 +682,8 @@ export default {
673682
button.innerHTML = this.getCopyIconSvg();
674683
button.title = this.t('core.common.copy');
675684
button.addEventListener('click', async () => {
676-
const ok = await this.copyCodeToClipboard(codeBlock.textContent || '');
685+
const res = await this.copyCodeToClipboard(codeBlock.textContent || '');
686+
const ok = !!res?.ok;
677687
button.innerHTML = ok ? this.getSuccessIconSvg() : this.getErrorIconSvg();
678688
button.style.color = ok ? '#4caf50' : '#f44336';
679689
button.setAttribute("title", this.t(`core.common.${ok ? "copied" : "copyFailed"}`));

0 commit comments

Comments
 (0)