优化: aichat 打磨收尾(切换信任保留 + save并发串行 + 后台授权守卫 + 分离窗口失效 + 气泡截图复制)

This commit is contained in:
lxy
2026-08-08 21:08:25 +08:00
parent 65c0f6b2b6
commit 6ba6daf188
14 changed files with 196 additions and 27 deletions
+48
View File
@@ -518,6 +518,42 @@ async function copyMsgContent(msg: AiMessage): Promise<void> {
})
}
// ── 消息气泡复制为图片(html2canvas 渲染 PNG 写入剪贴板) ──
// 原生 ClipboardItem 即可写图片到剪贴板,故不加 Tauri 剪贴板插件(少装依赖)
async function copyMsgAsImage(msg: AiMessage): Promise<void> {
const bubble = messagesContainer.value?.querySelector<HTMLElement>(
`.ai-msg[data-msg-id="${msg.id}"] .ai-msg-bubble`,
)
if (!bubble) {
emit('toast', { msg: t('aiChat.copyAsImageFailed'), type: 'error' })
return
}
try {
const { default: html2canvas } = await import('html2canvas')
const canvas = await html2canvas(bubble, { backgroundColor: null, useCORS: true, scale: 2 })
const blob = await new Promise<Blob | null>((resolve) => canvas.toBlob(resolve, 'image/png'))
if (!blob) throw new Error('toBlob failed')
if (typeof ClipboardItem !== 'undefined') {
try {
await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })])
emit('toast', { msg: t('aiChat.copiedAsImage'), type: 'info' })
return
} catch {
// 剪贴板被拒/不可用时降级下载,保证用户仍能拿到图片
}
}
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'message.png'
a.click()
URL.revokeObjectURL(url)
emit('toast', { msg: t('aiChat.copiedAsImage'), type: 'info' })
} catch {
emit('toast', { msg: t('aiChat.copyAsImageFailed'), type: 'error' })
}
}
// ── UX-03: 错误气泡操作入口 ──
// errorType 经 useAiEvents push 错误消息时附带(types.ts AiMessage 未含此字段,
// 经 cast 读写闭环在 AiChat.vue / useAiEvents 两端,不污染 types.ts 定义)。
@@ -857,6 +893,14 @@ defineExpose({
>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
</button>
<button
v-if="item.msg.content"
class="ai-copy-btn ai-copy-btn--ai ai-copy-btn--image"
:title="$t('aiChat.copyAsImage')"
@click.stop="copyMsgAsImage(item.msg)"
>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg>
</button>
</div>
<!-- 流式开始但尚无文本时显示光标 -->
<div v-else-if="isLastAi(item.msg) && isViewingGenerating" class="ai-msg-bubble ai-msg-bubble--ai">
@@ -1239,6 +1283,10 @@ defineExpose({
.ai-copy-btn:hover {
background: rgba(0, 0, 0, 0.55);
}
/* 图片复制按钮排在文本复制按钮左侧(right 让位 22px 按钮 + 4px 间距) */
.ai-copy-btn--image {
right: 30px;
}
/* ── UX-09: 末条 user 消息编辑按钮(hover 显示,回填输入框编辑重生成) ── */
.ai-msg-edit-btn {