feat: 拆分chatStore

This commit is contained in:
digua
2025-12-15 10:39:08 +08:00
parent 266f8644e1
commit 5c48f0c5c3
20 changed files with 715 additions and 690 deletions
+15 -8
View File
@@ -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 />
+6 -6
View File
@@ -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>
+10 -7
View File
@@ -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>
+3 -3
View File
@@ -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(() => {
+4 -4
View File
@@ -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
+5 -5
View File
@@ -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('/')
}
}
+9 -9
View File
@@ -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)
}
}
+5 -5
View File
@@ -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('/')
}
}
-584
View File
@@ -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,
},
],
}
)
+83
View File
@@ -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,
},
],
}
)
+293
View File
@@ -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,
},
],
}
)
+226
View File
@@ -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,
},
],
}
)