mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-05-24 23:51:43 +08:00
feat: 拆分chatStore
This commit is contained in:
+15
-8
@@ -1,15 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
import { useChatStore } from '@/stores/chat'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useRoute } from 'vue-router'
|
||||
import Sidebar from '@/components/common/Sidebar.vue'
|
||||
import SettingModal from '@/components/common/SettingModal.vue'
|
||||
import ScreenCaptureModal from '@/components/common/ScreenCaptureModal.vue'
|
||||
import { ChatRecordDrawer } from '@/components/common/ChatRecord'
|
||||
import { useSessionStore } from '@/stores/session'
|
||||
import { useLayoutStore } from '@/stores/layout'
|
||||
import { usePromptStore } from '@/stores/prompt'
|
||||
|
||||
const chatStore = useChatStore()
|
||||
const { isInitialized } = storeToRefs(chatStore)
|
||||
const sessionStore = useSessionStore()
|
||||
const layoutStore = useLayoutStore()
|
||||
const promptStore = usePromptStore()
|
||||
const { isInitialized } = storeToRefs(sessionStore)
|
||||
const route = useRoute()
|
||||
|
||||
const tooltip = {
|
||||
@@ -18,7 +22,7 @@ const tooltip = {
|
||||
|
||||
// 应用启动时从数据库加载会话列表
|
||||
onMounted(async () => {
|
||||
await chatStore.loadSessions()
|
||||
await sessionStore.loadSessions()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -44,11 +48,14 @@ onMounted(async () => {
|
||||
</main>
|
||||
</template>
|
||||
</div>
|
||||
<SettingModal v-model:open="chatStore.showSettingModal" @ai-config-saved="chatStore.notifyAIConfigChanged" />
|
||||
<SettingModal
|
||||
v-model:open="layoutStore.showSettingModal"
|
||||
@ai-config-saved="promptStore.notifyAIConfigChanged"
|
||||
/>
|
||||
<ScreenCaptureModal
|
||||
:open="chatStore.showScreenCaptureModal"
|
||||
:image-data="chatStore.screenCaptureImage"
|
||||
@update:open="(v) => (v ? null : chatStore.closeScreenCaptureModal())"
|
||||
:open="layoutStore.showScreenCaptureModal"
|
||||
:image-data="layoutStore.screenCaptureImage"
|
||||
@update:open="(v) => (v ? null : layoutStore.closeScreenCaptureModal())"
|
||||
/>
|
||||
<!-- 全局聊天记录查看器 -->
|
||||
<ChatRecordDrawer />
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'
|
||||
import { useChatStore } from '@/stores/chat'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import ConversationList from './ConversationList.vue'
|
||||
import DataSourcePanel from './DataSourcePanel.vue'
|
||||
@@ -8,6 +7,7 @@ import ChatMessage from './ChatMessage.vue'
|
||||
import ChatInput from './ChatInput.vue'
|
||||
import { useAIChat } from '@/composables/useAIChat'
|
||||
import CaptureButton from '@/components/common/CaptureButton.vue'
|
||||
import { usePromptStore } from '@/stores/prompt'
|
||||
|
||||
// Props
|
||||
const props = defineProps<{
|
||||
@@ -36,8 +36,8 @@ const {
|
||||
} = useAIChat(props.sessionId, props.timeFilter, props.chatType ?? 'group')
|
||||
|
||||
// Store
|
||||
const chatStore = useChatStore()
|
||||
const { groupPresets, privatePresets, aiPromptSettings } = storeToRefs(chatStore)
|
||||
const promptStore = usePromptStore()
|
||||
const { groupPresets, privatePresets, aiPromptSettings } = storeToRefs(promptStore)
|
||||
|
||||
// 当前聊天类型
|
||||
const currentChatType = computed(() => props.chatType ?? 'group')
|
||||
@@ -63,9 +63,9 @@ const isPresetPopoverOpen = ref(false)
|
||||
// 设置激活预设
|
||||
function setActivePreset(presetId: string) {
|
||||
if (currentChatType.value === 'group') {
|
||||
chatStore.setActiveGroupPreset(presetId)
|
||||
promptStore.setActiveGroupPreset(presetId)
|
||||
} else {
|
||||
chatStore.setActivePrivatePreset(presetId)
|
||||
promptStore.setActivePrivatePreset(presetId)
|
||||
}
|
||||
// 关闭下拉菜单
|
||||
isPresetPopoverOpen.value = false
|
||||
@@ -242,7 +242,7 @@ watch(
|
||||
|
||||
// 监听全局 AI 配置变化(从设置弹窗保存时触发)
|
||||
watch(
|
||||
() => (chatStore as unknown as { aiConfigVersion: number }).aiConfigVersion,
|
||||
() => promptStore.aiConfigVersion,
|
||||
async () => {
|
||||
await refreshConfig()
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { RepeatAnalysis } from '@/types/chat'
|
||||
import { ListPro } from '@/components/charts'
|
||||
import { LoadingState, EmptyState, SectionCard } from '@/components/UI'
|
||||
import { formatDate, getRankBadgeClass } from '@/utils'
|
||||
import { useChatStore } from '@/stores/chat'
|
||||
import { useLayoutStore } from '@/stores/layout'
|
||||
|
||||
interface TimeFilter {
|
||||
startTs?: number
|
||||
@@ -16,7 +16,7 @@ const props = defineProps<{
|
||||
timeFilter?: TimeFilter
|
||||
}>()
|
||||
|
||||
const chatStore = useChatStore()
|
||||
const layoutStore = useLayoutStore()
|
||||
|
||||
// ==================== 最火复读内容 ====================
|
||||
const repeatAnalysis = ref<RepeatAnalysis | null>(null)
|
||||
@@ -43,7 +43,7 @@ function truncateContent(content: string, maxLength = 30): string {
|
||||
* 查看复读内容的聊天记录上下文
|
||||
*/
|
||||
function viewRepeatContext(item: { content: string; firstMessageId: number }) {
|
||||
chatStore.openChatRecordDrawer({
|
||||
layoutStore.openChatRecordDrawer({
|
||||
scrollToMessageId: item.firstMessageId,
|
||||
highlightKeywords: [item.content],
|
||||
})
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ListPro } from '@/components/charts'
|
||||
import type { RankItem } from '@/components/charts'
|
||||
import { LoadingState } from '@/components/UI'
|
||||
import { getRankBadgeClass } from '@/utils'
|
||||
import { useChatStore } from '@/stores/chat'
|
||||
import { usePromptStore } from '@/stores/prompt'
|
||||
|
||||
interface TimeFilter {
|
||||
startTs?: number
|
||||
@@ -23,15 +23,8 @@ const props = defineProps<{
|
||||
timeFilter?: TimeFilter
|
||||
}>()
|
||||
|
||||
// 使用类型断言绕过 Pinia persist 插件的类型推断问题
|
||||
const chatStore = useChatStore() as ReturnType<typeof useChatStore> & {
|
||||
customKeywordTemplates: BaseKeywordTemplate[]
|
||||
deletedPresetTemplateIds: string[]
|
||||
addCustomKeywordTemplate: (template: BaseKeywordTemplate) => void
|
||||
updateCustomKeywordTemplate: (id: string, updates: Partial<Omit<BaseKeywordTemplate, 'id'>>) => void
|
||||
removeCustomKeywordTemplate: (id: string) => void
|
||||
addDeletedPresetTemplateId: (id: string) => void
|
||||
}
|
||||
// 使用提示词配置 store 管理关键词模板
|
||||
const promptStore = usePromptStore()
|
||||
|
||||
// 颜色模式:false = 单色,true = 多色
|
||||
const isMultiColor = ref(false)
|
||||
@@ -123,12 +116,12 @@ const PRESET_TEMPLATES: KeywordTemplate[] = [
|
||||
|
||||
// 合并预设和自定义模板
|
||||
const allTemplates = computed<KeywordTemplate[]>(() => {
|
||||
const custom = chatStore.customKeywordTemplates.map((t) => ({
|
||||
const custom = promptStore.customKeywordTemplates.map((t) => ({
|
||||
...t,
|
||||
isCustom: true,
|
||||
}))
|
||||
// 过滤掉已删除的预设模板
|
||||
const activePresets = PRESET_TEMPLATES.filter((t) => !chatStore.deletedPresetTemplateIds.includes(t.id))
|
||||
const activePresets = PRESET_TEMPLATES.filter((t) => !promptStore.deletedPresetTemplateIds.includes(t.id))
|
||||
return [...activePresets, ...custom]
|
||||
})
|
||||
|
||||
@@ -241,12 +234,12 @@ function saveTemplate() {
|
||||
name: templateName.value.trim(),
|
||||
keywords: [...templateKeywords.value],
|
||||
}
|
||||
chatStore.addCustomKeywordTemplate(newTemplate)
|
||||
promptStore.addCustomKeywordTemplate(newTemplate)
|
||||
selectedTemplateId.value = newTemplate.id
|
||||
currentKeywords.value = [...newTemplate.keywords]
|
||||
loadAnalysis()
|
||||
} else {
|
||||
chatStore.updateCustomKeywordTemplate(editingTemplateId.value, {
|
||||
promptStore.updateCustomKeywordTemplate(editingTemplateId.value, {
|
||||
name: templateName.value.trim(),
|
||||
keywords: [...templateKeywords.value],
|
||||
})
|
||||
@@ -261,7 +254,7 @@ function saveTemplate() {
|
||||
name: templateName.value.trim(),
|
||||
keywords: [...templateKeywords.value],
|
||||
}
|
||||
chatStore.addCustomKeywordTemplate(newTemplate)
|
||||
promptStore.addCustomKeywordTemplate(newTemplate)
|
||||
selectedTemplateId.value = newTemplate.id
|
||||
currentKeywords.value = [...newTemplate.keywords]
|
||||
loadAnalysis()
|
||||
@@ -273,9 +266,9 @@ function saveTemplate() {
|
||||
// 删除模板(支持预设和自定义)
|
||||
function deleteTemplate(templateId: string) {
|
||||
if (isPresetTemplate(templateId)) {
|
||||
chatStore.addDeletedPresetTemplateId(templateId)
|
||||
promptStore.addDeletedPresetTemplateId(templateId)
|
||||
} else {
|
||||
chatStore.removeCustomKeywordTemplate(templateId)
|
||||
promptStore.removeCustomKeywordTemplate(templateId)
|
||||
}
|
||||
|
||||
if (selectedTemplateId.value === templateId) {
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
* 主组件,组合筛选面板、消息列表等子组件
|
||||
*/
|
||||
import { ref, watch, computed, toRaw, nextTick } from 'vue'
|
||||
import { useChatStore } from '@/stores/chat'
|
||||
import FilterPanel from './FilterPanel.vue'
|
||||
import ActiveFilters from './ActiveFilters.vue'
|
||||
import MessageList from './MessageList.vue'
|
||||
import type { ChatRecordQuery } from './types'
|
||||
import { useLayoutStore } from '@/stores/layout'
|
||||
|
||||
const chatStore = useChatStore()
|
||||
const layoutStore = useLayoutStore()
|
||||
|
||||
// 消息列表组件引用
|
||||
const messageListRef = ref<InstanceType<typeof MessageList> | null>(null)
|
||||
@@ -72,11 +72,11 @@ function handleCountChange(count: number) {
|
||||
|
||||
// 监听 Drawer 打开
|
||||
watch(
|
||||
() => chatStore.showChatRecordDrawer,
|
||||
() => layoutStore.showChatRecordDrawer,
|
||||
async (isOpen) => {
|
||||
if (isOpen) {
|
||||
// 复制查询参数到本地
|
||||
const query = toRaw(chatStore.chatRecordQuery)
|
||||
const query = toRaw(layoutStore.chatRecordQuery)
|
||||
localQuery.value = query ? { ...query } : {}
|
||||
// 如果有外部传入的筛选条件,默认不展开筛选面板
|
||||
filterExpanded.value = false
|
||||
@@ -94,7 +94,7 @@ watch(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UDrawer v-model:open="chatStore.showChatRecordDrawer" direction="right" :handle="false">
|
||||
<UDrawer v-model:open="layoutStore.showChatRecordDrawer" direction="right" :handle="false">
|
||||
<template #content>
|
||||
<div class="flex h-full w-[580px] flex-col bg-white dark:bg-gray-900">
|
||||
<!-- 头部 -->
|
||||
@@ -105,7 +105,7 @@ watch(
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@click="chatStore.closeChatRecordDrawer()"
|
||||
@click="layoutStore.closeChatRecordDrawer()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -141,4 +141,3 @@ watch(
|
||||
</template>
|
||||
</UDrawer>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
* 支持无限滚动加载
|
||||
*/
|
||||
import { ref, watch, nextTick, toRaw } from 'vue'
|
||||
import { useChatStore } from '@/stores/chat'
|
||||
import MessageItem from './MessageItem.vue'
|
||||
import type { ChatRecordMessage, ChatRecordQuery } from './types'
|
||||
import { useSessionStore } from '@/stores/session'
|
||||
|
||||
const props = defineProps<{
|
||||
/** 当前查询条件 */
|
||||
@@ -18,7 +18,7 @@ const emit = defineEmits<{
|
||||
(e: 'count-change', count: number): void
|
||||
}>()
|
||||
|
||||
const chatStore = useChatStore()
|
||||
const sessionStore = useSessionStore()
|
||||
|
||||
// 消息列表
|
||||
const messages = ref<ChatRecordMessage[]>([])
|
||||
@@ -41,7 +41,7 @@ function buildFilterParams(query: ChatRecordQuery) {
|
||||
|
||||
// 初始加载消息
|
||||
async function loadInitialMessages() {
|
||||
const sessionId = chatStore.currentSessionId
|
||||
const sessionId = sessionStore.currentSessionId
|
||||
if (!sessionId) {
|
||||
messages.value = []
|
||||
emit('count-change', 0)
|
||||
@@ -99,7 +99,7 @@ async function loadInitialMessages() {
|
||||
async function loadMoreBefore() {
|
||||
if (isLoadingMore.value || !hasMoreBefore.value || messages.value.length === 0) return
|
||||
|
||||
const sessionId = chatStore.currentSessionId
|
||||
const sessionId = sessionStore.currentSessionId
|
||||
if (!sessionId) return
|
||||
|
||||
const firstMessage = messages.value[0]
|
||||
@@ -142,7 +142,7 @@ async function loadMoreBefore() {
|
||||
async function loadMoreAfter() {
|
||||
if (isLoadingMore.value || !hasMoreAfter.value || messages.value.length === 0) return
|
||||
|
||||
const sessionId = chatStore.currentSessionId
|
||||
const sessionId = sessionStore.currentSessionId
|
||||
if (!sessionId) return
|
||||
|
||||
const lastMessage = messages.value[messages.value.length - 1]
|
||||
@@ -269,4 +269,3 @@ defineExpose({
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
import { useChatStore } from '@/stores/chat'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { ref, onMounted, nextTick } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
@@ -8,13 +7,17 @@ import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import 'dayjs/locale/zh-cn'
|
||||
import SidebarFooter from './sidebar/SidebarFooter.vue'
|
||||
import { useSessionStore } from '@/stores/session'
|
||||
import { useLayoutStore } from '@/stores/layout'
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
dayjs.locale('zh-cn')
|
||||
|
||||
const chatStore = useChatStore()
|
||||
const { sessions, isSidebarCollapsed: isCollapsed } = storeToRefs(chatStore)
|
||||
const { toggleSidebar } = chatStore
|
||||
const sessionStore = useSessionStore()
|
||||
const layoutStore = useLayoutStore()
|
||||
const { sessions } = storeToRefs(sessionStore)
|
||||
const { isSidebarCollapsed: isCollapsed } = storeToRefs(layoutStore)
|
||||
const { toggleSidebar } = layoutStore
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
@@ -30,7 +33,7 @@ const deleteTarget = ref<AnalysisSession | null>(null)
|
||||
|
||||
// 加载会话列表
|
||||
onMounted(() => {
|
||||
chatStore.loadSessions()
|
||||
sessionStore.loadSessions()
|
||||
})
|
||||
|
||||
function handleImport() {
|
||||
@@ -58,7 +61,7 @@ function openRenameModal(session: AnalysisSession) {
|
||||
async function handleRename() {
|
||||
if (!renameTarget.value || !newName.value.trim()) return
|
||||
|
||||
const success = await chatStore.renameSession(renameTarget.value.id, newName.value.trim())
|
||||
const success = await sessionStore.renameSession(renameTarget.value.id, newName.value.trim())
|
||||
if (success) {
|
||||
showRenameModal.value = false
|
||||
renameTarget.value = null
|
||||
@@ -83,7 +86,7 @@ function openDeleteModal(session: AnalysisSession) {
|
||||
async function confirmDelete() {
|
||||
if (!deleteTarget.value) return
|
||||
|
||||
await chatStore.deleteSession(deleteTarget.value.id)
|
||||
await sessionStore.deleteSession(deleteTarget.value.id)
|
||||
showDeleteModal.value = false
|
||||
deleteTarget.value = null
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useChatStore } from '@/stores/chat'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { usePromptStore } from '@/stores/prompt'
|
||||
|
||||
// Store
|
||||
const chatStore = useChatStore()
|
||||
const { aiGlobalSettings } = storeToRefs(chatStore)
|
||||
const promptStore = usePromptStore()
|
||||
const { aiGlobalSettings } = storeToRefs(promptStore)
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
@@ -17,7 +17,7 @@ const globalMaxMessages = computed({
|
||||
get: () => aiGlobalSettings.value.maxMessagesPerRequest,
|
||||
set: (val: number) => {
|
||||
const clampedVal = Math.max(10, Math.min(5000, val || 200))
|
||||
chatStore.updateAIGlobalSettings({ maxMessagesPerRequest: clampedVal })
|
||||
promptStore.updateAIGlobalSettings({ maxMessagesPerRequest: clampedVal })
|
||||
emit('config-changed')
|
||||
},
|
||||
})
|
||||
@@ -56,4 +56,3 @@ const globalMaxMessages = computed({
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useChatStore } from '@/stores/chat'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import type { PromptPreset } from '@/types/chat'
|
||||
import AIPromptEditModal from './AIPromptEditModal.vue'
|
||||
import { usePromptStore } from '@/stores/prompt'
|
||||
|
||||
// Store
|
||||
const chatStore = useChatStore()
|
||||
const { groupPresets, privatePresets, aiPromptSettings, aiGlobalSettings } = storeToRefs(chatStore)
|
||||
const promptStore = usePromptStore()
|
||||
const { groupPresets, privatePresets, aiPromptSettings, aiGlobalSettings } = storeToRefs(promptStore)
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
@@ -25,12 +25,12 @@ const globalMaxMessages = computed({
|
||||
get: () => aiGlobalSettings.value.maxMessagesPerRequest,
|
||||
set: (val: number) => {
|
||||
const clampedVal = Math.max(10, Math.min(5000, val || 200))
|
||||
chatStore.updateAIGlobalSettings({ maxMessagesPerRequest: clampedVal })
|
||||
promptStore.updateAIGlobalSettings({ maxMessagesPerRequest: clampedVal })
|
||||
emit('config-changed')
|
||||
},
|
||||
})
|
||||
|
||||
// 方法
|
||||
/** 打开新增预设弹窗 */
|
||||
function openAddModal(chatType: 'group' | 'private') {
|
||||
editMode.value = 'add'
|
||||
editingPreset.value = null
|
||||
@@ -38,6 +38,7 @@ function openAddModal(chatType: 'group' | 'private') {
|
||||
showEditModal.value = true
|
||||
}
|
||||
|
||||
/** 打开编辑预设弹窗 */
|
||||
function openEditModal(preset: PromptPreset) {
|
||||
editMode.value = 'edit'
|
||||
editingPreset.value = preset
|
||||
@@ -45,29 +46,34 @@ function openEditModal(preset: PromptPreset) {
|
||||
showEditModal.value = true
|
||||
}
|
||||
|
||||
/** 处理子弹窗保存后的回调 */
|
||||
function handleModalSaved() {
|
||||
emit('config-changed')
|
||||
}
|
||||
|
||||
/** 设置当前激活的预设 */
|
||||
function setActivePreset(presetId: string, chatType: 'group' | 'private') {
|
||||
if (chatType === 'group') {
|
||||
chatStore.setActiveGroupPreset(presetId)
|
||||
promptStore.setActiveGroupPreset(presetId)
|
||||
} else {
|
||||
chatStore.setActivePrivatePreset(presetId)
|
||||
promptStore.setActivePrivatePreset(presetId)
|
||||
}
|
||||
emit('config-changed')
|
||||
}
|
||||
|
||||
/** 复制选中的预设 */
|
||||
function duplicatePreset(presetId: string) {
|
||||
chatStore.duplicatePromptPreset(presetId)
|
||||
promptStore.duplicatePromptPreset(presetId)
|
||||
emit('config-changed')
|
||||
}
|
||||
|
||||
/** 删除选中的预设 */
|
||||
function deletePreset(presetId: string) {
|
||||
chatStore.removePromptPreset(presetId)
|
||||
promptStore.removePromptPreset(presetId)
|
||||
emit('config-changed')
|
||||
}
|
||||
|
||||
/** 判断预设是否处于激活状态 */
|
||||
function isActivePreset(presetId: string, chatType: 'group' | 'private'): boolean {
|
||||
if (chatType === 'group') {
|
||||
return aiPromptSettings.value.activeGroupPresetId === presetId
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useChatStore } from '@/stores/chat'
|
||||
import type { PromptPreset } from '@/types/chat'
|
||||
import {
|
||||
getDefaultRoleDefinition,
|
||||
@@ -8,6 +7,7 @@ import {
|
||||
getLockedPromptSectionPreview,
|
||||
getOriginalBuiltinPreset,
|
||||
} from '@/config/prompts'
|
||||
import { usePromptStore } from '@/stores/prompt'
|
||||
|
||||
// Props
|
||||
const props = defineProps<{
|
||||
@@ -24,7 +24,7 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
// Store
|
||||
const chatStore = useChatStore()
|
||||
const promptStore = usePromptStore()
|
||||
|
||||
// 表单数据
|
||||
const formData = ref({
|
||||
@@ -39,7 +39,7 @@ const isBuiltIn = computed(() => props.preset?.isBuiltIn ?? false)
|
||||
const isEditMode = computed(() => props.mode === 'edit')
|
||||
const isModified = computed(() => {
|
||||
if (!isBuiltIn.value || !props.preset) return false
|
||||
return chatStore.isBuiltinPresetModified(props.preset.id)
|
||||
return promptStore.isBuiltinPresetModified(props.preset.id)
|
||||
})
|
||||
|
||||
const modalTitle = computed(() => {
|
||||
@@ -77,17 +77,18 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
// 方法
|
||||
/** 关闭弹窗 */
|
||||
function closeModal() {
|
||||
emit('update:open', false)
|
||||
}
|
||||
|
||||
/** 保存提示词预设 */
|
||||
function handleSave() {
|
||||
if (!canSave.value) return
|
||||
|
||||
if (isEditMode.value && props.preset) {
|
||||
// 更新现有预设(支持内置和自定义)
|
||||
chatStore.updatePromptPreset(props.preset.id, {
|
||||
promptStore.updatePromptPreset(props.preset.id, {
|
||||
name: formData.value.name.trim(),
|
||||
chatType: formData.value.chatType,
|
||||
roleDefinition: formData.value.roleDefinition.trim(),
|
||||
@@ -95,7 +96,7 @@ function handleSave() {
|
||||
})
|
||||
} else {
|
||||
// 添加新预设
|
||||
chatStore.addPromptPreset({
|
||||
promptStore.addPromptPreset({
|
||||
name: formData.value.name.trim(),
|
||||
chatType: formData.value.chatType,
|
||||
roleDefinition: formData.value.roleDefinition.trim(),
|
||||
@@ -107,7 +108,7 @@ function handleSave() {
|
||||
closeModal()
|
||||
}
|
||||
|
||||
// 重置内置预设为原始值
|
||||
/** 重置内置预设为原始值 */
|
||||
function handleReset() {
|
||||
if (!props.preset || !isBuiltIn.value) return
|
||||
|
||||
@@ -121,7 +122,7 @@ function handleReset() {
|
||||
responseRules: original.responseRules,
|
||||
}
|
||||
// 清除覆盖
|
||||
chatStore.resetBuiltinPreset(props.preset.id)
|
||||
promptStore.resetBuiltinPreset(props.preset.id)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { useChatStore } from '@/stores/chat'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useLayoutStore } from '@/stores/layout'
|
||||
|
||||
const chatStore = useChatStore()
|
||||
const { isSidebarCollapsed: isCollapsed } = storeToRefs(chatStore)
|
||||
const layoutStore = useLayoutStore()
|
||||
const { isSidebarCollapsed: isCollapsed } = storeToRefs(layoutStore)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -105,7 +105,7 @@ const { isSidebarCollapsed: isCollapsed } = storeToRefs(chatStore)
|
||||
:class="[isCollapsed ? 'flex w-12 items-center justify-center px-0' : 'justify-start pl-4']"
|
||||
color="gray"
|
||||
variant="ghost"
|
||||
@click="chatStore.showSettingModal = true"
|
||||
@click="layoutStore.showSettingModal = true"
|
||||
>
|
||||
<UIcon name="i-heroicons-cog-6-tooth" class="h-5 w-5 shrink-0" :class="[isCollapsed ? '' : 'mr-2']" />
|
||||
<span v-if="!isCollapsed" class="truncate">设置</span>
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
*/
|
||||
|
||||
import { ref, computed } from 'vue'
|
||||
import { useChatStore } from '@/stores/chat'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { usePromptStore } from '@/stores/prompt'
|
||||
|
||||
// 工具调用记录
|
||||
export interface ToolCallRecord {
|
||||
@@ -81,8 +81,8 @@ export function useAIChat(
|
||||
chatType: 'group' | 'private' = 'group'
|
||||
) {
|
||||
// 获取 chat store 中的提示词配置和全局设置
|
||||
const chatStore = useChatStore()
|
||||
const { activeGroupPreset, activePrivatePreset, aiGlobalSettings } = storeToRefs(chatStore)
|
||||
const promptStore = usePromptStore()
|
||||
const { activeGroupPreset, activePrivatePreset, aiGlobalSettings } = storeToRefs(promptStore)
|
||||
|
||||
// 获取当前聊天类型对应的提示词配置
|
||||
const currentPromptConfig = computed(() => {
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
* 提供简洁的 API 用于捕获页面/元素截图
|
||||
*/
|
||||
import { ref } from 'vue'
|
||||
import { useChatStore } from '@/stores/chat'
|
||||
import { captureAsImageData } from '@/utils/snapCapture'
|
||||
import { useToast } from '@nuxt/ui/runtime/composables/useToast.js'
|
||||
import { useLayoutStore } from '@/stores/layout'
|
||||
|
||||
export interface ScreenCaptureOptions {
|
||||
/** 截屏时要隐藏的元素选择器列表 */
|
||||
@@ -22,7 +22,7 @@ export interface ScreenCaptureOptions {
|
||||
* 截屏功能 Composable
|
||||
*/
|
||||
export function useScreenCapture() {
|
||||
const chatStore = useChatStore()
|
||||
const layoutStore = useLayoutStore()
|
||||
const toast = useToast()
|
||||
const isCapturing = ref(false)
|
||||
const captureError = ref<string | null>(null)
|
||||
@@ -87,7 +87,7 @@ export function useScreenCapture() {
|
||||
icon: 'i-heroicons-eye',
|
||||
onClick: () => {
|
||||
if (lastCapturedImage) {
|
||||
chatStore.openScreenCaptureModal(lastCapturedImage)
|
||||
layoutStore.openScreenCaptureModal(lastCapturedImage)
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -388,7 +388,7 @@ export function useScreenCapture() {
|
||||
color: 'warning',
|
||||
duration: 3000,
|
||||
})
|
||||
chatStore.openScreenCaptureModal(imageData)
|
||||
layoutStore.openScreenCaptureModal(imageData)
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch, computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useChatStore } from '@/stores/chat'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import type { AnalysisSession, MemberActivity, HourlyActivity, DailyActivity, MessageType } from '@/types/chat'
|
||||
import { formatDateRange } from '@/utils'
|
||||
@@ -13,11 +12,12 @@ import RankingTab from './components/RankingTab.vue'
|
||||
import QuotesTab from './components/QuotesTab.vue'
|
||||
import MemberTab from './components/MemberTab.vue'
|
||||
import PageHeader from '@/components/layout/PageHeader.vue'
|
||||
import { useSessionStore } from '@/stores/session'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const chatStore = useChatStore()
|
||||
const { currentSessionId } = storeToRefs(chatStore)
|
||||
const sessionStore = useSessionStore()
|
||||
const { currentSessionId } = storeToRefs(sessionStore)
|
||||
|
||||
// 数据状态
|
||||
const isLoading = ref(true)
|
||||
@@ -97,9 +97,9 @@ const dateRangeText = computed(() => {
|
||||
function syncSession() {
|
||||
const id = route.params.id as string
|
||||
if (id) {
|
||||
chatStore.selectSession(id)
|
||||
sessionStore.selectSession(id)
|
||||
// If selection failed (e.g. invalid ID), redirect to home
|
||||
if (chatStore.currentSessionId !== id) {
|
||||
if (sessionStore.currentSessionId !== id) {
|
||||
router.replace('/')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { useChatStore } from '@/stores/chat'
|
||||
import { FileDropZone } from '@/components/UI'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import ImportTutorialModal from './components/ImportTutorialModal.vue'
|
||||
import { useSessionStore } from '@/stores/session'
|
||||
|
||||
const chatStore = useChatStore()
|
||||
const { isImporting, importProgress } = storeToRefs(chatStore)
|
||||
const sessionStore = useSessionStore()
|
||||
const { isImporting, importProgress } = storeToRefs(sessionStore)
|
||||
|
||||
const importError = ref<string | null>(null)
|
||||
const showTutorialModal = ref(false)
|
||||
@@ -53,11 +53,11 @@ async function navigateToSession(sessionId: string) {
|
||||
// 处理文件选择(点击选择)
|
||||
async function handleClickImport() {
|
||||
importError.value = null
|
||||
const result = await chatStore.importFile()
|
||||
const result = await sessionStore.importFile()
|
||||
if (!result.success && result.error && result.error !== '未选择文件') {
|
||||
importError.value = result.error
|
||||
} else if (result.success && chatStore.currentSessionId) {
|
||||
await navigateToSession(chatStore.currentSessionId)
|
||||
} else if (result.success && sessionStore.currentSessionId) {
|
||||
await navigateToSession(sessionStore.currentSessionId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,11 +69,11 @@ async function handleFileDrop({ paths }: { files: File[]; paths: string[] }) {
|
||||
}
|
||||
|
||||
importError.value = null
|
||||
const result = await chatStore.importFileFromPath(paths[0])
|
||||
const result = await sessionStore.importFileFromPath(paths[0])
|
||||
if (!result.success && result.error) {
|
||||
importError.value = result.error
|
||||
} else if (result.success && chatStore.currentSessionId) {
|
||||
await navigateToSession(chatStore.currentSessionId)
|
||||
} else if (result.success && sessionStore.currentSessionId) {
|
||||
await navigateToSession(sessionStore.currentSessionId)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch, computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useChatStore } from '@/stores/chat'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import type { AnalysisSession, MemberActivity, HourlyActivity, DailyActivity, MessageType } from '@/types/chat'
|
||||
import { formatDateRange } from '@/utils'
|
||||
@@ -12,11 +11,12 @@ import OverviewTab from './components/OverviewTab.vue'
|
||||
import QuotesTab from './components/QuotesTab.vue'
|
||||
import MemberTab from './components/MemberTab.vue'
|
||||
import PageHeader from '@/components/layout/PageHeader.vue'
|
||||
import { useSessionStore } from '@/stores/session'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const chatStore = useChatStore()
|
||||
const { currentSessionId } = storeToRefs(chatStore)
|
||||
const sessionStore = useSessionStore()
|
||||
const { currentSessionId } = storeToRefs(sessionStore)
|
||||
|
||||
// 数据状态
|
||||
const isLoading = ref(true)
|
||||
@@ -88,9 +88,9 @@ const dateRangeText = computed(() => {
|
||||
function syncSession() {
|
||||
const id = route.params.id as string
|
||||
if (id) {
|
||||
chatStore.selectSession(id)
|
||||
sessionStore.selectSession(id)
|
||||
// If selection failed (e.g. invalid ID), redirect to home
|
||||
if (chatStore.currentSessionId !== id) {
|
||||
if (sessionStore.currentSessionId !== id) {
|
||||
router.replace('/')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,584 +0,0 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import type {
|
||||
AnalysisSession,
|
||||
ImportProgress,
|
||||
KeywordTemplate,
|
||||
PromptPreset,
|
||||
AIPromptSettings,
|
||||
ChatRecordQuery,
|
||||
} from '@/types/chat'
|
||||
import {
|
||||
BUILTIN_PRESETS,
|
||||
DEFAULT_GROUP_PRESET_ID,
|
||||
DEFAULT_PRIVATE_PRESET_ID,
|
||||
CYBER_JUDGE_GROUP_PRESET_ID,
|
||||
CYBER_JUDGE_PRIVATE_PRESET_ID,
|
||||
getOriginalBuiltinPreset,
|
||||
} from '@/config/prompts'
|
||||
|
||||
// 重新导出常量,保持向后兼容
|
||||
export { DEFAULT_GROUP_PRESET_ID, DEFAULT_PRIVATE_PRESET_ID, BUILTIN_PRESETS }
|
||||
|
||||
export const useChatStore = defineStore(
|
||||
'chat',
|
||||
() => {
|
||||
// 会话列表
|
||||
const sessions = ref<AnalysisSession[]>([])
|
||||
// 当前选中的会话ID
|
||||
const currentSessionId = ref<string | null>(null)
|
||||
// 导入状态
|
||||
const isImporting = ref(false)
|
||||
const importProgress = ref<ImportProgress | null>(null)
|
||||
|
||||
// 当前选中的会话
|
||||
const currentSession = computed(() => {
|
||||
if (!currentSessionId.value) return null
|
||||
return sessions.value.find((s) => s.id === currentSessionId.value) || null
|
||||
})
|
||||
|
||||
// 是否已初始化
|
||||
const isInitialized = ref(false)
|
||||
|
||||
/**
|
||||
* 从数据库加载会话列表
|
||||
*/
|
||||
async function loadSessions() {
|
||||
try {
|
||||
const list = await window.chatApi.getSessions()
|
||||
sessions.value = list
|
||||
// 如果当前选中的会话不存在了,清除选中状态
|
||||
if (currentSessionId.value && !list.find((s) => s.id === currentSessionId.value)) {
|
||||
currentSessionId.value = null
|
||||
}
|
||||
isInitialized.value = true
|
||||
} catch (error) {
|
||||
console.error('加载会话列表失败:', error)
|
||||
isInitialized.value = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择文件并导入
|
||||
*/
|
||||
async function importFile(): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
// 选择文件
|
||||
const result = await window.chatApi.selectFile()
|
||||
if (!result || !result.filePath) {
|
||||
return { success: false, error: '未选择文件' }
|
||||
}
|
||||
if (result.error) {
|
||||
return { success: false, error: result.error }
|
||||
}
|
||||
|
||||
// 使用共享的导入逻辑
|
||||
return await importFileFromPath(result.filePath)
|
||||
} catch (error) {
|
||||
return { success: false, error: String(error) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从文件路径直接导入(用于拖拽导入)
|
||||
*/
|
||||
async function importFileFromPath(filePath: string): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
// 开始导入
|
||||
isImporting.value = true
|
||||
|
||||
// 初始化状态
|
||||
importProgress.value = {
|
||||
stage: 'detecting',
|
||||
progress: 0,
|
||||
message: '准备导入...',
|
||||
}
|
||||
|
||||
// 进度队列控制
|
||||
const queue: ImportProgress[] = []
|
||||
let isProcessing = false
|
||||
let currentStage = 'reading'
|
||||
let lastStageTime = Date.now()
|
||||
const MIN_STAGE_TIME = 1000 // 每个阶段至少展示1秒
|
||||
|
||||
const processQueue = async () => {
|
||||
if (isProcessing) return
|
||||
isProcessing = true
|
||||
|
||||
while (queue.length > 0) {
|
||||
const next = queue[0]
|
||||
|
||||
// 如果阶段发生变化,确保上一阶段展示了足够时间
|
||||
if (next.stage !== currentStage) {
|
||||
const elapsed = Date.now() - lastStageTime
|
||||
if (elapsed < MIN_STAGE_TIME) {
|
||||
await new Promise((resolve) => setTimeout(resolve, MIN_STAGE_TIME - elapsed))
|
||||
}
|
||||
currentStage = next.stage
|
||||
lastStageTime = Date.now()
|
||||
}
|
||||
|
||||
// 更新状态
|
||||
importProgress.value = queue.shift()!
|
||||
}
|
||||
isProcessing = false
|
||||
}
|
||||
|
||||
// 监听导入进度
|
||||
const unsubscribe = window.chatApi.onImportProgress((progress) => {
|
||||
// 跳过完成状态,直接跳转
|
||||
if (progress.stage === 'done') return
|
||||
queue.push(progress)
|
||||
processQueue()
|
||||
})
|
||||
|
||||
// 执行导入
|
||||
const importResult = await window.chatApi.import(filePath)
|
||||
|
||||
// 取消监听
|
||||
unsubscribe()
|
||||
|
||||
// 等待队列处理完成
|
||||
while (queue.length > 0 || isProcessing) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
}
|
||||
|
||||
// 确保最后一个阶段也展示足够时间
|
||||
const elapsed = Date.now() - lastStageTime
|
||||
if (elapsed < MIN_STAGE_TIME) {
|
||||
await new Promise((resolve) => setTimeout(resolve, MIN_STAGE_TIME - elapsed))
|
||||
}
|
||||
|
||||
// 确保进度条走完
|
||||
if (importProgress.value) {
|
||||
importProgress.value.progress = 100
|
||||
}
|
||||
|
||||
// 给一点时间展示 100%
|
||||
await new Promise((resolve) => setTimeout(resolve, 300))
|
||||
|
||||
if (importResult.success && importResult.sessionId) {
|
||||
// 刷新会话列表
|
||||
await loadSessions()
|
||||
// 自动选中新导入的会话,进入分析页面
|
||||
currentSessionId.value = importResult.sessionId
|
||||
return { success: true }
|
||||
} else {
|
||||
return { success: false, error: importResult.error || '导入失败' }
|
||||
}
|
||||
} catch (error) {
|
||||
return { success: false, error: String(error) }
|
||||
} finally {
|
||||
isImporting.value = false
|
||||
// 延迟清除进度,让用户看到完成状态
|
||||
setTimeout(() => {
|
||||
importProgress.value = null
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择会话
|
||||
*/
|
||||
function selectSession(id: string) {
|
||||
currentSessionId.value = id
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除会话
|
||||
*/
|
||||
async function deleteSession(id: string): Promise<boolean> {
|
||||
try {
|
||||
const success = await window.chatApi.deleteSession(id)
|
||||
if (success) {
|
||||
// 从列表中移除
|
||||
const index = sessions.value.findIndex((s) => s.id === id)
|
||||
if (index !== -1) {
|
||||
sessions.value.splice(index, 1)
|
||||
}
|
||||
// 如果删除的是当前选中的会话,清除选中状态
|
||||
if (currentSessionId.value === id) {
|
||||
currentSessionId.value = null
|
||||
}
|
||||
// 强制从后端刷新会话列表,确保与文件系统同步
|
||||
await loadSessions()
|
||||
}
|
||||
return success
|
||||
} catch (error) {
|
||||
console.error('删除会话失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重命名会话
|
||||
*/
|
||||
async function renameSession(id: string, newName: string): Promise<boolean> {
|
||||
try {
|
||||
const success = await window.chatApi.renameSession(id, newName)
|
||||
if (success) {
|
||||
// 更新本地列表中的名称
|
||||
const session = sessions.value.find((s) => s.id === id)
|
||||
if (session) {
|
||||
session.name = newName
|
||||
}
|
||||
}
|
||||
return success
|
||||
} catch (error) {
|
||||
console.error('重命名会话失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除选中状态
|
||||
*/
|
||||
function clearSelection() {
|
||||
currentSessionId.value = null
|
||||
}
|
||||
|
||||
// 侧边栏状态
|
||||
const isSidebarCollapsed = ref(false)
|
||||
|
||||
function toggleSidebar() {
|
||||
isSidebarCollapsed.value = !isSidebarCollapsed.value
|
||||
}
|
||||
|
||||
// 设置弹窗状态
|
||||
const showSettingModal = ref(false)
|
||||
|
||||
// 截屏弹窗状态
|
||||
const showScreenCaptureModal = ref(false)
|
||||
const screenCaptureImage = ref<string | null>(null)
|
||||
|
||||
function openScreenCaptureModal(imageData: string) {
|
||||
screenCaptureImage.value = imageData
|
||||
showScreenCaptureModal.value = true
|
||||
}
|
||||
|
||||
function closeScreenCaptureModal() {
|
||||
showScreenCaptureModal.value = false
|
||||
// 延迟清除图片数据,避免关闭动画时闪烁
|
||||
setTimeout(() => {
|
||||
screenCaptureImage.value = null
|
||||
}, 300)
|
||||
}
|
||||
|
||||
// ==================== 聊天记录查看器 Drawer ====================
|
||||
const showChatRecordDrawer = ref(false)
|
||||
const chatRecordQuery = ref<ChatRecordQuery | null>(null)
|
||||
|
||||
/**
|
||||
* 打开聊天记录查看器
|
||||
* @param query 查询参数,支持组合查询
|
||||
*/
|
||||
function openChatRecordDrawer(query: ChatRecordQuery) {
|
||||
chatRecordQuery.value = query
|
||||
showChatRecordDrawer.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭聊天记录查看器
|
||||
*/
|
||||
function closeChatRecordDrawer() {
|
||||
showChatRecordDrawer.value = false
|
||||
// 延迟清除查询参数,避免关闭动画时内容闪烁
|
||||
setTimeout(() => {
|
||||
chatRecordQuery.value = null
|
||||
}, 300)
|
||||
}
|
||||
|
||||
// AI 配置更新计数器(用于触发其他组件刷新)
|
||||
const aiConfigVersion = ref(0)
|
||||
|
||||
function notifyAIConfigChanged() {
|
||||
aiConfigVersion.value++
|
||||
}
|
||||
|
||||
// AI 全局设置
|
||||
interface AIGlobalSettings {
|
||||
/** 每次发送给 AI 的最大消息条数 */
|
||||
maxMessagesPerRequest: number
|
||||
}
|
||||
|
||||
const aiGlobalSettings = ref<AIGlobalSettings>({
|
||||
maxMessagesPerRequest: 200,
|
||||
})
|
||||
|
||||
function updateAIGlobalSettings(settings: Partial<AIGlobalSettings>) {
|
||||
aiGlobalSettings.value = { ...aiGlobalSettings.value, ...settings }
|
||||
notifyAIConfigChanged()
|
||||
}
|
||||
|
||||
// ==================== 自定义关键词模板 ====================
|
||||
const customKeywordTemplates = ref<KeywordTemplate[]>([])
|
||||
|
||||
function addCustomKeywordTemplate(template: KeywordTemplate) {
|
||||
customKeywordTemplates.value.push(template)
|
||||
}
|
||||
|
||||
function updateCustomKeywordTemplate(templateId: string, updates: Partial<Omit<KeywordTemplate, 'id'>>) {
|
||||
const index = customKeywordTemplates.value.findIndex((t) => t.id === templateId)
|
||||
if (index !== -1) {
|
||||
customKeywordTemplates.value[index] = {
|
||||
...customKeywordTemplates.value[index],
|
||||
...updates,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function removeCustomKeywordTemplate(templateId: string) {
|
||||
const index = customKeywordTemplates.value.findIndex((t) => t.id === templateId)
|
||||
if (index !== -1) {
|
||||
customKeywordTemplates.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 已删除的预设模板 ====================
|
||||
const deletedPresetTemplateIds = ref<string[]>([])
|
||||
|
||||
// ==================== AI 提示词预设管理 ====================
|
||||
const customPromptPresets = ref<PromptPreset[]>([])
|
||||
/** 内置预设的用户覆盖(key: presetId, value: 覆盖的字段) */
|
||||
const builtinPresetOverrides = ref<
|
||||
Record<string, { name?: string; roleDefinition?: string; responseRules?: string; updatedAt?: number }>
|
||||
>({})
|
||||
const aiPromptSettings = ref<AIPromptSettings>({
|
||||
activeGroupPresetId: CYBER_JUDGE_GROUP_PRESET_ID,
|
||||
activePrivatePresetId: CYBER_JUDGE_PRIVATE_PRESET_ID,
|
||||
})
|
||||
|
||||
/** 获取所有预设(内置+覆盖 + 自定义) */
|
||||
const allPromptPresets = computed(() => {
|
||||
// 合并内置预设和用户覆盖
|
||||
const mergedBuiltins = BUILTIN_PRESETS.map((preset) => {
|
||||
const override = builtinPresetOverrides.value[preset.id]
|
||||
if (override) {
|
||||
return {
|
||||
...preset,
|
||||
...override,
|
||||
}
|
||||
}
|
||||
return preset
|
||||
})
|
||||
return [...mergedBuiltins, ...customPromptPresets.value]
|
||||
})
|
||||
|
||||
/** 获取群聊可用的预设 */
|
||||
const groupPresets = computed(() => allPromptPresets.value.filter((p) => p.chatType === 'group'))
|
||||
|
||||
/** 获取私聊可用的预设 */
|
||||
const privatePresets = computed(() => allPromptPresets.value.filter((p) => p.chatType === 'private'))
|
||||
|
||||
/** 获取当前群聊激活的预设 */
|
||||
const activeGroupPreset = computed(() => {
|
||||
const preset = allPromptPresets.value.find((p) => p.id === aiPromptSettings.value.activeGroupPresetId)
|
||||
return preset || BUILTIN_PRESETS.find((p) => p.id === DEFAULT_GROUP_PRESET_ID)!
|
||||
})
|
||||
|
||||
/** 获取当前私聊激活的预设 */
|
||||
const activePrivatePreset = computed(() => {
|
||||
const preset = allPromptPresets.value.find((p) => p.id === aiPromptSettings.value.activePrivatePresetId)
|
||||
return preset || BUILTIN_PRESETS.find((p) => p.id === DEFAULT_PRIVATE_PRESET_ID)!
|
||||
})
|
||||
|
||||
/** 添加自定义预设 */
|
||||
function addPromptPreset(preset: {
|
||||
name: string
|
||||
chatType: PromptPreset['chatType']
|
||||
roleDefinition: string
|
||||
responseRules: string
|
||||
}) {
|
||||
const newPreset: PromptPreset = {
|
||||
...preset,
|
||||
id: `custom-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||
isBuiltIn: false,
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
}
|
||||
customPromptPresets.value.push(newPreset)
|
||||
return newPreset.id
|
||||
}
|
||||
|
||||
/** 更新预设(支持内置和自定义) */
|
||||
function updatePromptPreset(
|
||||
presetId: string,
|
||||
updates: { name?: string; chatType?: PromptPreset['chatType']; roleDefinition?: string; responseRules?: string }
|
||||
) {
|
||||
// 检查是否是内置预设
|
||||
const isBuiltin = BUILTIN_PRESETS.some((p) => p.id === presetId)
|
||||
if (isBuiltin) {
|
||||
// 更新内置预设的覆盖
|
||||
builtinPresetOverrides.value[presetId] = {
|
||||
...builtinPresetOverrides.value[presetId],
|
||||
name: updates.name,
|
||||
roleDefinition: updates.roleDefinition,
|
||||
responseRules: updates.responseRules,
|
||||
updatedAt: Date.now(),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 更新自定义预设
|
||||
const index = customPromptPresets.value.findIndex((p) => p.id === presetId)
|
||||
if (index !== -1) {
|
||||
customPromptPresets.value[index] = {
|
||||
...customPromptPresets.value[index],
|
||||
...updates,
|
||||
updatedAt: Date.now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 重置内置预设为原始值 */
|
||||
function resetBuiltinPreset(presetId: string): boolean {
|
||||
const original = getOriginalBuiltinPreset(presetId)
|
||||
if (!original) return false
|
||||
|
||||
// 删除覆盖
|
||||
delete builtinPresetOverrides.value[presetId]
|
||||
return true
|
||||
}
|
||||
|
||||
/** 检查内置预设是否被修改过 */
|
||||
function isBuiltinPresetModified(presetId: string): boolean {
|
||||
return !!builtinPresetOverrides.value[presetId]
|
||||
}
|
||||
|
||||
/** 删除自定义预设 */
|
||||
function removePromptPreset(presetId: string) {
|
||||
const index = customPromptPresets.value.findIndex((p) => p.id === presetId)
|
||||
if (index !== -1) {
|
||||
customPromptPresets.value.splice(index, 1)
|
||||
// 如果删除的是激活的预设,切换到默认
|
||||
if (aiPromptSettings.value.activeGroupPresetId === presetId) {
|
||||
aiPromptSettings.value.activeGroupPresetId = DEFAULT_GROUP_PRESET_ID
|
||||
}
|
||||
if (aiPromptSettings.value.activePrivatePresetId === presetId) {
|
||||
aiPromptSettings.value.activePrivatePresetId = DEFAULT_PRIVATE_PRESET_ID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 复制预设 */
|
||||
function duplicatePromptPreset(presetId: string) {
|
||||
const source = allPromptPresets.value.find((p) => p.id === presetId)
|
||||
if (source) {
|
||||
return addPromptPreset({
|
||||
name: `${source.name} (副本)`,
|
||||
chatType: source.chatType,
|
||||
roleDefinition: source.roleDefinition,
|
||||
responseRules: source.responseRules,
|
||||
})
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/** 设置群聊激活的预设 */
|
||||
function setActiveGroupPreset(presetId: string) {
|
||||
const preset = allPromptPresets.value.find((p) => p.id === presetId)
|
||||
if (preset && preset.chatType === 'group') {
|
||||
aiPromptSettings.value.activeGroupPresetId = presetId
|
||||
notifyAIConfigChanged()
|
||||
}
|
||||
}
|
||||
|
||||
/** 设置私聊激活的预设 */
|
||||
function setActivePrivatePreset(presetId: string) {
|
||||
const preset = allPromptPresets.value.find((p) => p.id === presetId)
|
||||
if (preset && preset.chatType === 'private') {
|
||||
aiPromptSettings.value.activePrivatePresetId = presetId
|
||||
notifyAIConfigChanged()
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取指定聊天类型的激活预设 */
|
||||
function getActivePresetForChatType(chatType: 'group' | 'private'): PromptPreset {
|
||||
return chatType === 'group' ? activeGroupPreset.value : activePrivatePreset.value
|
||||
}
|
||||
|
||||
function addDeletedPresetTemplateId(id: string) {
|
||||
if (!deletedPresetTemplateIds.value.includes(id)) {
|
||||
deletedPresetTemplateIds.value.push(id)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
sessions,
|
||||
currentSessionId,
|
||||
isImporting,
|
||||
importProgress,
|
||||
isInitialized,
|
||||
isSidebarCollapsed,
|
||||
showSettingModal,
|
||||
showScreenCaptureModal,
|
||||
screenCaptureImage,
|
||||
showChatRecordDrawer,
|
||||
chatRecordQuery,
|
||||
aiConfigVersion,
|
||||
aiGlobalSettings,
|
||||
customKeywordTemplates,
|
||||
deletedPresetTemplateIds,
|
||||
customPromptPresets,
|
||||
builtinPresetOverrides,
|
||||
aiPromptSettings,
|
||||
// Computed
|
||||
currentSession,
|
||||
allPromptPresets,
|
||||
groupPresets,
|
||||
privatePresets,
|
||||
activeGroupPreset,
|
||||
activePrivatePreset,
|
||||
// Actions
|
||||
loadSessions,
|
||||
importFile,
|
||||
importFileFromPath,
|
||||
selectSession,
|
||||
deleteSession,
|
||||
renameSession,
|
||||
clearSelection,
|
||||
toggleSidebar,
|
||||
openScreenCaptureModal,
|
||||
closeScreenCaptureModal,
|
||||
openChatRecordDrawer,
|
||||
closeChatRecordDrawer,
|
||||
notifyAIConfigChanged,
|
||||
updateAIGlobalSettings,
|
||||
addCustomKeywordTemplate,
|
||||
updateCustomKeywordTemplate,
|
||||
removeCustomKeywordTemplate,
|
||||
addDeletedPresetTemplateId,
|
||||
addPromptPreset,
|
||||
updatePromptPreset,
|
||||
removePromptPreset,
|
||||
duplicatePromptPreset,
|
||||
setActiveGroupPreset,
|
||||
setActivePrivatePreset,
|
||||
getActivePresetForChatType,
|
||||
resetBuiltinPreset,
|
||||
isBuiltinPresetModified,
|
||||
}
|
||||
},
|
||||
{
|
||||
persist: [
|
||||
{
|
||||
// 会话状态:sessionStorage(页面刷新保留,应用重启清除)
|
||||
pick: ['currentSessionId', 'isSidebarCollapsed'],
|
||||
storage: sessionStorage,
|
||||
},
|
||||
{
|
||||
// 自定义模板、AI 全局设置和提示词预设:localStorage(持久保存)
|
||||
pick: [
|
||||
'customKeywordTemplates',
|
||||
'deletedPresetTemplateIds',
|
||||
'aiGlobalSettings',
|
||||
'customPromptPresets',
|
||||
'builtinPresetOverrides',
|
||||
'aiPromptSettings',
|
||||
],
|
||||
storage: localStorage,
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,83 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import type { ChatRecordQuery } from '@/types/chat'
|
||||
|
||||
/**
|
||||
* 全局界面状态(侧边栏、弹窗、聊天记录抽屉等)
|
||||
*/
|
||||
export const useLayoutStore = defineStore(
|
||||
'layout',
|
||||
() => {
|
||||
const isSidebarCollapsed = ref(false)
|
||||
const showSettingModal = ref(false)
|
||||
const showScreenCaptureModal = ref(false)
|
||||
const screenCaptureImage = ref<string | null>(null)
|
||||
const showChatRecordDrawer = ref(false)
|
||||
const chatRecordQuery = ref<ChatRecordQuery | null>(null)
|
||||
|
||||
/**
|
||||
* 切换侧边栏展开/折叠状态
|
||||
*/
|
||||
function toggleSidebar() {
|
||||
isSidebarCollapsed.value = !isSidebarCollapsed.value
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开截屏预览弹窗
|
||||
*/
|
||||
function openScreenCaptureModal(imageData: string) {
|
||||
screenCaptureImage.value = imageData
|
||||
showScreenCaptureModal.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭截屏预览弹窗
|
||||
*/
|
||||
function closeScreenCaptureModal() {
|
||||
showScreenCaptureModal.value = false
|
||||
setTimeout(() => {
|
||||
screenCaptureImage.value = null
|
||||
}, 300)
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开聊天记录抽屉并设置查询参数
|
||||
*/
|
||||
function openChatRecordDrawer(query: ChatRecordQuery) {
|
||||
chatRecordQuery.value = query
|
||||
showChatRecordDrawer.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭聊天记录抽屉并重置查询
|
||||
*/
|
||||
function closeChatRecordDrawer() {
|
||||
showChatRecordDrawer.value = false
|
||||
setTimeout(() => {
|
||||
chatRecordQuery.value = null
|
||||
}, 300)
|
||||
}
|
||||
|
||||
return {
|
||||
isSidebarCollapsed,
|
||||
showSettingModal,
|
||||
showScreenCaptureModal,
|
||||
screenCaptureImage,
|
||||
showChatRecordDrawer,
|
||||
chatRecordQuery,
|
||||
toggleSidebar,
|
||||
openScreenCaptureModal,
|
||||
closeScreenCaptureModal,
|
||||
openChatRecordDrawer,
|
||||
closeChatRecordDrawer,
|
||||
}
|
||||
},
|
||||
{
|
||||
persist: [
|
||||
{
|
||||
pick: ['isSidebarCollapsed'],
|
||||
storage: sessionStorage,
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,293 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import type { PromptPreset, KeywordTemplate, AIPromptSettings } from '@/types/chat'
|
||||
import {
|
||||
BUILTIN_PRESETS,
|
||||
DEFAULT_GROUP_PRESET_ID,
|
||||
DEFAULT_PRIVATE_PRESET_ID,
|
||||
CYBER_JUDGE_GROUP_PRESET_ID,
|
||||
CYBER_JUDGE_PRIVATE_PRESET_ID,
|
||||
getOriginalBuiltinPreset,
|
||||
} from '@/config/prompts'
|
||||
|
||||
/**
|
||||
* AI 配置、提示词和关键词模板相关的全局状态
|
||||
*/
|
||||
export const usePromptStore = defineStore(
|
||||
'prompt',
|
||||
() => {
|
||||
const customPromptPresets = ref<PromptPreset[]>([])
|
||||
const builtinPresetOverrides = ref<
|
||||
Record<string, { name?: string; roleDefinition?: string; responseRules?: string; updatedAt?: number }>
|
||||
>({})
|
||||
const aiPromptSettings = ref<AIPromptSettings>({
|
||||
activeGroupPresetId: CYBER_JUDGE_GROUP_PRESET_ID,
|
||||
activePrivatePresetId: CYBER_JUDGE_PRIVATE_PRESET_ID,
|
||||
})
|
||||
const aiConfigVersion = ref(0)
|
||||
const aiGlobalSettings = ref({
|
||||
maxMessagesPerRequest: 200,
|
||||
})
|
||||
const customKeywordTemplates = ref<KeywordTemplate[]>([])
|
||||
const deletedPresetTemplateIds = ref<string[]>([])
|
||||
|
||||
/** 获取所有提示词预设(内置 + 覆盖 + 自定义) */
|
||||
const allPromptPresets = computed(() => {
|
||||
const mergedBuiltins = BUILTIN_PRESETS.map((preset) => {
|
||||
const override = builtinPresetOverrides.value[preset.id]
|
||||
if (override) {
|
||||
return { ...preset, ...override }
|
||||
}
|
||||
return preset
|
||||
})
|
||||
return [...mergedBuiltins, ...customPromptPresets.value]
|
||||
})
|
||||
|
||||
/** 群聊预设列表 */
|
||||
const groupPresets = computed(() => allPromptPresets.value.filter((p) => p.chatType === 'group'))
|
||||
|
||||
/** 私聊预设列表 */
|
||||
const privatePresets = computed(() => allPromptPresets.value.filter((p) => p.chatType === 'private'))
|
||||
|
||||
/** 当前激活的群聊预设 */
|
||||
const activeGroupPreset = computed(() => {
|
||||
const preset = allPromptPresets.value.find((p) => p.id === aiPromptSettings.value.activeGroupPresetId)
|
||||
return preset || BUILTIN_PRESETS.find((p) => p.id === DEFAULT_GROUP_PRESET_ID)!
|
||||
})
|
||||
|
||||
/** 当前激活的私聊预设 */
|
||||
const activePrivatePreset = computed(() => {
|
||||
const preset = allPromptPresets.value.find((p) => p.id === aiPromptSettings.value.activePrivatePresetId)
|
||||
return preset || BUILTIN_PRESETS.find((p) => p.id === DEFAULT_PRIVATE_PRESET_ID)!
|
||||
})
|
||||
|
||||
/**
|
||||
* 通知外部 AI 配置已经被修改
|
||||
*/
|
||||
function notifyAIConfigChanged() {
|
||||
aiConfigVersion.value++
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 AI 全局设置
|
||||
*/
|
||||
function updateAIGlobalSettings(settings: Partial<{ maxMessagesPerRequest: number }>) {
|
||||
aiGlobalSettings.value = { ...aiGlobalSettings.value, ...settings }
|
||||
notifyAIConfigChanged()
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增自定义关键词模板
|
||||
*/
|
||||
function addCustomKeywordTemplate(template: KeywordTemplate) {
|
||||
customKeywordTemplates.value.push(template)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新自定义关键词模板
|
||||
*/
|
||||
function updateCustomKeywordTemplate(templateId: string, updates: Partial<Omit<KeywordTemplate, 'id'>>) {
|
||||
const index = customKeywordTemplates.value.findIndex((t) => t.id === templateId)
|
||||
if (index !== -1) {
|
||||
customKeywordTemplates.value[index] = {
|
||||
...customKeywordTemplates.value[index],
|
||||
...updates,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除自定义关键词模板
|
||||
*/
|
||||
function removeCustomKeywordTemplate(templateId: string) {
|
||||
const index = customKeywordTemplates.value.findIndex((t) => t.id === templateId)
|
||||
if (index !== -1) {
|
||||
customKeywordTemplates.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记预设模板为已删除
|
||||
*/
|
||||
function addDeletedPresetTemplateId(id: string) {
|
||||
if (!deletedPresetTemplateIds.value.includes(id)) {
|
||||
deletedPresetTemplateIds.value.push(id)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加新的提示词预设
|
||||
*/
|
||||
function addPromptPreset(preset: {
|
||||
name: string
|
||||
chatType: PromptPreset['chatType']
|
||||
roleDefinition: string
|
||||
responseRules: string
|
||||
}) {
|
||||
const newPreset: PromptPreset = {
|
||||
...preset,
|
||||
id: `custom-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||
isBuiltIn: false,
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
}
|
||||
customPromptPresets.value.push(newPreset)
|
||||
return newPreset.id
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新提示词预设(含内置覆盖)
|
||||
*/
|
||||
function updatePromptPreset(
|
||||
presetId: string,
|
||||
updates: { name?: string; chatType?: PromptPreset['chatType']; roleDefinition?: string; responseRules?: string }
|
||||
) {
|
||||
const isBuiltin = BUILTIN_PRESETS.some((p) => p.id === presetId)
|
||||
if (isBuiltin) {
|
||||
builtinPresetOverrides.value[presetId] = {
|
||||
...builtinPresetOverrides.value[presetId],
|
||||
name: updates.name,
|
||||
roleDefinition: updates.roleDefinition,
|
||||
responseRules: updates.responseRules,
|
||||
updatedAt: Date.now(),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const index = customPromptPresets.value.findIndex((p) => p.id === presetId)
|
||||
if (index !== -1) {
|
||||
customPromptPresets.value[index] = {
|
||||
...customPromptPresets.value[index],
|
||||
...updates,
|
||||
updatedAt: Date.now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置内置预设为初始状态
|
||||
*/
|
||||
function resetBuiltinPreset(presetId: string): boolean {
|
||||
const original = getOriginalBuiltinPreset(presetId)
|
||||
if (!original) return false
|
||||
delete builtinPresetOverrides.value[presetId]
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断内置预设是否被自定义过
|
||||
*/
|
||||
function isBuiltinPresetModified(presetId: string): boolean {
|
||||
return !!builtinPresetOverrides.value[presetId]
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除提示词预设(自定义)
|
||||
*/
|
||||
function removePromptPreset(presetId: string) {
|
||||
const index = customPromptPresets.value.findIndex((p) => p.id === presetId)
|
||||
if (index !== -1) {
|
||||
customPromptPresets.value.splice(index, 1)
|
||||
if (aiPromptSettings.value.activeGroupPresetId === presetId) {
|
||||
aiPromptSettings.value.activeGroupPresetId = DEFAULT_GROUP_PRESET_ID
|
||||
}
|
||||
if (aiPromptSettings.value.activePrivatePresetId === presetId) {
|
||||
aiPromptSettings.value.activePrivatePresetId = DEFAULT_PRIVATE_PRESET_ID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制指定提示词预设
|
||||
*/
|
||||
function duplicatePromptPreset(presetId: string) {
|
||||
const source = allPromptPresets.value.find((p) => p.id === presetId)
|
||||
if (source) {
|
||||
return addPromptPreset({
|
||||
name: `${source.name} (副本)`,
|
||||
chatType: source.chatType,
|
||||
roleDefinition: source.roleDefinition,
|
||||
responseRules: source.responseRules,
|
||||
})
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前激活的群聊预设
|
||||
*/
|
||||
function setActiveGroupPreset(presetId: string) {
|
||||
const preset = allPromptPresets.value.find((p) => p.id === presetId)
|
||||
if (preset && preset.chatType === 'group') {
|
||||
aiPromptSettings.value.activeGroupPresetId = presetId
|
||||
notifyAIConfigChanged()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前激活的私聊预设
|
||||
*/
|
||||
function setActivePrivatePreset(presetId: string) {
|
||||
const preset = allPromptPresets.value.find((p) => p.id === presetId)
|
||||
if (preset && preset.chatType === 'private') {
|
||||
aiPromptSettings.value.activePrivatePresetId = presetId
|
||||
notifyAIConfigChanged()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定聊天类型对应的激活预设
|
||||
*/
|
||||
function getActivePresetForChatType(chatType: 'group' | 'private'): PromptPreset {
|
||||
return chatType === 'group' ? activeGroupPreset.value : activePrivatePreset.value
|
||||
}
|
||||
|
||||
return {
|
||||
// state
|
||||
customPromptPresets,
|
||||
builtinPresetOverrides,
|
||||
aiPromptSettings,
|
||||
aiConfigVersion,
|
||||
aiGlobalSettings,
|
||||
customKeywordTemplates,
|
||||
deletedPresetTemplateIds,
|
||||
// getters
|
||||
allPromptPresets,
|
||||
groupPresets,
|
||||
privatePresets,
|
||||
activeGroupPreset,
|
||||
activePrivatePreset,
|
||||
// actions
|
||||
notifyAIConfigChanged,
|
||||
updateAIGlobalSettings,
|
||||
addCustomKeywordTemplate,
|
||||
updateCustomKeywordTemplate,
|
||||
removeCustomKeywordTemplate,
|
||||
addDeletedPresetTemplateId,
|
||||
addPromptPreset,
|
||||
updatePromptPreset,
|
||||
resetBuiltinPreset,
|
||||
isBuiltinPresetModified,
|
||||
removePromptPreset,
|
||||
duplicatePromptPreset,
|
||||
setActiveGroupPreset,
|
||||
setActivePrivatePreset,
|
||||
getActivePresetForChatType,
|
||||
}
|
||||
},
|
||||
{
|
||||
persist: [
|
||||
{
|
||||
pick: [
|
||||
'customKeywordTemplates',
|
||||
'deletedPresetTemplateIds',
|
||||
'aiGlobalSettings',
|
||||
'customPromptPresets',
|
||||
'builtinPresetOverrides',
|
||||
'aiPromptSettings',
|
||||
],
|
||||
storage: localStorage,
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,226 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import type { AnalysisSession, ImportProgress } from '@/types/chat'
|
||||
|
||||
/**
|
||||
* 会话与导入相关的全局状态
|
||||
*/
|
||||
export const useSessionStore = defineStore(
|
||||
'session',
|
||||
() => {
|
||||
// 会话列表
|
||||
const sessions = ref<AnalysisSession[]>([])
|
||||
// 当前会话 ID
|
||||
const currentSessionId = ref<string | null>(null)
|
||||
// 导入状态
|
||||
const isImporting = ref(false)
|
||||
const importProgress = ref<ImportProgress | null>(null)
|
||||
// 是否初始化完成
|
||||
const isInitialized = ref(false)
|
||||
|
||||
// 当前选中的会话
|
||||
const currentSession = computed(() => {
|
||||
if (!currentSessionId.value) return null
|
||||
return sessions.value.find((s) => s.id === currentSessionId.value) || null
|
||||
})
|
||||
|
||||
/**
|
||||
* 从数据库加载会话列表
|
||||
*/
|
||||
async function loadSessions() {
|
||||
try {
|
||||
const list = await window.chatApi.getSessions()
|
||||
sessions.value = list
|
||||
// 如果当前选中的会话不存在了,清除选中状态
|
||||
if (currentSessionId.value && !list.find((s) => s.id === currentSessionId.value)) {
|
||||
currentSessionId.value = null
|
||||
}
|
||||
isInitialized.value = true
|
||||
} catch (error) {
|
||||
console.error('加载会话列表失败:', error)
|
||||
isInitialized.value = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择文件并导入
|
||||
*/
|
||||
async function importFile(): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const result = await window.chatApi.selectFile()
|
||||
if (!result || !result.filePath) {
|
||||
return { success: false, error: '未选择文件' }
|
||||
}
|
||||
if (result.error) {
|
||||
return { success: false, error: result.error }
|
||||
}
|
||||
return await importFileFromPath(result.filePath)
|
||||
} catch (error) {
|
||||
return { success: false, error: String(error) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从指定路径执行导入(支持拖拽)
|
||||
*/
|
||||
async function importFileFromPath(filePath: string): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
isImporting.value = true
|
||||
importProgress.value = {
|
||||
stage: 'detecting',
|
||||
progress: 0,
|
||||
message: '准备导入...',
|
||||
}
|
||||
|
||||
// 进度队列控制
|
||||
const queue: ImportProgress[] = []
|
||||
let isProcessing = false
|
||||
let currentStage = 'reading'
|
||||
let lastStageTime = Date.now()
|
||||
const MIN_STAGE_TIME = 1000
|
||||
|
||||
/**
|
||||
* 处理导入进度队列,确保阶段展示足够时间
|
||||
*/
|
||||
const processQueue = async () => {
|
||||
if (isProcessing) return
|
||||
isProcessing = true
|
||||
|
||||
while (queue.length > 0) {
|
||||
const next = queue[0]
|
||||
|
||||
if (next.stage !== currentStage) {
|
||||
const elapsed = Date.now() - lastStageTime
|
||||
if (elapsed < MIN_STAGE_TIME) {
|
||||
await new Promise((resolve) => setTimeout(resolve, MIN_STAGE_TIME - elapsed))
|
||||
}
|
||||
currentStage = next.stage
|
||||
lastStageTime = Date.now()
|
||||
}
|
||||
|
||||
importProgress.value = queue.shift()!
|
||||
}
|
||||
isProcessing = false
|
||||
}
|
||||
|
||||
const unsubscribe = window.chatApi.onImportProgress((progress) => {
|
||||
if (progress.stage === 'done') return
|
||||
queue.push(progress)
|
||||
processQueue()
|
||||
})
|
||||
|
||||
const importResult = await window.chatApi.import(filePath)
|
||||
unsubscribe()
|
||||
|
||||
while (queue.length > 0 || isProcessing) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
}
|
||||
|
||||
const elapsed = Date.now() - lastStageTime
|
||||
if (elapsed < MIN_STAGE_TIME) {
|
||||
await new Promise((resolve) => setTimeout(resolve, MIN_STAGE_TIME - elapsed))
|
||||
}
|
||||
|
||||
if (importProgress.value) {
|
||||
importProgress.value.progress = 100
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 300))
|
||||
|
||||
if (importResult.success && importResult.sessionId) {
|
||||
await loadSessions()
|
||||
currentSessionId.value = importResult.sessionId
|
||||
return { success: true }
|
||||
} else {
|
||||
return { success: false, error: importResult.error || '导入失败' }
|
||||
}
|
||||
} catch (error) {
|
||||
return { success: false, error: String(error) }
|
||||
} finally {
|
||||
isImporting.value = false
|
||||
setTimeout(() => {
|
||||
importProgress.value = null
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择指定会话
|
||||
*/
|
||||
function selectSession(id: string) {
|
||||
currentSessionId.value = id
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除会话
|
||||
*/
|
||||
async function deleteSession(id: string): Promise<boolean> {
|
||||
try {
|
||||
const success = await window.chatApi.deleteSession(id)
|
||||
if (success) {
|
||||
const index = sessions.value.findIndex((s) => s.id === id)
|
||||
if (index !== -1) {
|
||||
sessions.value.splice(index, 1)
|
||||
}
|
||||
if (currentSessionId.value === id) {
|
||||
currentSessionId.value = null
|
||||
}
|
||||
await loadSessions()
|
||||
}
|
||||
return success
|
||||
} catch (error) {
|
||||
console.error('删除会话失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重命名会话
|
||||
*/
|
||||
async function renameSession(id: string, newName: string): Promise<boolean> {
|
||||
try {
|
||||
const success = await window.chatApi.renameSession(id, newName)
|
||||
if (success) {
|
||||
const session = sessions.value.find((s) => s.id === id)
|
||||
if (session) {
|
||||
session.name = newName
|
||||
}
|
||||
}
|
||||
return success
|
||||
} catch (error) {
|
||||
console.error('重命名会话失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空当前选中会话
|
||||
*/
|
||||
function clearSelection() {
|
||||
currentSessionId.value = null
|
||||
}
|
||||
|
||||
return {
|
||||
sessions,
|
||||
currentSessionId,
|
||||
isImporting,
|
||||
importProgress,
|
||||
isInitialized,
|
||||
currentSession,
|
||||
loadSessions,
|
||||
importFile,
|
||||
importFileFromPath,
|
||||
selectSession,
|
||||
deleteSession,
|
||||
renameSession,
|
||||
clearSelection,
|
||||
}
|
||||
},
|
||||
{
|
||||
persist: [
|
||||
{
|
||||
pick: ['currentSessionId'],
|
||||
storage: sessionStorage,
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
Reference in New Issue
Block a user