feat: 消息级别分享功能

- 新增消息选择模式,支持在当前对话内多选消息分享
- MessageBubble 添加选择模式UI(复选框、选中样式)
- MessageList 添加选择操作栏(全选、取消、确认分享)
- ShareModal 支持消息分享和对话分享两种模式
- 后端分享API支持直接传递消息数据
- chat store 新增消息选择状态和方法

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
肖应宇 2026-03-25 16:18:29 +08:00
parent 9566c6e0c4
commit 7b4fb72cdc
8 changed files with 483 additions and 41 deletions

View File

@ -23,7 +23,8 @@ from core import log_error, log_info
class CreateShareRequest(BaseModel): class CreateShareRequest(BaseModel):
"""创建分享请求""" """创建分享请求"""
conversationIds: List[str] conversationIds: List[str] = []
messages: Optional[List[dict]] = None # 直接传递消息数据(消息分享模式)
passwordHash: str passwordHash: str
expiresIn: Optional[int] = 604800 # 默认7天 expiresIn: Optional[int] = 604800 # 默认7天
@ -42,27 +43,68 @@ async def create_share_handler(data: dict):
请求体: 请求体:
{ {
"conversationIds": ["conv-1", "conv-2"], "conversationIds": ["conv-1", "conv-2"],
"messages": [...], // 可选消息分享模式
"passwordHash": "sha256-hash", "passwordHash": "sha256-hash",
"expiresIn": 604800 "expiresIn": 604800
} }
""" """
try: try:
conversation_ids = data.get("conversationIds", []) conversation_ids = data.get("conversationIds", [])
messages = data.get("messages") # 消息分享模式
password_hash = data.get("passwordHash", "") password_hash = data.get("passwordHash", "")
expires_in = data.get("expiresIn", 604800) expires_in = data.get("expiresIn", 604800)
if not conversation_ids:
raise HTTPException(status_code=400, detail="请选择要分享的对话")
if len(conversation_ids) > 10:
raise HTTPException(status_code=400, detail="最多分享10个对话")
if not password_hash: if not password_hash:
raise HTTPException(status_code=400, detail="请设置访问密码") raise HTTPException(status_code=400, detail="请设置访问密码")
# 获取数据库实例 # 获取数据库实例
db = get_db() db = get_db()
# 计算过期时间
now = int(datetime.now(timezone.utc).timestamp() * 1000)
expires_at = now + (expires_in * 1000)
# 消息分享模式
if messages:
if len(messages) == 0:
raise HTTPException(status_code=400, detail="请选择要分享的消息")
# 创建虚拟对话
virtual_conv_id = str(uuid.uuid4())
virtual_conversation = {
"id": virtual_conv_id,
"title": f"分享的消息 ({len(messages)}条)",
"messages": messages,
"createdAt": now,
"updatedAt": now,
}
# 创建分享记录
share_data = {
"conversationIds": [virtual_conv_id],
"conversations": [virtual_conversation],
"passwordHash": password_hash,
"expiresAt": expires_at,
}
# 保存到数据库
share = db.create_share(share_data)
log_info(f"[分享] 消息分享创建成功: {share['id']}, 消息数: {len(messages)}")
return {
"id": share["id"],
"shareUrl": f"/#/share/{share['id']}",
"expiresAt": share["expiresAt"],
}
# 对话分享模式
if not conversation_ids:
raise HTTPException(status_code=400, detail="请选择要分享的对话")
if len(conversation_ids) > 10:
raise HTTPException(status_code=400, detail="最多分享10个对话")
# 获取对话数据(快照) # 获取对话数据(快照)
conversations = [] conversations = []
for conv_id in conversation_ids: for conv_id in conversation_ids:
@ -81,10 +123,6 @@ async def create_share_handler(data: dict):
if not conversations: if not conversations:
raise HTTPException(status_code=404, detail="未找到有效的对话") raise HTTPException(status_code=404, detail="未找到有效的对话")
# 计算过期时间
now = int(datetime.now(timezone.utc).timestamp() * 1000)
expires_at = now + (expires_in * 1000)
# 创建分享记录 # 创建分享记录
share_data = { share_data = {
"conversationIds": conversation_ids, "conversationIds": conversation_ids,

View File

@ -1,5 +1,29 @@
<template> <template>
<div class="message-list-container"> <div class="message-list-container">
<!-- 消息选择操作栏 -->
<Transition name="slide-down">
<div v-if="isMessageSelectMode" class="message-select-bar">
<div class="select-info">
<span class="select-count">已选择 {{ selectedMessageCount }} 条消息</span>
</div>
<div class="select-actions">
<button class="action-btn select-all" @click="handleSelectAll">
全选
</button>
<button class="action-btn cancel" @click="handleCancelSelect">
取消
</button>
<button
class="action-btn confirm"
:disabled="selectedMessageCount === 0"
@click="handleConfirmShare"
>
确认分享
</button>
</div>
</div>
</Transition>
<div ref="containerRef" class="message-list" @scroll="handleScroll"> <div ref="containerRef" class="message-list" @scroll="handleScroll">
<!-- 欢迎界面 --> <!-- 欢迎界面 -->
<WelcomeScreen <WelcomeScreen
@ -18,6 +42,8 @@
:show-timestamp="showTimestamp" :show-timestamp="showTimestamp"
:compact="compact" :compact="compact"
:is-New="index === visibleMessages.length - 1" :is-New="index === visibleMessages.length - 1"
:is-message-select-mode="isMessageSelectMode"
:is-selected="isMessageSelected(message.id)"
@retry="$emit('retry', message.id)" @retry="$emit('retry', message.id)"
@regenerate="$emit('regenerate', message.id)" @regenerate="$emit('regenerate', message.id)"
@copy="handleCopy(message)" @copy="handleCopy(message)"
@ -27,6 +53,8 @@
@preview-image="handlePreviewImage" @preview-image="handlePreviewImage"
@play-video="handlePlayVideo" @play-video="handlePlayVideo"
@download-file="handleDownloadFile" @download-file="handleDownloadFile"
@toggle-select="handleToggleMessageSelect(message.id)"
@enter-select-mode="handleEnterSelectMode(message.id)"
/> />
</TransitionGroup> </TransitionGroup>
@ -64,6 +92,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch, nextTick, onMounted, computed } from "vue"; import { ref, watch, nextTick, onMounted, computed } from "vue";
import { useChatStore } from "@/stores/chat"; import { useChatStore } from "@/stores/chat";
import { useSettingsStore } from "@/stores/settings";
import MessageBubble from "@/components/message/MessageBubble.vue"; import MessageBubble from "@/components/message/MessageBubble.vue";
import WelcomeScreen from "./WelcomeScreen.vue"; import WelcomeScreen from "./WelcomeScreen.vue";
import { Bot, ChevronDown } from "@/components/icons"; import { Bot, ChevronDown } from "@/components/icons";
@ -101,6 +130,11 @@ const emit = defineEmits<{
}>(); }>();
const chatStore = useChatStore(); const chatStore = useChatStore();
const settingsStore = useSettingsStore();
//
const isMessageSelectMode = computed(() => chatStore.isMessageSelectMode);
const selectedMessageCount = computed(() => chatStore.selectedMessageCount);
// //
const containerRef = ref<HTMLElement | null>(null); const containerRef = ref<HTMLElement | null>(null);
@ -180,6 +214,33 @@ function handleDownloadFile(file: Attachment) {
emit("download-file", file); emit("download-file", file);
} }
//
function handleEnterSelectMode(messageId: string) {
chatStore.enterMessageSelectMode(chatStore.currentConversationId || '', messageId);
}
function handleToggleMessageSelect(messageId: string) {
chatStore.toggleMessageSelection(messageId);
}
function handleSelectAll() {
chatStore.selectAllMessages();
}
function handleCancelSelect() {
chatStore.exitMessageSelectMode();
}
function handleConfirmShare() {
if (chatStore.selectedMessageCount > 0) {
settingsStore.openShareModal();
}
}
function isMessageSelected(messageId: string): boolean {
return chatStore.isMessageSelected(messageId);
}
// //
watch( watch(
() => visibleMessages.value.length, () => visibleMessages.value.length,
@ -408,4 +469,97 @@ onMounted(() => {
opacity: 1; opacity: 1;
} }
} }
//
.message-select-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 20px;
background: white;
border-bottom: 1px solid #e5e7eb;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
.dark & {
background: #1e1e2e;
border-bottom-color: #2d2d3d;
}
}
.select-info {
display: flex;
align-items: center;
gap: 12px;
}
.select-count {
font-size: 14px;
font-weight: 500;
color: #374151;
.dark & {
color: #e5e7eb;
}
}
.select-actions {
display: flex;
align-items: center;
gap: 8px;
}
.action-btn {
padding: 8px 16px;
border: none;
border-radius: 8px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
&.select-all,
&.cancel {
background: #f3f4f6;
color: #6b7280;
.dark & {
background: #2d2d3d;
color: #9ca3af;
}
&:hover {
background: #e5e7eb;
.dark & {
background: #374151;
}
}
}
&.confirm {
background: #3b82f6;
color: white;
&:hover:not(:disabled) {
background: #2563eb;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
}
//
.slide-down-enter-active,
.slide-down-leave-active {
transition: all 0.3s ease;
}
.slide-down-enter-from,
.slide-down-leave-to {
opacity: 0;
transform: translateY(-100%);
}
</style> </style>

View File

@ -8,11 +8,21 @@
'is-end': !message.isEnd && message.role !== 'user', 'is-end': !message.isEnd && message.role !== 'user',
'is-error': message.isError, 'is-error': message.isError,
compact: compact, compact: compact,
'message-select-mode': isMessageSelectMode,
'message-selected': isSelected,
}, },
]" ]"
@click="handleBubbleClick"
@mouseenter="isHovered = true" @mouseenter="isHovered = true"
@mouseleave="isHovered = false" @mouseleave="isHovered = false"
> >
<!-- 消息选择模式复选框 -->
<div v-if="isMessageSelectMode" class="message-checkbox" @click.stop="handleToggleSelect">
<div class="checkbox" :class="{ checked: isSelected }">
<Check v-if="isSelected" :size="14" />
</div>
</div>
<!-- 头像 --> <!-- 头像 -->
<div class="avatar"> <div class="avatar">
<div class="avatar-inner" :class="message.role"> <div class="avatar-inner" :class="message.role">
@ -164,7 +174,8 @@
v-if=" v-if="
message.role === 'assistant' && message.role === 'assistant' &&
!message.isStreaming && !message.isStreaming &&
!message.isError !message.isError &&
!isMessageSelectMode
" "
:content="message.content.text || ''" :content="message.content.text || ''"
:feedback="message.feedback" :feedback="message.feedback"
@ -176,6 +187,7 @@
@like="handleLike" @like="handleLike"
@dislike="handleDislike" @dislike="handleDislike"
@regenerate="$emit('regenerate')" @regenerate="$emit('regenerate')"
@share="handleShareClick"
/> />
</div> </div>
</div> </div>
@ -195,6 +207,7 @@ import {
Zap, Zap,
Maximize2, Maximize2,
Play, Play,
Check,
} from "@/components/icons"; } from "@/components/icons";
import MessageActions from "./MessageActions.vue"; import MessageActions from "./MessageActions.vue";
import { formatFileSize, getFileIcon } from "@/utils/helpers"; import { formatFileSize, getFileIcon } from "@/utils/helpers";
@ -208,10 +221,14 @@ const props = withDefaults(
showTimestamp?: boolean; showTimestamp?: boolean;
compact?: boolean; compact?: boolean;
isNew?: boolean; isNew?: boolean;
isMessageSelectMode?: boolean;
isSelected?: boolean;
}>(), }>(),
{ {
showTimestamp: true, showTimestamp: true,
compact: false, compact: false,
isMessageSelectMode: false,
isSelected: false,
}, },
); );
const { copy } = useClipboard({ legacy: true }); const { copy } = useClipboard({ legacy: true });
@ -225,10 +242,29 @@ const emit = defineEmits<{
"preview-image": [image: Attachment, index: number]; "preview-image": [image: Attachment, index: number];
"play-video": [video: VideoInfo]; "play-video": [video: VideoInfo];
"download-file": [file: Attachment]; "download-file": [file: Attachment];
"toggle-select": [];
"enter-select-mode": [];
}>(); }>();
const isHovered = ref(false); const isHovered = ref(false);
//
function handleBubbleClick() {
if (props.isMessageSelectMode) {
emit("toggle-select");
}
}
//
function handleToggleSelect() {
emit("toggle-select");
}
//
function handleShareClick() {
emit("enter-select-mode");
}
function getFileEmoji(mimeType?: string) { function getFileEmoji(mimeType?: string) {
return getFileIcon(mimeType || ""); return getFileIcon(mimeType || "");
} }
@ -274,6 +310,28 @@ setCustomComponents("playground-demo", {
padding: 20px 10%; padding: 20px 10%;
animation: fadeIn 0.3s ease; animation: fadeIn 0.3s ease;
//
&.message-select-mode {
cursor: pointer;
transition: background 0.2s ease;
&:hover {
background: rgba(59, 130, 246, 0.05);
.dark & {
background: rgba(59, 130, 246, 0.1);
}
}
}
&.message-selected {
background: rgba(59, 130, 246, 0.1);
.dark & {
background: rgba(59, 130, 246, 0.15);
}
}
&.role-user { &.role-user {
flex-direction: row-reverse; flex-direction: row-reverse;
@ -830,4 +888,41 @@ setCustomComponents("playground-demo", {
opacity: 1; opacity: 1;
} }
} }
//
.message-checkbox {
position: absolute;
left: 10px;
top: 50%;
transform: translateY(-50%);
z-index: 10;
.checkbox {
width: 22px;
height: 22px;
border: 2px solid #d1d5db;
border-radius: 6px;
background: white;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
cursor: pointer;
.dark & {
border-color: #4b5563;
background: #2d2d3d;
}
&.checked {
background: #3b82f6;
border-color: #3b82f6;
color: white;
}
&:hover {
border-color: #3b82f6;
}
}
}
</style> </style>

View File

@ -4,15 +4,35 @@
<div v-if="show" class="modal-overlay" @click.self="handleClose"> <div v-if="show" class="modal-overlay" @click.self="handleClose">
<div class="modal-container"> <div class="modal-container">
<div class="modal-header"> <div class="modal-header">
<h3>分享对话</h3> <h3>{{ isMessageShare ? '分享消息' : '分享对话' }}</h3>
<button class="close-btn" @click="handleClose"> <button class="close-btn" @click="handleClose">
<X :size="20" /> <X :size="20" />
</button> </button>
</div> </div>
<div class="modal-content"> <div class="modal-content">
<!-- 已选择的对话预览 --> <!-- 消息分享预览 -->
<div class="selected-preview"> <div v-if="isMessageShare" class="selected-preview">
<div class="preview-header">
<span class="preview-title">已选择 {{ selectedMessageCount }} 条消息</span>
<span class="preview-hint">来自当前对话</span>
</div>
<div class="preview-list messages-preview">
<div
v-for="msg in selectedMessages"
:key="msg.id"
class="preview-item message-item"
>
<span class="message-role" :class="msg.role">
{{ msg.role === 'user' ? '用户' : 'AI' }}
</span>
<span class="item-title">{{ getMessagePreview(msg) }}</span>
</div>
</div>
</div>
<!-- 对话分享预览 -->
<div v-else class="selected-preview">
<div class="preview-header"> <div class="preview-header">
<span class="preview-title">已选择 {{ selectedCount }} 个对话</span> <span class="preview-title">已选择 {{ selectedCount }} 个对话</span>
<span class="preview-hint">最多分享 10 个对话</span> <span class="preview-hint">最多分享 10 个对话</span>
@ -86,6 +106,7 @@ import { useSettingsStore } from "@/stores/settings";
import { hashPassword } from "@/utils/crypto"; import { hashPassword } from "@/utils/crypto";
import { shareApi } from "@/services/shareApi"; import { shareApi } from "@/services/shareApi";
import { SHARE_LIMITS } from "@/types/share"; import { SHARE_LIMITS } from "@/types/share";
import type { Message } from "@/types/chat";
import { import {
X, X,
Lock, Lock,
@ -99,7 +120,10 @@ const settingsStore = useSettingsStore();
const chatStore = useChatStore(); const chatStore = useChatStore();
const show = computed(() => settingsStore.showShareModal); const show = computed(() => settingsStore.showShareModal);
const { selectedConversations, selectedCount } = storeToRefs(chatStore); const { selectedConversations, selectedCount, isMessageSelectMode, selectedMessages, selectedMessageCount } = storeToRefs(chatStore);
//
const isMessageShare = computed(() => isMessageSelectMode.value);
const password = ref(""); const password = ref("");
const showPassword = ref(false); const showPassword = ref(false);
@ -109,6 +133,12 @@ const isValidPassword = computed(() => {
return password.value.length >= 4 && password.value.length <= 20; return password.value.length >= 4 && password.value.length <= 20;
}); });
//
function getMessagePreview(message: Message): string {
const text = message.content.text || '';
return text.length > 50 ? text.substring(0, 50) + '...' : text;
}
function togglePasswordVisibility() { function togglePasswordVisibility() {
showPassword.value = !showPassword.value; showPassword.value = !showPassword.value;
const input = document.querySelector('.password-input') as HTMLInputElement; const input = document.querySelector('.password-input') as HTMLInputElement;
@ -120,27 +150,43 @@ function togglePasswordVisibility() {
async function handleCreateShare() { async function handleCreateShare() {
if (!isValidPassword.value || isCreating.value) return; if (!isValidPassword.value || isCreating.value) return;
//
if (selectedCount.value > SHARE_LIMITS.MAX_CONVERSATIONS) {
window.$toast?.(`最多分享 ${SHARE_LIMITS.MAX_CONVERSATIONS} 个对话`, 'error');
return;
}
isCreating.value = true; isCreating.value = true;
try { try {
// //
const passwordHash = await hashPassword(password.value); const passwordHash = await hashPassword(password.value);
// let result;
const conversationIds = selectedConversations.value.map(c => c.id);
// API if (isMessageShare.value) {
const result = await shareApi.createShare({ //
conversationIds, result = await shareApi.createShare({
passwordHash, conversationIds: [],
expiresIn: SHARE_LIMITS.DEFAULT_EXPIRE_SECONDS, messages: selectedMessages.value.map(m => ({
}); id: m.id,
role: m.role,
content: m.content,
timestamp: m.timestamp,
})),
passwordHash,
expiresIn: SHARE_LIMITS.DEFAULT_EXPIRE_SECONDS,
});
} else {
//
//
if (selectedCount.value > SHARE_LIMITS.MAX_CONVERSATIONS) {
window.$toast?.(`最多分享 ${SHARE_LIMITS.MAX_CONVERSATIONS} 个对话`, 'error');
return;
}
const conversationIds = selectedConversations.value.map(c => c.id);
result = await shareApi.createShare({
conversationIds,
passwordHash,
expiresIn: SHARE_LIMITS.DEFAULT_EXPIRE_SECONDS,
});
}
// //
handleClose() handleClose()
@ -159,7 +205,11 @@ async function handleCreateShare() {
// 退 // 退
password.value = ""; password.value = "";
chatStore.exitSelectMode(); if (isMessageShare.value) {
chatStore.exitMessageSelectMode();
} else {
chatStore.exitSelectMode();
}
} catch (error) { } catch (error) {
console.error('Failed to create share:', error); console.error('Failed to create share:', error);
@ -327,6 +377,38 @@ watch(show, (newVal: boolean) => {
color: #9ca3af; color: #9ca3af;
} }
} }
&.messages-preview {
max-height: 200px;
}
.message-item {
flex-direction: column;
align-items: flex-start;
gap: 4px;
.message-role {
font-size: 11px;
font-weight: 500;
padding: 2px 8px;
border-radius: 4px;
&.user {
background: rgba(59, 130, 246, 0.1);
color: #3b82f6;
}
&.assistant {
background: rgba(16, 185, 129, 0.1);
color: #10b981;
}
}
.item-title {
font-size: 12px;
line-height: 1.4;
}
}
} }
.password-section { .password-section {

View File

@ -269,7 +269,7 @@ function handleDelete() {
.action-btn { .action-btn {
display: flex; display: flex;
align-items: center; align-items: center;
justify: center; justify-content: center;
width: 26px; width: 26px;
height: 26px; height: 26px;
border: none; border: none;

View File

@ -19,10 +19,15 @@ export const useChatStore = defineStore("chat", () => {
const isInitialized = ref(false); const isInitialized = ref(false);
const isLoading = ref(false); const isLoading = ref(false);
// 分享多选模式状态 // 分享多选模式状态(对话级别)
const isSelectMode = ref(false); const isSelectMode = ref(false);
const selectedConversationIds = ref<string[]>([]); const selectedConversationIds = ref<string[]>([]);
// 消息分享选择模式状态
const isMessageSelectMode = ref(false);
const selectedMessageIds = ref<string[]>([]);
const sourceConversationId = ref<string | null>(null);
// 计算属性 // 计算属性
const currentConversation = computed(() => { const currentConversation = computed(() => {
return ( return (
@ -55,6 +60,16 @@ export const useChatStore = defineStore("chat", () => {
// 选中的对话数量 // 选中的对话数量
const selectedCount = computed(() => selectedConversationIds.value.length); const selectedCount = computed(() => selectedConversationIds.value.length);
// 选中的消息列表
const selectedMessages = computed(() => {
const conv = conversations.value.find((c) => c.id === sourceConversationId.value);
if (!conv) return [];
return conv.messages.filter((m) => selectedMessageIds.value.includes(m.id));
});
// 选中的消息数量
const selectedMessageCount = computed(() => selectedMessageIds.value.length);
// 初始化方法 - 从后端 API 加载数据 // 初始化方法 - 从后端 API 加载数据
async function initializeFromApi() { async function initializeFromApi() {
if (isInitialized.value || isLoading.value) return; if (isInitialized.value || isLoading.value) return;
@ -546,6 +561,47 @@ export const useChatStore = defineStore("chat", () => {
return selectedConversationIds.value.includes(id); return selectedConversationIds.value.includes(id);
} }
// ========== 消息分享选择模式方法 ==========
// 进入消息选择模式
function enterMessageSelectMode(conversationId: string, initialMessageId?: string) {
isMessageSelectMode.value = true;
sourceConversationId.value = conversationId;
selectedMessageIds.value = initialMessageId ? [initialMessageId] : [];
}
// 退出消息选择模式
function exitMessageSelectMode() {
isMessageSelectMode.value = false;
sourceConversationId.value = null;
selectedMessageIds.value = [];
}
// 切换消息选择状态
function toggleMessageSelection(messageId: string) {
const index = selectedMessageIds.value.indexOf(messageId);
if (index === -1) {
selectedMessageIds.value.push(messageId);
} else {
selectedMessageIds.value.splice(index, 1);
}
}
// 全选当前对话消息
function selectAllMessages() {
const conv = conversations.value.find((c) => c.id === sourceConversationId.value);
if (conv) {
selectedMessageIds.value = conv.messages
.filter((m) => m.role !== MessageRole.SYSTEM)
.map((m) => m.id);
}
}
// 检查消息是否被选中
function isMessageSelected(messageId: string): boolean {
return selectedMessageIds.value.includes(messageId);
}
// 初始化 // 初始化
initializeFromApi(); initializeFromApi();
@ -560,6 +616,12 @@ export const useChatStore = defineStore("chat", () => {
// 分享多选模式状态 // 分享多选模式状态
isSelectMode, isSelectMode,
selectedConversationIds, selectedConversationIds,
// 消息分享选择模式状态
isMessageSelectMode,
selectedMessageIds,
sourceConversationId,
selectedMessages,
selectedMessageCount,
// 计算属性 // 计算属性
currentConversation, currentConversation,
sortedConversations, sortedConversations,
@ -595,5 +657,11 @@ export const useChatStore = defineStore("chat", () => {
selectAllConversations, selectAllConversations,
clearSelection, clearSelection,
isConversationSelected, isConversationSelected,
// 消息分享选择模式方法
enterMessageSelectMode,
exitMessageSelectMode,
toggleMessageSelection,
selectAllMessages,
isMessageSelected,
}; };
}); });

View File

@ -18,10 +18,21 @@ export interface Share {
*/ */
export interface ShareCreateRequest { export interface ShareCreateRequest {
conversationIds: string[]; // 要分享的对话ID列表 conversationIds: string[]; // 要分享的对话ID列表
messages?: MessageData[]; // 直接传递消息数据(消息分享模式)
passwordHash: string; // 密码哈希值 passwordHash: string; // 密码哈希值
expiresIn?: number; // 过期时间默认7天 expiresIn?: number; // 过期时间默认7天
} }
/**
*
*/
export interface MessageData {
id: string;
role: string;
content: any;
timestamp: number;
}
/** /**
* *
*/ */

View File

@ -73,10 +73,10 @@
{{ shareData?.conversations?.length || 0 }} 个对话 {{ shareData?.conversations?.length || 0 }} 个对话
</span> </span>
</div> </div>
<button class="theme-toggle" @click="toggleTheme"> <!-- <button class="theme-toggle" @click="toggleTheme">
<Sun v-if="isDark" :size="20" /> <Sun v-if="isDark" :size="20" />
<Moon v-else :size="20" /> <Moon v-else :size="20" />
</button> </button> -->
</header> </header>
<!-- 对话切换标签 --> <!-- 对话切换标签 -->
@ -126,8 +126,6 @@ import {
AlertCircle, AlertCircle,
Eye, Eye,
EyeOff, EyeOff,
Sun,
Moon,
} from '@/components/icons' } from '@/components/icons'
const route = useRoute() const route = useRoute()
@ -171,10 +169,6 @@ const visibleMessages = computed(() => {
}) })
// //
function toggleTheme() {
settingsStore.toggleTheme()
}
async function loadShareInfo() { async function loadShareInfo() {
const shareId = route.params.id as string const shareId = route.params.id as string
if (!shareId) { if (!shareId) {