refactor: 重构AIChat组织

This commit is contained in:
digua
2026-03-15 15:03:01 +08:00
committed by digua
parent db9b67071c
commit ef7ac49959
21 changed files with 15 additions and 236 deletions
@@ -1,20 +1,20 @@
<script setup lang="ts">
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import ConversationList from './ConversationList.vue'
import DataSourcePanel from './DataSourcePanel.vue'
import ChatMessage from './ChatMessage.vue'
import AIChatInput from './AIChatInput.vue'
import AIThinkingIndicator from './AIThinkingIndicator.vue'
import ChatStatusBar from './ChatStatusBar.vue'
import ConversationList from './chat/ConversationList.vue'
import DataSourcePanel from './chat/DataSourcePanel.vue'
import ChatMessage from './chat/ChatMessage.vue'
import AIChatInput from './input/AIChatInput.vue'
import AIThinkingIndicator from './chat/AIThinkingIndicator.vue'
import ChatStatusBar from './chat/ChatStatusBar.vue'
import { useAIChat } from '@/composables/useAIChat'
import CaptureButton from '@/components/common/CaptureButton.vue'
import AssistantSelector from './AssistantSelector.vue'
import AssistantConfigModal from './AssistantConfigModal.vue'
import AssistantMarketModal from './AssistantMarketModal.vue'
import SkillMarketModal from './SkillMarketModal.vue'
import SkillConfigModal from './SkillConfigModal.vue'
import PresetQuestions from './PresetQuestions.vue'
import AssistantSelector from './assistant/AssistantSelector.vue'
import AssistantConfigModal from './assistant/AssistantConfigModal.vue'
import AssistantMarketModal from './assistant/AssistantMarketModal.vue'
import SkillMarketModal from './skill/SkillMarketModal.vue'
import SkillConfigModal from './skill/SkillConfigModal.vue'
import PresetQuestions from './input/PresetQuestions.vue'
import { usePromptStore } from '@/stores/prompt'
import { useSettingsStore } from '@/stores/settings'
import { useAssistantStore } from '@/stores/assistant'
+2
View File
@@ -0,0 +1,2 @@
// AIChat 组件统一导出,外部优先通过目录入口引用,降低后续目录调整的影响。
export { default as ChatExplorer } from './ChatExplorer.vue'
@@ -1,67 +0,0 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
// Props
const props = defineProps<{
disabled?: boolean
placeholder?: string
status?: 'ready' | 'submitted' | 'streaming' | 'error'
}>()
// Emits
const emit = defineEmits<{
send: [content: string]
stop: []
}>()
// 输入内容
const inputValue = ref('')
// 计算 status
const chatStatus = computed(() => {
if (props.disabled) {
return props.status || 'submitted'
}
return 'ready'
})
// 发送消息
function handleSubmit() {
if (!inputValue.value.trim() || props.disabled) return
emit('send', inputValue.value.trim())
inputValue.value = ''
}
// 停止生成
function handleStop() {
emit('stop')
}
</script>
<template>
<div class="shrink-0 border-t border-gray-200 pt-4 pb-2 dark:border-gray-800">
<div class="w-full">
<UChatPrompt
v-model="inputValue"
:placeholder="placeholder || t('ai.chat.input.placeholder')"
:disabled="disabled"
variant="subtle"
@submit="handleSubmit"
>
<UButton
v-if="chatStatus === 'streaming'"
icon="i-heroicons-stop-20-solid"
color="primary"
variant="solid"
class="rounded-full"
@click="handleStop"
/>
<UChatPromptSubmit v-else :status="chatStatus" class="rounded-full" color="primary" @stop="handleStop" />
</UChatPrompt>
</div>
</div>
</template>
@@ -1,55 +0,0 @@
<script setup lang="ts">
import type { SkillSummary } from '@/stores/skill'
defineProps<{
skill: SkillSummary
selected?: boolean
disabled?: boolean
}>()
const emit = defineEmits<{
select: [id: string]
}>()
function getChatScopeIcon(scope: string): string {
if (scope === 'group') return 'i-heroicons-user-group'
if (scope === 'private') return 'i-heroicons-user'
return 'i-heroicons-globe-alt'
}
</script>
<template>
<div
class="group relative cursor-pointer rounded-lg border px-3 py-2 transition-all duration-150"
:class="[
disabled
? 'cursor-not-allowed border-gray-200 bg-gray-50 opacity-50 dark:border-gray-700 dark:bg-gray-800/50'
: selected
? 'border-primary-500 bg-primary-50 shadow-sm dark:border-primary-400 dark:bg-primary-950/30'
: 'border-gray-200 bg-white hover:border-primary-300 hover:shadow-sm dark:border-gray-700 dark:bg-gray-800 dark:hover:border-primary-600',
]"
@click="!disabled && emit('select', skill.id)"
>
<div class="flex items-start gap-2">
<UIcon :name="getChatScopeIcon(skill.chatScope)" class="mt-0.5 h-3.5 w-3.5 shrink-0 text-gray-400" />
<div class="min-w-0 flex-1">
<h4 class="truncate text-xs font-medium text-gray-900 dark:text-gray-100">
{{ skill.name }}
</h4>
<p class="mt-0.5 line-clamp-1 text-[11px] leading-relaxed text-gray-500 dark:text-gray-400">
{{ skill.description }}
</p>
</div>
</div>
<!-- Tags -->
<div v-if="skill.tags.length" class="mt-1.5 flex flex-wrap gap-1">
<span
v-for="tag in skill.tags.slice(0, 3)"
:key="tag"
class="rounded-full bg-gray-100 px-1.5 py-0.5 text-[10px] text-gray-500 dark:bg-gray-700 dark:text-gray-400"
>
{{ tag }}
</span>
</div>
</div>
</template>
@@ -1,101 +0,0 @@
<script setup lang="ts">
import { onMounted, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { storeToRefs } from 'pinia'
import { useSkillStore } from '@/stores/skill'
import SkillCard from './SkillCard.vue'
const { t } = useI18n()
const props = defineProps<{
chatType: 'group' | 'private'
}>()
const emit = defineEmits<{
manage: []
}>()
const skillStore = useSkillStore()
const { compatibleSkills, activeSkillId, isLoaded } = storeToRefs(skillStore)
watch(
() => props.chatType,
(chatType) => {
skillStore.setFilterContext(chatType)
},
{ immediate: true }
)
onMounted(async () => {
if (!isLoaded.value) {
await skillStore.loadSkills()
}
})
function handleSelectSkill(id: string) {
if (activeSkillId.value === id) {
skillStore.activateSkill(null)
} else {
skillStore.activateSkill(id)
}
}
function handleFreeChat() {
skillStore.activateSkill(null)
}
</script>
<template>
<div class="space-y-3 rounded-xl border border-gray-200 bg-white p-3 dark:border-gray-700 dark:bg-gray-800">
<!-- 标题 + 管理入口 -->
<div class="flex items-center justify-between">
<span class="text-xs font-medium text-gray-600 dark:text-gray-300">{{ t('ai.skill.selector.title') }}</span>
<button
class="text-[11px] text-gray-400 transition-colors hover:text-primary-500 dark:text-gray-500 dark:hover:text-primary-400"
@click="emit('manage')"
>
{{ t('ai.skill.selector.manage') }}
</button>
</div>
<!-- 自由对话选项 -->
<div
class="cursor-pointer rounded-lg border px-3 py-2 transition-all duration-150"
:class="[
!activeSkillId
? 'border-primary-500 bg-primary-50 dark:border-primary-400 dark:bg-primary-950/30'
: 'border-gray-200 hover:border-primary-300 dark:border-gray-700 dark:hover:border-primary-600',
]"
@click="handleFreeChat"
>
<div class="flex items-center gap-2">
<UIcon name="i-heroicons-chat-bubble-left-right" class="h-3.5 w-3.5 text-gray-500" />
<div>
<span class="text-xs font-medium text-gray-900 dark:text-gray-100">
{{ t('ai.skill.selector.freeChat') }}
</span>
<p class="text-[11px] text-gray-400 dark:text-gray-500">
{{ t('ai.skill.selector.freeChatDesc') }}
</p>
</div>
</div>
</div>
<!-- 技能列表 -->
<div v-if="compatibleSkills.length > 0" class="max-h-48 space-y-1.5 overflow-y-auto">
<SkillCard
v-for="skill in compatibleSkills"
:key="skill.id"
:skill="skill"
:selected="activeSkillId === skill.id"
@select="handleSelectSkill"
/>
</div>
<!-- 无可用技能 -->
<div v-else-if="isLoaded" class="py-3 text-center">
<p class="text-xs text-gray-400 dark:text-gray-500">{{ t('ai.skill.selector.noSkills') }}</p>
<p class="mt-1 text-[11px] text-gray-400 dark:text-gray-500">{{ t('ai.skill.selector.noSkillsHint') }}</p>
</div>
</div>
</template>
+1 -1
View File
@@ -2,7 +2,7 @@
import { ref, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { SubTabs } from '@/components/UI'
import ChatExplorer from './AIChat/ChatExplorer.vue'
import { ChatExplorer } from '../AIChat'
import SQLLabTab from './SQLLabTab.vue'
const { t } = useI18n()