ai-chat-ui/src/components/message/MessageActions.vue

321 lines
5.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="message-actions" :class="{ visible: alwaysVisible || isHovered }">
<!-- 复制按钮 -->
<button
v-if="!isBreak"
class="action-btn"
:class="{ success: copied }"
title="复制内容"
@click="handleCopy"
>
<Check v-if="copied" :size="15" />
<Copy v-else :size="15" />
</button>
<!-- 点赞按钮 -->
<button
v-if="isNew && !isBreak"
class="action-btn"
:class="{ active: feedback?.liked }"
title="有帮助"
@click="handleLike"
>
<ThumbsUp :size="15" />
</button>
<!-- 点踩按钮 -->
<button
v-if="isNew && !isBreak"
class="action-btn"
:class="{ active: feedback?.disliked }"
title="没帮助"
@click="handleDislike"
>
<ThumbsDown :size="15" />
</button>
<!-- 重新生成仅AI消息 -->
<button
v-if="(showRegenerate && isNew) || isBreak"
class="action-btn"
title="重新生成"
@click="handleRegenerate"
>
<RefreshCw :size="15" />
</button>
<!-- 更多操作 -->
<div class="more-menu" v-if="showMore">
<button class="action-btn" title="更多" @click="toggleMoreMenu">
<MoreHorizontal :size="15" />
</button>
<Transition name="dropdown">
<div v-if="showMoreMenu" class="dropdown-menu">
<button
v-if="isNew && !isBreak"
class="dropdown-item"
@click="handleEdit"
>
<Edit3 :size="14" />
<span>编辑</span>
</button>
<button v-if="!isBreak" class="dropdown-item" @click="handleShare">
<ExternalLink :size="14" />
<span>分享</span>
</button>
<button class="dropdown-item danger" @click="handleDelete">
<Trash2 :size="14" />
<span>删除</span>
</button>
</div>
</Transition>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import {
Copy,
Check,
ThumbsUp,
ThumbsDown,
RefreshCw,
MoreHorizontal,
Edit3,
ExternalLink,
Trash2,
} from "@/components/icons";
import { copyToClipboard } from "@/utils/helpers";
import type { MessageFeedback } from "@/types/chat";
const props = withDefaults(
defineProps<{
content: string;
feedback?: MessageFeedback;
showRegenerate?: boolean;
showMore?: boolean;
alwaysVisible?: boolean;
isHovered?: boolean;
isNew?: boolean;
isBreak?: boolean;
}>(),
{
showRegenerate: false,
showMore: true,
alwaysVisible: false,
isHovered: false,
},
);
const emit = defineEmits<{
copy: [];
like: [];
dislike: [];
regenerate: [];
edit: [];
share: [];
delete: [];
}>();
const copied = ref(false);
const showMoreMenu = ref(false);
async function handleCopy() {
const success = await copyToClipboard(props.content);
if (success) {
copied.value = true;
emit("copy");
setTimeout(() => {
copied.value = false;
}, 2000);
}
}
function handleLike() {
emit("like");
}
function handleDislike() {
emit("dislike");
}
function handleRegenerate() {
emit("regenerate");
}
function toggleMoreMenu() {
showMoreMenu.value = !showMoreMenu.value;
}
function handleEdit() {
showMoreMenu.value = false;
emit("edit");
}
function handleShare() {
showMoreMenu.value = false;
emit("share");
}
function handleDelete() {
showMoreMenu.value = false;
emit("delete");
}
// 点击外部关闭菜单
function handleClickOutside(event: MouseEvent) {
const target = event.target as HTMLElement;
if (!target.closest(".more-menu")) {
showMoreMenu.value = false;
}
}
// 挂载时添加事件监听
if (typeof window !== "undefined") {
document.addEventListener("click", handleClickOutside);
}
</script>
<style lang="scss" scoped>
.message-actions {
display: flex;
align-items: center;
gap: 4px;
padding: 4px;
border-radius: 10px;
background: white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
opacity: 0;
transform: translateY(4px);
transition: all 0.2s ease;
pointer-events: none;
.dark & {
background: #2d2d3d;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
&.visible {
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
}
.action-btn {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border: none;
border-radius: 8px;
background: transparent;
color: #6b7280;
cursor: pointer;
transition: all 0.15s ease;
&:hover {
background: #f3f4f6;
color: #374151;
.dark & {
background: #374151;
color: #e5e7eb;
}
}
&.active {
color: #3b82f6;
&:hover {
background: rgba(59, 130, 246, 0.1);
}
}
&.success {
color: #10b981;
&:hover {
background: rgba(16, 185, 129, 0.1);
}
}
}
.more-menu {
position: relative;
}
.dropdown-menu {
position: absolute;
bottom: 100%;
right: 0;
margin-bottom: 8px;
min-width: 140px;
padding: 6px;
background: white;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
z-index: 100;
.dark & {
background: #2d2d3d;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
}
}
.dropdown-item {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
padding: 10px 12px;
border: none;
border-radius: 8px;
background: transparent;
color: #374151;
font-size: 13px;
text-align: left;
cursor: pointer;
transition: all 0.15s ease;
.dark & {
color: #e5e7eb;
}
&:hover {
background: #f3f4f6;
.dark & {
background: #374151;
}
}
&.danger {
color: #ef4444;
&:hover {
background: rgba(239, 68, 68, 0.1);
}
}
svg {
flex-shrink: 0;
}
}
// 下拉动画
.dropdown-enter-active,
.dropdown-leave-active {
transition: all 0.2s ease;
}
.dropdown-enter-from,
.dropdown-leave-to {
opacity: 0;
transform: translateY(8px) scale(0.95);
}
</style>