321 lines
5.9 KiB
Vue
321 lines
5.9 KiB
Vue
<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: 14px;
|
||
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>
|