mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-05-13 01:30:57 +08:00
feat: 样式优化
This commit is contained in:
@@ -58,9 +58,9 @@ class MainProcess {
|
||||
async createWindow() {
|
||||
this.mainWindow = new BrowserWindow({
|
||||
width: 1180,
|
||||
height: 720,
|
||||
height: 752,
|
||||
minWidth: 1180,
|
||||
minHeight: 720,
|
||||
minHeight: 752,
|
||||
show: false,
|
||||
autoHideMenuBar: true,
|
||||
webPreferences: {
|
||||
|
||||
@@ -97,7 +97,7 @@ const mainIpcMain = (win: BrowserWindow) => {
|
||||
if (data.resize) {
|
||||
win.setResizable(true)
|
||||
} else {
|
||||
win.setSize(1180, 720)
|
||||
win.setSize(1180, 752)
|
||||
win.setResizable(false)
|
||||
}
|
||||
})
|
||||
|
||||
Vendored
+1
@@ -13,6 +13,7 @@ declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
UAlert: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/Alert.vue')['default']
|
||||
UApp: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/App.vue')['default']
|
||||
UBadge: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/Badge.vue')['default']
|
||||
UButton: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/Button.vue')['default']
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import type { CatchphraseAnalysis, RepeatAnalysis } from '@/types/chat'
|
||||
import { ListPro } from '@/components/charts'
|
||||
import { SectionCard, EmptyState, LoadingState } from '@/components/UI'
|
||||
import { KeywordAnalysis } from './quotes'
|
||||
import { formatDate, getRankBadgeClass } from '@/utils'
|
||||
import { ref } from 'vue'
|
||||
import { SubTabs } from '@/components/UI'
|
||||
import { CatchphraseTab, HotRepeatTab, KeywordAnalysis } from './quotes'
|
||||
|
||||
interface TimeFilter {
|
||||
startTs?: number
|
||||
@@ -16,141 +13,55 @@ const props = defineProps<{
|
||||
timeFilter?: TimeFilter
|
||||
}>()
|
||||
|
||||
// ==================== 口头禅分析 ====================
|
||||
const catchphraseAnalysis = ref<CatchphraseAnalysis | null>(null)
|
||||
const isLoadingCatchphrase = ref(false)
|
||||
// 子 Tab 配置
|
||||
const subTabs = [
|
||||
{ id: 'catchphrase', label: '口头禅', icon: 'i-heroicons-chat-bubble-bottom-center-text' },
|
||||
{ id: 'hot-repeat', label: '最火复读', icon: 'i-heroicons-fire' },
|
||||
{ id: 'keyword', label: '关键词分析', icon: 'i-heroicons-magnifying-glass' },
|
||||
]
|
||||
|
||||
async function loadCatchphraseAnalysis() {
|
||||
if (!props.sessionId) return
|
||||
isLoadingCatchphrase.value = true
|
||||
try {
|
||||
catchphraseAnalysis.value = await window.chatApi.getCatchphraseAnalysis(props.sessionId, props.timeFilter)
|
||||
} catch (error) {
|
||||
console.error('加载口头禅分析失败:', error)
|
||||
} finally {
|
||||
isLoadingCatchphrase.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 最火复读内容 ====================
|
||||
const repeatAnalysis = ref<RepeatAnalysis | null>(null)
|
||||
const isLoadingRepeat = ref(false)
|
||||
|
||||
async function loadRepeatAnalysis() {
|
||||
if (!props.sessionId) return
|
||||
isLoadingRepeat.value = true
|
||||
try {
|
||||
repeatAnalysis.value = await window.chatApi.getRepeatAnalysis(props.sessionId, props.timeFilter)
|
||||
} catch (error) {
|
||||
console.error('加载复读分析失败:', error)
|
||||
} finally {
|
||||
isLoadingRepeat.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function truncateContent(content: string, maxLength = 30): string {
|
||||
if (content.length <= maxLength) return content
|
||||
return content.slice(0, maxLength) + '...'
|
||||
}
|
||||
|
||||
// 监听 sessionId 和 timeFilter 变化
|
||||
watch(
|
||||
() => [props.sessionId, props.timeFilter],
|
||||
() => {
|
||||
loadCatchphraseAnalysis()
|
||||
loadRepeatAnalysis()
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
)
|
||||
const activeSubTab = ref('catchphrase')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mx-auto max-w-3xl space-y-6 p-6">
|
||||
<!-- 口头禅分析模块 -->
|
||||
<LoadingState v-if="isLoadingCatchphrase" text="正在分析口头禅数据..." />
|
||||
<div class="flex h-full flex-col">
|
||||
<!-- 子 Tab 导航 -->
|
||||
<SubTabs v-model="activeSubTab" :items="subTabs" />
|
||||
|
||||
<ListPro
|
||||
v-else-if="catchphraseAnalysis && catchphraseAnalysis.members.length > 0"
|
||||
:items="catchphraseAnalysis.members"
|
||||
title="💬 口头禅分析"
|
||||
:description="`分析了 ${catchphraseAnalysis.members.length} 位成员的高频发言`"
|
||||
countTemplate="共 {count} 位成员"
|
||||
>
|
||||
<template #item="{ item: member }">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="w-28 shrink-0 pt-1 font-medium text-gray-900 dark:text-white">
|
||||
{{ member.name }}
|
||||
</div>
|
||||
<!-- 子 Tab 内容 -->
|
||||
<div class="flex-1 min-h-0 overflow-auto">
|
||||
<Transition name="fade" mode="out-in">
|
||||
<!-- 口头禅分析 -->
|
||||
<CatchphraseTab
|
||||
v-if="activeSubTab === 'catchphrase'"
|
||||
:session-id="props.sessionId"
|
||||
:time-filter="props.timeFilter"
|
||||
/>
|
||||
|
||||
<div class="flex flex-1 flex-wrap items-center gap-2">
|
||||
<div
|
||||
v-for="(phrase, index) in member.catchphrases"
|
||||
:key="index"
|
||||
class="flex items-center gap-1.5 rounded-lg px-3 py-1.5"
|
||||
:class="
|
||||
index === 0
|
||||
? 'bg-amber-50 dark:bg-amber-900/20'
|
||||
: index === 1
|
||||
? 'bg-gray-100 dark:bg-gray-800'
|
||||
: 'bg-gray-50 dark:bg-gray-800/50'
|
||||
"
|
||||
>
|
||||
<span
|
||||
class="text-sm"
|
||||
:class="
|
||||
index === 0 ? 'font-medium text-amber-700 dark:text-amber-400' : 'text-gray-700 dark:text-gray-300'
|
||||
"
|
||||
:title="phrase.content"
|
||||
>
|
||||
{{ truncateContent(phrase.content, 20) }}
|
||||
</span>
|
||||
<span class="text-xs text-gray-400">{{ phrase.count }}次</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 最火复读内容 -->
|
||||
<HotRepeatTab
|
||||
v-else-if="activeSubTab === 'hot-repeat'"
|
||||
:session-id="props.sessionId"
|
||||
:time-filter="props.timeFilter"
|
||||
/>
|
||||
|
||||
<!-- 关键词分析 -->
|
||||
<div v-else-if="activeSubTab === 'keyword'" class="mx-auto max-w-3xl p-6">
|
||||
<KeywordAnalysis :session-id="props.sessionId" :time-filter="props.timeFilter" />
|
||||
</div>
|
||||
</template>
|
||||
</ListPro>
|
||||
|
||||
<SectionCard v-else title="💬 口头禅分析">
|
||||
<EmptyState text="暂无口头禅数据" />
|
||||
</SectionCard>
|
||||
|
||||
<!-- 最火复读内容 -->
|
||||
<LoadingState v-if="isLoadingRepeat" text="正在加载复读数据..." />
|
||||
|
||||
<ListPro
|
||||
v-else-if="repeatAnalysis && repeatAnalysis.hotContents.length > 0"
|
||||
:items="repeatAnalysis.hotContents"
|
||||
title="🔥 最火复读内容"
|
||||
description="单次复读参与人数最多的内容"
|
||||
:topN="10"
|
||||
countTemplate="共 {count} 条热门复读"
|
||||
>
|
||||
<template #item="{ item, index }">
|
||||
<div class="flex items-center gap-3">
|
||||
<span
|
||||
class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-xs font-bold"
|
||||
:class="getRankBadgeClass(index)"
|
||||
>
|
||||
{{ index + 1 }}
|
||||
</span>
|
||||
<span class="shrink-0 text-lg font-bold text-pink-600">{{ item.maxChainLength }}人</span>
|
||||
<div class="flex flex-1 items-center gap-1 overflow-hidden text-sm">
|
||||
<span class="shrink-0 font-medium text-gray-900 dark:text-white">{{ item.originatorName }}:</span>
|
||||
<span class="truncate text-gray-600 dark:text-gray-400" :title="item.content">
|
||||
{{ truncateContent(item.content) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex shrink-0 items-center gap-2 text-xs text-gray-500">
|
||||
<span>{{ item.count }} 次</span>
|
||||
<span class="text-gray-300 dark:text-gray-600">|</span>
|
||||
<span>{{ formatDate(item.lastTs) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ListPro>
|
||||
|
||||
<!-- 关键词分析 -->
|
||||
<KeywordAnalysis :session-id="sessionId" :time-filter="timeFilter" />
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -269,7 +269,7 @@ watch(
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex justify-end">
|
||||
<UButton color="gray" variant="soft" @click="showMemberDetailModal = false">关闭</UButton>
|
||||
<UButton variant="soft" @click="showMemberDetailModal = false">关闭</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -54,20 +54,12 @@ function highlightKeywords(text: string): string {
|
||||
<!-- 展开状态 -->
|
||||
<template v-else>
|
||||
<!-- 头部 -->
|
||||
<div
|
||||
class="flex items-center justify-between border-b border-gray-200 px-4 py-3 dark:border-gray-800"
|
||||
>
|
||||
<div class="flex items-center justify-between border-b border-gray-200 px-4 py-3 dark:border-gray-800">
|
||||
<div class="flex items-center gap-2">
|
||||
<UIcon name="i-heroicons-document-magnifying-glass" class="h-5 w-5 text-gray-500" />
|
||||
<span class="font-medium text-gray-900 dark:text-white">数据源</span>
|
||||
</div>
|
||||
<UButton
|
||||
icon="i-heroicons-chevron-right"
|
||||
color="gray"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
@click="emit('toggle')"
|
||||
/>
|
||||
<UButton icon="i-heroicons-chevron-right" color="gray" variant="ghost" size="xs" @click="emit('toggle')" />
|
||||
</div>
|
||||
|
||||
<!-- 当前关键词 -->
|
||||
@@ -92,10 +84,7 @@ function highlightKeywords(text: string): string {
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div
|
||||
v-else-if="messages.length === 0"
|
||||
class="flex flex-col items-center justify-center py-8 text-center"
|
||||
>
|
||||
<div v-else-if="messages.length === 0" class="flex flex-col items-center justify-center py-8 text-center">
|
||||
<UIcon name="i-heroicons-inbox" class="h-10 w-10 text-gray-300 dark:text-gray-600" />
|
||||
<p class="mt-2 text-sm text-gray-500">暂无数据</p>
|
||||
<p class="text-xs text-gray-400">发送问题后,相关记录会显示在这里</p>
|
||||
@@ -114,33 +103,22 @@ function highlightKeywords(text: string): string {
|
||||
</span>
|
||||
<span class="text-xs text-gray-400">{{ formatTime(msg.timestamp) }}</span>
|
||||
</div>
|
||||
<p
|
||||
class="line-clamp-3 text-sm text-gray-600 dark:text-gray-400"
|
||||
v-html="highlightKeywords(msg.content)"
|
||||
/>
|
||||
<p class="line-clamp-3 text-sm text-gray-600 dark:text-gray-400" v-html="highlightKeywords(msg.content)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部统计 & 加载更多 -->
|
||||
<div
|
||||
v-if="messages.length > 0"
|
||||
class="border-t border-gray-200 px-4 py-2 dark:border-gray-800"
|
||||
>
|
||||
<div v-if="messages.length > 0" class="border-t border-gray-200 px-4 py-2 dark:border-gray-800">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-gray-500">共 {{ messages.length }} 条记录</span>
|
||||
<UButton size="xs" color="gray" variant="ghost" @click="emit('loadMore')">
|
||||
加载更多
|
||||
</UButton>
|
||||
<UButton size="xs" variant="ghost" @click="emit('loadMore')">加载更多</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 半透明数据流背景效果 -->
|
||||
<div
|
||||
v-if="!isCollapsed"
|
||||
class="pointer-events-none absolute inset-0 overflow-hidden rounded-xl opacity-[0.03]"
|
||||
>
|
||||
<div v-if="!isCollapsed" class="pointer-events-none absolute inset-0 overflow-hidden rounded-xl opacity-[0.03]">
|
||||
<div class="data-flow-bg absolute inset-0" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -154,13 +132,7 @@ function highlightKeywords(text: string): string {
|
||||
|
||||
/* 数据流背景动画 */
|
||||
.data-flow-bg {
|
||||
background: repeating-linear-gradient(
|
||||
0deg,
|
||||
transparent,
|
||||
transparent 20px,
|
||||
currentColor 20px,
|
||||
currentColor 21px
|
||||
);
|
||||
background: repeating-linear-gradient(0deg, transparent, transparent 20px, currentColor 20px, currentColor 21px);
|
||||
animation: dataFlow 20s linear infinite;
|
||||
}
|
||||
|
||||
@@ -173,4 +145,3 @@ function highlightKeywords(text: string): string {
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import type { CatchphraseAnalysis } from '@/types/chat'
|
||||
import { ListPro } from '@/components/charts'
|
||||
import { SectionCard, EmptyState, LoadingState } from '@/components/UI'
|
||||
|
||||
interface TimeFilter {
|
||||
startTs?: number
|
||||
endTs?: number
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
sessionId: string
|
||||
timeFilter?: TimeFilter
|
||||
}>()
|
||||
|
||||
// ==================== 口头禅分析 ====================
|
||||
const catchphraseAnalysis = ref<CatchphraseAnalysis | null>(null)
|
||||
const isLoading = ref(false)
|
||||
|
||||
async function loadCatchphraseAnalysis() {
|
||||
if (!props.sessionId) return
|
||||
isLoading.value = true
|
||||
try {
|
||||
catchphraseAnalysis.value = await window.chatApi.getCatchphraseAnalysis(props.sessionId, props.timeFilter)
|
||||
} catch (error) {
|
||||
console.error('加载口头禅分析失败:', error)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function truncateContent(content: string, maxLength = 20): string {
|
||||
if (content.length <= maxLength) return content
|
||||
return content.slice(0, maxLength) + '...'
|
||||
}
|
||||
|
||||
// 监听 sessionId 和 timeFilter 变化
|
||||
watch(
|
||||
() => [props.sessionId, props.timeFilter],
|
||||
() => {
|
||||
loadCatchphraseAnalysis()
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mx-auto max-w-3xl p-6">
|
||||
<!-- 加载中 -->
|
||||
<LoadingState v-if="isLoading" text="正在分析口头禅数据..." />
|
||||
|
||||
<!-- 口头禅列表 -->
|
||||
<ListPro
|
||||
v-else-if="catchphraseAnalysis && catchphraseAnalysis.members.length > 0"
|
||||
:items="catchphraseAnalysis.members"
|
||||
title="💬 口头禅分析"
|
||||
:description="`分析了 ${catchphraseAnalysis.members.length} 位成员的高频发言`"
|
||||
countTemplate="共 {count} 位成员"
|
||||
>
|
||||
<template #item="{ item: member }">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="w-28 shrink-0 pt-1 font-medium text-gray-900 dark:text-white">
|
||||
{{ member.name }}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-1 flex-wrap items-center gap-2">
|
||||
<div
|
||||
v-for="(phrase, index) in member.catchphrases"
|
||||
:key="index"
|
||||
class="flex items-center gap-1.5 rounded-lg px-3 py-1.5"
|
||||
:class="
|
||||
index === 0
|
||||
? 'bg-amber-50 dark:bg-amber-900/20'
|
||||
: index === 1
|
||||
? 'bg-gray-100 dark:bg-gray-800'
|
||||
: 'bg-gray-50 dark:bg-gray-800/50'
|
||||
"
|
||||
>
|
||||
<span
|
||||
class="text-sm"
|
||||
:class="
|
||||
index === 0 ? 'font-medium text-amber-700 dark:text-amber-400' : 'text-gray-700 dark:text-gray-300'
|
||||
"
|
||||
:title="phrase.content"
|
||||
>
|
||||
{{ truncateContent(phrase.content) }}
|
||||
</span>
|
||||
<span class="text-xs text-gray-400">{{ phrase.count }}次</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ListPro>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<SectionCard v-else title="💬 口头禅分析">
|
||||
<EmptyState text="暂无口头禅数据" />
|
||||
</SectionCard>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,92 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import type { RepeatAnalysis } from '@/types/chat'
|
||||
import { ListPro } from '@/components/charts'
|
||||
import { LoadingState, EmptyState, SectionCard } from '@/components/UI'
|
||||
import { formatDate, getRankBadgeClass } from '@/utils'
|
||||
|
||||
interface TimeFilter {
|
||||
startTs?: number
|
||||
endTs?: number
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
sessionId: string
|
||||
timeFilter?: TimeFilter
|
||||
}>()
|
||||
|
||||
// ==================== 最火复读内容 ====================
|
||||
const repeatAnalysis = ref<RepeatAnalysis | null>(null)
|
||||
const isLoading = ref(false)
|
||||
|
||||
async function loadRepeatAnalysis() {
|
||||
if (!props.sessionId) return
|
||||
isLoading.value = true
|
||||
try {
|
||||
repeatAnalysis.value = await window.chatApi.getRepeatAnalysis(props.sessionId, props.timeFilter)
|
||||
} catch (error) {
|
||||
console.error('加载复读分析失败:', error)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function truncateContent(content: string, maxLength = 30): string {
|
||||
if (content.length <= maxLength) return content
|
||||
return content.slice(0, maxLength) + '...'
|
||||
}
|
||||
|
||||
// 监听 sessionId 和 timeFilter 变化
|
||||
watch(
|
||||
() => [props.sessionId, props.timeFilter],
|
||||
() => {
|
||||
loadRepeatAnalysis()
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mx-auto max-w-3xl p-6">
|
||||
<!-- 加载中 -->
|
||||
<LoadingState v-if="isLoading" text="正在加载复读数据..." />
|
||||
|
||||
<!-- 最火复读内容列表 -->
|
||||
<ListPro
|
||||
v-else-if="repeatAnalysis && repeatAnalysis.hotContents.length > 0"
|
||||
:items="repeatAnalysis.hotContents"
|
||||
title="🔥 最火复读内容"
|
||||
description="单次复读参与人数最多的内容"
|
||||
:topN="10"
|
||||
countTemplate="共 {count} 条热门复读"
|
||||
>
|
||||
<template #item="{ item, index }">
|
||||
<div class="flex items-center gap-3">
|
||||
<span
|
||||
class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-xs font-bold"
|
||||
:class="getRankBadgeClass(index)"
|
||||
>
|
||||
{{ index + 1 }}
|
||||
</span>
|
||||
<span class="shrink-0 text-lg font-bold text-pink-600">{{ item.maxChainLength }}人</span>
|
||||
<div class="flex flex-1 items-center gap-1 overflow-hidden text-sm">
|
||||
<span class="shrink-0 font-medium text-gray-900 dark:text-white">{{ item.originatorName }}:</span>
|
||||
<span class="truncate text-gray-600 dark:text-gray-400" :title="item.content">
|
||||
{{ truncateContent(item.content) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex shrink-0 items-center gap-2 text-xs text-gray-500">
|
||||
<span>{{ item.count }} 次</span>
|
||||
<span class="text-gray-300 dark:text-gray-600">|</span>
|
||||
<span>{{ formatDate(item.lastTs) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ListPro>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<SectionCard v-else title="🔥 最火复读内容">
|
||||
<EmptyState text="暂无复读数据" />
|
||||
</SectionCard>
|
||||
</div>
|
||||
</template>
|
||||
@@ -446,7 +446,7 @@ watch(
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex justify-end gap-2">
|
||||
<UButton color="gray" variant="soft" @click="showTemplateModal = false">取消</UButton>
|
||||
<UButton variant="soft" @click="showTemplateModal = false">取消</UButton>
|
||||
<UButton
|
||||
color="primary"
|
||||
:disabled="!templateName.trim() || templateKeywords.length === 0"
|
||||
@@ -471,10 +471,10 @@ watch(
|
||||
{{ keyword }}
|
||||
<span class="ml-0.5 hover:text-red-500">×</span>
|
||||
</UBadge>
|
||||
<UInput v-model="newKeyword" placeholder="输入后回车添加" class="w-32" @keydown.enter.prevent="addKeyword" />
|
||||
<UInput v-model="newKeyword" placeholder="输入并搜索" class="w-32" @keydown.enter.prevent="addKeyword" />
|
||||
<button
|
||||
v-if="currentKeywords.length > 0"
|
||||
class="text-xs text-gray-400 hover:text-red-500"
|
||||
class="text-sm text-pink-500 hover:text-red-500"
|
||||
@click="clearAllKeywords"
|
||||
>
|
||||
清空
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
export { default as KeywordAnalysis } from './KeywordAnalysis.vue'
|
||||
export { default as CatchphraseTab } from './CatchphraseTab.vue'
|
||||
export { default as HotRepeatTab } from './HotRepeatTab.vue'
|
||||
|
||||
@@ -47,7 +47,7 @@ const formattedCount = computed(() => props.countTemplate.replace('{count}', Str
|
||||
|
||||
<!-- 完整列表弹窗 -->
|
||||
<UModal v-model:open="isOpen" :ui="{ content: 'md:w-full max-w-4xl' }">
|
||||
<UButton v-if="showViewAll" icon="i-heroicons-list-bullet" color="gray" variant="ghost">查看完整排行</UButton>
|
||||
<UButton v-if="showViewAll" icon="i-heroicons-list-bullet" variant="ghost">查看完整排行</UButton>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">{{ title }}</h3>
|
||||
|
||||
@@ -45,7 +45,7 @@ const showViewAll = computed(() => {
|
||||
|
||||
<!-- 完整排行榜 Dialog -->
|
||||
<UModal v-model:open="isOpen" :ui="{ content: 'md:w-full max-w-3xl' }">
|
||||
<UButton v-if="showViewAll" icon="i-heroicons-list-bullet" color="gray" variant="ghost">查看完整排行</UButton>
|
||||
<UButton v-if="showViewAll" icon="i-heroicons-list-bullet" variant="ghost">查看完整排行</UButton>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">{{ title }}</h3>
|
||||
|
||||
@@ -55,7 +55,7 @@ watch(
|
||||
<!-- Header -->
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">全局设置</h2>
|
||||
<UButton icon="i-heroicons-x-mark" color="gray" variant="ghost" size="sm" @click="closeModal" />
|
||||
<UButton icon="i-heroicons-x-mark" variant="ghost" size="sm" @click="closeModal" />
|
||||
</div>
|
||||
|
||||
<!-- Tab 导航 -->
|
||||
@@ -104,7 +104,7 @@ watch(
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">深色模式</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">跟随系统自动切换</p>
|
||||
</div>
|
||||
<UBadge color="gray" variant="soft">自动</UBadge>
|
||||
<UBadge variant="soft">自动</UBadge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -256,7 +256,7 @@ function getContextMenuItems(session: AnalysisSession) {
|
||||
@keydown.enter="handleRename"
|
||||
/>
|
||||
<div class="flex justify-end gap-2">
|
||||
<UButton color="gray" variant="soft" @click="closeRenameModal">取消</UButton>
|
||||
<UButton variant="soft" @click="closeRenameModal">取消</UButton>
|
||||
<UButton color="primary" :disabled="!newName.trim()" @click="handleRename">确定</UButton>
|
||||
</div>
|
||||
</div>
|
||||
@@ -274,7 +274,7 @@ function getContextMenuItems(session: AnalysisSession) {
|
||||
吗?此操作无法撤销。
|
||||
</p>
|
||||
<div class="flex justify-end gap-2">
|
||||
<UButton color="gray" variant="soft" @click="closeDeleteModal">取消</UButton>
|
||||
<UButton variant="soft" @click="closeDeleteModal">取消</UButton>
|
||||
<UButton color="error" @click="confirmDelete">删除</UButton>
|
||||
</div>
|
||||
</div>
|
||||
@@ -283,8 +283,8 @@ function getContextMenuItems(session: AnalysisSession) {
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="px-4 py-2 dark:border-gray-800 space-y-2">
|
||||
<!-- 问题反馈 -->
|
||||
<UTooltip :text="isCollapsed ? '问题反馈' : ''" :popper="{ placement: 'right' }">
|
||||
<!-- 帮助和反馈 -->
|
||||
<UTooltip :text="isCollapsed ? '帮助和反馈' : ''" :popper="{ placement: 'right' }">
|
||||
<UButton
|
||||
:block="!isCollapsed"
|
||||
class="transition-all rounded-full hover:bg-gray-200/60 dark:hover:bg-gray-800 h-12 cursor-pointer"
|
||||
@@ -297,12 +297,12 @@ function getContextMenuItems(session: AnalysisSession) {
|
||||
class="h-5 w-5 shrink-0"
|
||||
:class="[isCollapsed ? '' : 'mr-2']"
|
||||
/>
|
||||
<span v-if="!isCollapsed" class="truncate">更新和反馈</span>
|
||||
<span v-if="!isCollapsed" class="truncate">帮助和反馈</span>
|
||||
</UButton>
|
||||
</UTooltip>
|
||||
|
||||
<!-- 设置和帮助 -->
|
||||
<UTooltip :text="isCollapsed ? '设置和帮助' : ''" :popper="{ placement: 'right' }">
|
||||
<!-- 设置 -->
|
||||
<UTooltip :text="isCollapsed ? '设置' : ''" :popper="{ placement: 'right' }">
|
||||
<UButton
|
||||
:block="!isCollapsed"
|
||||
class="transition-all rounded-full hover:bg-gray-200/60 dark:hover:bg-gray-800 h-12 cursor-pointer"
|
||||
@@ -312,7 +312,7 @@ function getContextMenuItems(session: AnalysisSession) {
|
||||
@click="chatStore.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>
|
||||
<span v-if="!isCollapsed" class="truncate">设置</span>
|
||||
</UButton>
|
||||
</UTooltip>
|
||||
|
||||
|
||||
@@ -407,7 +407,7 @@ watch(
|
||||
>
|
||||
本地服务
|
||||
</p>
|
||||
<p class="mt-0.5 text-[10px] text-gray-500">Ollama 等</p>
|
||||
<p class="mt-0.5 text-[10px] text-gray-500">Ollama、Parallax 等</p>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -453,6 +453,7 @@ watch(
|
||||
<UInput
|
||||
v-model="formData.name"
|
||||
:placeholder="configType === 'preset' ? '留空将使用服务商名称' : '留空将使用 API 端点地址'"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -524,23 +525,16 @@ watch(
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">API 端点</label>
|
||||
<div class="flex gap-2">
|
||||
<UInput v-model="formData.baseUrl" placeholder="http://localhost:11434/v1" class="flex-1" />
|
||||
<UButton
|
||||
:loading="isValidating"
|
||||
:disabled="!formData.baseUrl"
|
||||
color="gray"
|
||||
variant="soft"
|
||||
@click="validateKey"
|
||||
>
|
||||
<UButton :loading="isValidating" :disabled="!formData.baseUrl" variant="soft" @click="validateKey">
|
||||
测试
|
||||
</UButton>
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-gray-500">Ollama 默认:http://localhost:11434/v1</p>
|
||||
</div>
|
||||
|
||||
<!-- 模型名称 -->
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">模型名称</label>
|
||||
<UInput v-model="formData.model" placeholder="如 llama3.2、qwen2.5、deepseek-r1" />
|
||||
<UInput v-model="formData.model" placeholder="如 qwen3、deepseek-r1" />
|
||||
<p class="mt-1 text-xs text-gray-500">输入本地部署的模型名称</p>
|
||||
</div>
|
||||
|
||||
@@ -659,7 +653,7 @@ watch(
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<div class="mt-6 flex justify-end gap-2">
|
||||
<UButton color="gray" variant="soft" @click="closeModal">取消</UButton>
|
||||
<UButton variant="soft" @click="closeModal">取消</UButton>
|
||||
<UButton color="primary" :disabled="!canSave" :loading="isSaving" @click="saveConfig">
|
||||
{{ mode === 'add' ? '添加' : '保存' }}
|
||||
</UButton>
|
||||
|
||||
@@ -135,6 +135,14 @@ onMounted(() => {
|
||||
|
||||
<!-- 配置列表视图 -->
|
||||
<div v-else class="space-y-4">
|
||||
<UAlert color="warning" variant="outline" icon="i-lucide-terminal" class="p-2">
|
||||
<template #title>
|
||||
<p>
|
||||
强烈建议配置本地模型,分析聊天记录更加安全,个人实测3B小模型也能满足分析需求,而且可以无限量分析聊天记录,参考教程
|
||||
<a href="https://baidu.com" class="text-pink-500" target="_blank">配置Parallax本地模型</a>
|
||||
</p>
|
||||
</template>
|
||||
</UAlert>
|
||||
<!-- 配置列表 -->
|
||||
<div v-if="configs.length > 0" class="space-y-2">
|
||||
<div
|
||||
@@ -207,37 +215,11 @@ onMounted(() => {
|
||||
</div>
|
||||
|
||||
<!-- 添加按钮 -->
|
||||
<UButton block color="gray" variant="soft" :disabled="isMaxConfigs" class="mt-4" @click="openAddModal">
|
||||
<UIcon name="i-heroicons-plus" class="mr-2 h-4 w-4" />
|
||||
{{ isMaxConfigs ? '已达最大配置数量(10个)' : '添加新配置' }}
|
||||
</UButton>
|
||||
|
||||
<!-- 获取 API Key 链接(仅在没有配置时显示) -->
|
||||
<div v-if="configs.length === 0" class="mt-4 rounded-lg bg-gray-50 p-3 dark:bg-gray-800">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">还没有 API Key?前往获取:</p>
|
||||
<div class="mt-2 flex flex-wrap gap-3">
|
||||
<a
|
||||
href="https://platform.deepseek.com/"
|
||||
target="_blank"
|
||||
class="text-sm text-violet-600 hover:underline dark:text-violet-400"
|
||||
>
|
||||
DeepSeek →
|
||||
</a>
|
||||
<a
|
||||
href="https://dashscope.console.aliyun.com/"
|
||||
target="_blank"
|
||||
class="text-sm text-violet-600 hover:underline dark:text-violet-400"
|
||||
>
|
||||
通义千问 →
|
||||
</a>
|
||||
<a
|
||||
href="https://ollama.com/"
|
||||
target="_blank"
|
||||
class="text-sm text-violet-600 hover:underline dark:text-violet-400"
|
||||
>
|
||||
Ollama (本地) →
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex justify-center">
|
||||
<UButton variant="soft" :disabled="isMaxConfigs" class="mt-4" @click="openAddModal">
|
||||
<UIcon name="i-heroicons-plus" class="mr-2 h-4 w-4" />
|
||||
{{ isMaxConfigs ? '已达最大配置数量(10个)' : '添加新配置' }}
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { useChatStore } from '@/stores/chat'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
const emit = defineEmits<{
|
||||
'config-changed': []
|
||||
}>()
|
||||
|
||||
const chatStore = useChatStore()
|
||||
const { aiGlobalSettings } = storeToRefs(chatStore)
|
||||
|
||||
// 本地状态
|
||||
const maxMessages = ref(aiGlobalSettings.value.maxMessagesPerRequest)
|
||||
const isSaving = ref(false)
|
||||
|
||||
// 保存配置
|
||||
async function saveConfig() {
|
||||
isSaving.value = true
|
||||
try {
|
||||
chatStore.updateAIGlobalSettings({
|
||||
maxMessagesPerRequest: maxMessages.value,
|
||||
})
|
||||
emit('config-changed')
|
||||
} finally {
|
||||
isSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 监听 store 变化
|
||||
watch(
|
||||
() => aiGlobalSettings.value.maxMessagesPerRequest,
|
||||
(newVal) => {
|
||||
maxMessages.value = newVal
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<!-- 标题 -->
|
||||
<h3 class="mb-3 flex items-center gap-2 text-sm font-semibold text-gray-900 dark:text-white">
|
||||
<UIcon name="i-heroicons-chat-bubble-left-right" class="h-4 w-4 text-primary-500" />
|
||||
AI 聊天配置
|
||||
</h3>
|
||||
|
||||
<div class="space-y-4 rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800/50">
|
||||
<!-- 上下文消息数量 -->
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">上下文消息数量</label>
|
||||
<div class="flex items-center gap-4">
|
||||
<URange v-model="maxMessages" :min="10" :max="1000" :step="10" class="flex-1" />
|
||||
<UInput v-model.number="maxMessages" type="number" class="w-24" />
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
每次发送给 AI 的历史消息数量(10-1000)。数量越多,AI 了解的上下文越多,但消耗的 Token 也越多。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 保存按钮 -->
|
||||
<div class="flex justify-end pt-2">
|
||||
<UButton color="primary" :loading="isSaving" @click="saveConfig">保存配置</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,308 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
'config-changed': []
|
||||
}>()
|
||||
|
||||
// 状态
|
||||
const isLoading = ref(false)
|
||||
const isValidating = ref(false)
|
||||
const isSaving = ref(false)
|
||||
const providers = ref<
|
||||
Array<{
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
models: Array<{ id: string; name: string; description?: string }>
|
||||
}>
|
||||
>([])
|
||||
|
||||
// 表单数据
|
||||
const selectedProvider = ref('')
|
||||
const apiKey = ref('')
|
||||
const selectedModel = ref('')
|
||||
const maxTokens = ref(200)
|
||||
|
||||
// 当前配置状态
|
||||
const hasExistingConfig = ref(false)
|
||||
const existingConfigDisplay = ref('')
|
||||
|
||||
// 验证结果
|
||||
const validationResult = ref<'idle' | 'valid' | 'invalid'>('idle')
|
||||
const validationMessage = ref('')
|
||||
|
||||
// 当前选中的提供商信息
|
||||
const currentProvider = computed(() => {
|
||||
return providers.value.find((p) => p.id === selectedProvider.value)
|
||||
})
|
||||
|
||||
// 模型选项
|
||||
const modelOptions = computed(() => {
|
||||
if (!currentProvider.value) return []
|
||||
return currentProvider.value.models.map((m) => ({
|
||||
label: m.name,
|
||||
value: m.id,
|
||||
description: m.description,
|
||||
}))
|
||||
})
|
||||
|
||||
// 是否可以保存(有提供商,且有新 API Key 或已有配置)
|
||||
const canSave = computed(() => {
|
||||
return selectedProvider.value && (apiKey.value.trim().length > 0 || hasExistingConfig.value)
|
||||
})
|
||||
|
||||
// 加载提供商列表
|
||||
async function loadProviders() {
|
||||
isLoading.value = true
|
||||
try {
|
||||
providers.value = await window.llmApi.getProviders()
|
||||
if (providers.value.length > 0 && !selectedProvider.value) {
|
||||
selectedProvider.value = providers.value[0].id
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载提供商列表失败:', error)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载当前配置
|
||||
async function loadCurrentConfig() {
|
||||
try {
|
||||
const config = await window.llmApi.getConfig()
|
||||
if (config && config.apiKeySet) {
|
||||
hasExistingConfig.value = true
|
||||
existingConfigDisplay.value = config.apiKey
|
||||
selectedProvider.value = config.provider
|
||||
selectedModel.value = config.model || ''
|
||||
maxTokens.value = config.maxTokens || 200
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载当前配置失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 验证 API Key
|
||||
async function validateKey() {
|
||||
if (!selectedProvider.value || !apiKey.value) {
|
||||
validationResult.value = 'idle'
|
||||
validationMessage.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
isValidating.value = true
|
||||
validationResult.value = 'idle'
|
||||
|
||||
try {
|
||||
const isValid = await window.llmApi.validateApiKey(selectedProvider.value, apiKey.value)
|
||||
validationResult.value = isValid ? 'valid' : 'invalid'
|
||||
validationMessage.value = isValid ? 'API Key 验证成功' : 'API Key 无效,请检查'
|
||||
} catch (error) {
|
||||
validationResult.value = 'invalid'
|
||||
validationMessage.value = '验证失败:' + String(error)
|
||||
} finally {
|
||||
isValidating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
async function saveConfig() {
|
||||
if (!canSave.value) return
|
||||
|
||||
isSaving.value = true
|
||||
try {
|
||||
const result = await window.llmApi.saveConfig({
|
||||
provider: selectedProvider.value,
|
||||
apiKey: apiKey.value,
|
||||
model: selectedModel.value || undefined,
|
||||
maxTokens: maxTokens.value,
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
emit('config-changed')
|
||||
// 重新加载配置以更新状态
|
||||
await loadCurrentConfig()
|
||||
apiKey.value = '' // 清空输入框
|
||||
validationResult.value = 'idle'
|
||||
validationMessage.value = ''
|
||||
} else {
|
||||
console.error('保存配置失败:', result.error)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存配置失败:', error)
|
||||
} finally {
|
||||
isSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 删除配置
|
||||
async function deleteConfig() {
|
||||
try {
|
||||
await window.llmApi.deleteConfig()
|
||||
hasExistingConfig.value = false
|
||||
existingConfigDisplay.value = ''
|
||||
apiKey.value = ''
|
||||
validationResult.value = 'idle'
|
||||
validationMessage.value = ''
|
||||
emit('config-changed')
|
||||
} catch (error) {
|
||||
console.error('删除配置失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 监听提供商变化,自动选择默认模型
|
||||
watch(selectedProvider, (newProvider) => {
|
||||
const provider = providers.value.find((p) => p.id === newProvider)
|
||||
if (provider && provider.models.length > 0) {
|
||||
selectedModel.value = provider.models[0].id
|
||||
}
|
||||
// 重置验证状态
|
||||
validationResult.value = 'idle'
|
||||
validationMessage.value = ''
|
||||
})
|
||||
|
||||
// 监听 API Key 变化,重置验证状态
|
||||
watch(apiKey, () => {
|
||||
validationResult.value = 'idle'
|
||||
validationMessage.value = ''
|
||||
})
|
||||
|
||||
// 暴露 refresh 方法
|
||||
defineExpose({
|
||||
refresh: () => {
|
||||
loadProviders()
|
||||
loadCurrentConfig()
|
||||
},
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
loadProviders()
|
||||
loadCurrentConfig()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<!-- 标题 -->
|
||||
<h3 class="mb-3 flex items-center gap-2 text-sm font-semibold text-gray-900 dark:text-white">
|
||||
<UIcon name="i-heroicons-sparkles" class="h-4 w-4 text-violet-500" />
|
||||
AI 服务配置
|
||||
</h3>
|
||||
|
||||
<!-- 加载中 -->
|
||||
<div v-if="isLoading" class="flex items-center justify-center py-8">
|
||||
<UIcon name="i-heroicons-arrow-path" class="h-6 w-6 animate-spin text-gray-400" />
|
||||
</div>
|
||||
|
||||
<!-- 配置表单 -->
|
||||
<div
|
||||
v-else
|
||||
class="space-y-4 rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800/50"
|
||||
>
|
||||
<!-- 已有配置提示 -->
|
||||
<div v-if="hasExistingConfig" class="rounded-lg bg-green-50 p-3 dark:bg-green-900/20">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<UIcon name="i-heroicons-check-circle" class="h-5 w-5 text-green-500" />
|
||||
<span class="text-sm text-green-700 dark:text-green-400">已配置 API Key: {{ existingConfigDisplay }}</span>
|
||||
</div>
|
||||
<UButton size="xs" color="error" variant="ghost" @click="deleteConfig">删除</UButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 服务商选择 -->
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">AI 服务商</label>
|
||||
<USelect
|
||||
v-model="selectedProvider"
|
||||
:items="providers.map((p) => ({ label: p.name, value: p.id }))"
|
||||
placeholder="选择服务商"
|
||||
/>
|
||||
<p v-if="currentProvider" class="mt-1 text-xs text-gray-500">
|
||||
{{ currentProvider.description }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- API Key -->
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">API Key</label>
|
||||
<div class="flex gap-2">
|
||||
<UInput
|
||||
v-model="apiKey"
|
||||
type="password"
|
||||
:placeholder="hasExistingConfig ? '输入新的 API Key(留空保持原有)' : '输入你的 API Key'"
|
||||
class="flex-1"
|
||||
/>
|
||||
<UButton :loading="isValidating" :disabled="!apiKey" color="gray" variant="soft" @click="validateKey">
|
||||
验证
|
||||
</UButton>
|
||||
</div>
|
||||
<!-- 验证结果 -->
|
||||
<div v-if="validationMessage" class="mt-2">
|
||||
<div
|
||||
v-if="validationResult === 'valid'"
|
||||
class="flex items-center gap-1 text-sm text-green-600 dark:text-green-400"
|
||||
>
|
||||
<UIcon name="i-heroicons-check-circle" class="h-4 w-4" />
|
||||
{{ validationMessage }}
|
||||
</div>
|
||||
<div
|
||||
v-else-if="validationResult === 'invalid'"
|
||||
class="flex items-center gap-1 text-sm text-red-600 dark:text-red-400"
|
||||
>
|
||||
<UIcon name="i-heroicons-x-circle" class="h-4 w-4" />
|
||||
{{ validationMessage }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 模型选择 -->
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">模型</label>
|
||||
<USelect v-model="selectedModel" :items="modelOptions" placeholder="选择模型" />
|
||||
</div>
|
||||
|
||||
<!-- 发送条数限制 -->
|
||||
<!-- 注意:用户在 AIChatConfigTab 中也添加了类似的配置,这里可能需要移除或保留作为特定模型的配置 -->
|
||||
<!-- 暂时保留,但注意可能冲突。不过 AIConfigTab 是针对 LLM Provider 的配置,AIChatConfigTab 是针对 Chat 行为的配置 -->
|
||||
<!-- 用户在 AIConfigTab 中有 maxTokens (发送条数限制?),在 AIChatConfigTab 中有 maxMessagesPerRequest -->
|
||||
<!-- 这里的 maxTokens 实际上是 maxTokens 还是 maxMessages? -->
|
||||
<!-- 原代码: const maxTokens = ref(200) -->
|
||||
<!-- 原注释: 每次发送给 AI 的最大消息条数(10-5000,默认 200) -->
|
||||
<!-- 这看起来像是 maxMessages。 -->
|
||||
<!-- 如果 AIChatConfigTab 也有这个配置,那么这里应该移除,或者同步。 -->
|
||||
<!-- 既然用户添加了 AIChatConfigTab 专门管理这个,我应该从这里移除它,以免混淆。 -->
|
||||
<!-- 但是为了保持兼容性,我先保留它,或者根据用户意图调整。 -->
|
||||
<!-- 考虑到用户删除了这个文件,可能是因为他们想重构。 -->
|
||||
<!-- 我将恢复它,但移除 maxTokens 部分,因为它现在在 AIChatConfigTab 中管理。 -->
|
||||
|
||||
<!-- 获取 API Key 链接 -->
|
||||
<div class="rounded-lg bg-white p-3 dark:bg-gray-900">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">还没有 API Key?前往获取:</p>
|
||||
<div class="mt-2 flex gap-2">
|
||||
<a
|
||||
href="https://platform.deepseek.com/"
|
||||
target="_blank"
|
||||
class="text-sm text-violet-600 hover:underline dark:text-violet-400"
|
||||
>
|
||||
DeepSeek →
|
||||
</a>
|
||||
<a
|
||||
href="https://dashscope.console.aliyun.com/"
|
||||
target="_blank"
|
||||
class="text-sm text-violet-600 hover:underline dark:text-violet-400"
|
||||
>
|
||||
通义千问 →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 保存按钮 -->
|
||||
<div class="flex justify-end pt-2">
|
||||
<UButton color="primary" :disabled="!canSave" :loading="isSaving" @click="saveConfig">保存配置</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -446,7 +446,7 @@ const file2Name = computed(() => files.value[1]?.name || '文件 2')
|
||||
<label class="mb-1 block text-xs text-gray-500 dark:text-gray-400">输出目录(可选)</label>
|
||||
<div class="flex gap-2">
|
||||
<UInput v-model="outputDir" placeholder="默认保存到文档/ChatLab/merged/" class="flex-1" readonly />
|
||||
<UButton icon="i-heroicons-folder" color="gray" variant="soft" @click="selectOutputDir">选择</UButton>
|
||||
<UButton icon="i-heroicons-folder" variant="soft" @click="selectOutputDir">选择</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -501,13 +501,9 @@ const file2Name = computed(() => files.value[1]?.name || '文件 2')
|
||||
class="flex flex-wrap items-center gap-2 border-b border-gray-200 bg-gray-50 px-5 py-3 dark:border-gray-800 dark:bg-gray-800/50"
|
||||
>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">一键选择:</span>
|
||||
<UButton size="xs" color="gray" variant="soft" @click="batchSelectAll('keep1')">
|
||||
全部保留「{{ file1Name }}」
|
||||
</UButton>
|
||||
<UButton size="xs" color="gray" variant="soft" @click="batchSelectAll('keep2')">
|
||||
全部保留「{{ file2Name }}」
|
||||
</UButton>
|
||||
<UButton size="xs" color="gray" variant="soft" @click="batchSelectAll('keepBoth')">全部保留两者</UButton>
|
||||
<UButton size="xs" variant="soft" @click="batchSelectAll('keep1')">全部保留「{{ file1Name }}」</UButton>
|
||||
<UButton size="xs" variant="soft" @click="batchSelectAll('keep2')">全部保留「{{ file2Name }}」</UButton>
|
||||
<UButton size="xs" variant="soft" @click="batchSelectAll('keepBoth')">全部保留两者</UButton>
|
||||
</div>
|
||||
|
||||
<!-- 冲突列表 -->
|
||||
@@ -629,7 +625,7 @@ const file2Name = computed(() => files.value[1]?.name || '文件 2')
|
||||
|
||||
<!-- 底部操作 -->
|
||||
<div class="flex items-center justify-between border-t border-gray-200 px-5 py-4 dark:border-gray-800">
|
||||
<UButton color="gray" variant="ghost" @click="currentStep = 'select'">
|
||||
<UButton variant="ghost" @click="currentStep = 'select'">
|
||||
<UIcon name="i-heroicons-arrow-left" class="mr-1 h-4 w-4" />
|
||||
返回
|
||||
</UButton>
|
||||
@@ -660,7 +656,7 @@ const file2Name = computed(() => files.value[1]?.name || '文件 2')
|
||||
<UIcon name="i-heroicons-folder-open" class="mr-1 h-4 w-4" />
|
||||
打开文件夹
|
||||
</UButton>
|
||||
<UButton color="gray" @click="reset">继续合并</UButton>
|
||||
<UButton @click="reset">继续合并</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -679,7 +675,7 @@ const file2Name = computed(() => files.value[1]?.name || '文件 2')
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6 flex justify-end">
|
||||
<UButton color="gray" @click="showErrorModal = false">我知道了</UButton>
|
||||
<UButton @click="showErrorModal = false">我知道了</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -258,14 +258,6 @@ onMounted(() => {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Year Filter & Actions -->
|
||||
<div class="flex items-center gap-3">
|
||||
<!-- Actions -->
|
||||
<UButton icon="i-heroicons-arrow-down-tray" color="gray" variant="ghost" size="sm" disabled>
|
||||
生成报告
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
|
||||
+2
-2
@@ -134,7 +134,7 @@ function getProgressDetail(): string {
|
||||
</div>
|
||||
|
||||
<!-- Content Container -->
|
||||
<div class="relative h-full w-full overflow-y-auto z-10">
|
||||
<div class="relative h-full w-full overflow-y-auto">
|
||||
<div class="flex min-h-full w-full flex-col items-center justify-center px-4 py-12">
|
||||
<!-- Hero Section -->
|
||||
<div class="xl:mb-16 mb-8 text-center">
|
||||
@@ -157,7 +157,7 @@ function getProgressDetail(): string {
|
||||
:key="feature.title"
|
||||
class="group relative overflow-hidden rounded-3xl border border-transparent p-4 transition-all duration-500"
|
||||
>
|
||||
<div class="relative z-10">
|
||||
<div class="relative">
|
||||
<div class="mb-3 flex items-center">
|
||||
<div
|
||||
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl transition-all duration-500 group-hover:scale-110 group-hover:rotate-3"
|
||||
|
||||
Reference in New Issue
Block a user