mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-05-25 16:10:18 +08:00
feat: SQL实验室支持总结
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
@import 'tailwindcss';
|
||||
@import '@nuxt/ui';
|
||||
@import './markdown.css';
|
||||
|
||||
/* 自定义主题颜色 - 使用 @theme static 定义色板 */
|
||||
@theme static {
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
/**
|
||||
* Markdown 渲染通用样式
|
||||
* 用于 AI 消息、总结等需要渲染 Markdown 的地方
|
||||
* 使用方式:在组件的 <style> 中 @import 此文件,或在全局样式中引入
|
||||
*/
|
||||
|
||||
/* 段落 */
|
||||
.prose :deep(p) {
|
||||
margin: 0.5em 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.prose :deep(p:first-child) {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.prose :deep(p:last-child) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* 标题 */
|
||||
.prose :deep(h1) {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
margin: 1em 0 0.5em;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.prose :deep(h2) {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
margin: 0.875em 0 0.5em;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.prose :deep(h3) {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin: 0.75em 0 0.375em;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.prose :deep(h1:first-child),
|
||||
.prose :deep(h2:first-child),
|
||||
.prose :deep(h3:first-child) {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* 列表 */
|
||||
.prose :deep(ul),
|
||||
.prose :deep(ol) {
|
||||
margin: 0.5em 0;
|
||||
padding-left: 1.5em;
|
||||
}
|
||||
|
||||
.prose :deep(ul) {
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
.prose :deep(ol) {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
|
||||
.prose :deep(li) {
|
||||
margin: 0.25em 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.prose :deep(li > p) {
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
|
||||
/* 嵌套列表 */
|
||||
.prose :deep(ul ul),
|
||||
.prose :deep(ol ol),
|
||||
.prose :deep(ul ol),
|
||||
.prose :deep(ol ul) {
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
|
||||
/* 代码 */
|
||||
.prose :deep(code) {
|
||||
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 0.875em;
|
||||
padding: 0.15em 0.4em;
|
||||
border-radius: 0.25rem;
|
||||
background-color: rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.dark .prose :deep(code) {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* 代码块 */
|
||||
.prose :deep(pre) {
|
||||
margin: 0.75em 0;
|
||||
padding: 0.875em 1em;
|
||||
border-radius: 0.5rem;
|
||||
background-color: #1e293b;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.prose :deep(pre code) {
|
||||
padding: 0;
|
||||
background: none;
|
||||
color: #e2e8f0;
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 引用块 - 使用项目主题粉色 */
|
||||
.prose :deep(blockquote) {
|
||||
margin: 0.75em 0;
|
||||
padding: 0.5em 0 0.5em 1em;
|
||||
border-left: 3px solid var(--color-pink-500, #ee4567);
|
||||
background-color: color-mix(in srgb, var(--color-pink-500, #ee4567) 5%, transparent);
|
||||
border-radius: 0 0.25rem 0.25rem 0;
|
||||
}
|
||||
|
||||
.prose :deep(blockquote p) {
|
||||
margin: 0;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.dark .prose :deep(blockquote p) {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
/* 链接 - 使用项目主题粉色 */
|
||||
.prose :deep(a) {
|
||||
color: var(--color-pink-500, #ee4567);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
.prose :deep(a:hover) {
|
||||
color: var(--color-pink-600, #de335e);
|
||||
}
|
||||
|
||||
/* 分割线 */
|
||||
.prose :deep(hr) {
|
||||
margin: 1em 0;
|
||||
border: none;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.dark .prose :deep(hr) {
|
||||
border-top-color: #374151;
|
||||
}
|
||||
|
||||
/* 加粗和斜体 */
|
||||
.prose :deep(strong) {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.prose :deep(em) {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* 表格 */
|
||||
.prose :deep(table) {
|
||||
width: 100%;
|
||||
margin: 0.75em 0;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
.prose :deep(th),
|
||||
.prose :deep(td) {
|
||||
padding: 0.5em 0.75em;
|
||||
border: 1px solid #e5e7eb;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.dark .prose :deep(th),
|
||||
.dark .prose :deep(td) {
|
||||
border-color: #374151;
|
||||
}
|
||||
|
||||
.prose :deep(th) {
|
||||
background-color: #f9fafb;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dark .prose :deep(th) {
|
||||
background-color: #1f2937;
|
||||
}
|
||||
|
||||
/* 用户消息中的样式调整(白色文字背景) */
|
||||
.prose-invert :deep(code) {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.prose-invert :deep(a) {
|
||||
color: var(--color-pink-300, #faa7b4);
|
||||
}
|
||||
|
||||
.prose-invert :deep(a:hover) {
|
||||
color: var(--color-pink-200, #fccfd5);
|
||||
}
|
||||
|
||||
.prose-invert :deep(blockquote) {
|
||||
border-left-color: rgba(255, 255, 255, 0.5);
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.prose-invert :deep(blockquote p) {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ const sql = ref('SELECT * FROM message LIMIT 10')
|
||||
const isExecuting = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const result = ref<SQLResult | null>(null)
|
||||
const lastPrompt = ref('') // 记录最后使用的 AI 提示词
|
||||
|
||||
// 弹窗状态
|
||||
const showAIModal = ref(false)
|
||||
@@ -107,6 +108,7 @@ function handleInsertColumn(tableName: string, columnName: string) {
|
||||
// 处理 AI 生成成功
|
||||
function handleAIGenerated(generatedSql: string, explanation: string, prompt: string) {
|
||||
addToHistory(prompt, generatedSql, explanation)
|
||||
lastPrompt.value = prompt // 记录提示词
|
||||
}
|
||||
|
||||
// 处理使用 SQL
|
||||
@@ -123,6 +125,7 @@ async function handleRunSQL(generatedSql: string) {
|
||||
// 从历史记录执行
|
||||
async function executeFromHistory(record: AIHistory) {
|
||||
sql.value = record.sql
|
||||
lastPrompt.value = record.prompt // 记录历史的提示词
|
||||
showHistoryModal.value = false
|
||||
await executeSQL()
|
||||
}
|
||||
@@ -187,7 +190,7 @@ onMounted(() => {
|
||||
</div>
|
||||
|
||||
<!-- 结果区域 -->
|
||||
<ResultTable ref="resultTableRef" :result="result" :error="error" />
|
||||
<ResultTable ref="resultTableRef" :result="result" :error="error" :sql="sql" :prompt="lastPrompt" />
|
||||
</div>
|
||||
|
||||
<!-- AI 生成弹窗 -->
|
||||
|
||||
@@ -244,10 +244,7 @@ function formatToolParams(tool: ContentBlock extends { type: 'tool'; tool: infer
|
||||
<span class="font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ block.tool.displayName }}
|
||||
</span>
|
||||
<span
|
||||
v-if="formatToolParams(block.tool)"
|
||||
class="ml-2 text-xs text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
<span v-if="formatToolParams(block.tool)" class="ml-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ formatToolParams(block.tool) }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -272,208 +269,4 @@ function formatToolParams(tool: ContentBlock extends { type: 'tool'; tool: infer
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Markdown 渲染样式 */
|
||||
.prose :deep(p) {
|
||||
margin: 0.5em 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.prose :deep(p:first-child) {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.prose :deep(p:last-child) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* 标题 */
|
||||
.prose :deep(h1) {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
margin: 1em 0 0.5em;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.prose :deep(h2) {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
margin: 0.875em 0 0.5em;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.prose :deep(h3) {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin: 0.75em 0 0.375em;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.prose :deep(h1:first-child),
|
||||
.prose :deep(h2:first-child),
|
||||
.prose :deep(h3:first-child) {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* 列表 */
|
||||
.prose :deep(ul),
|
||||
.prose :deep(ol) {
|
||||
margin: 0.5em 0;
|
||||
padding-left: 1.5em;
|
||||
}
|
||||
|
||||
.prose :deep(ul) {
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
.prose :deep(ol) {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
|
||||
.prose :deep(li) {
|
||||
margin: 0.25em 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.prose :deep(li > p) {
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
|
||||
/* 嵌套列表 */
|
||||
.prose :deep(ul ul),
|
||||
.prose :deep(ol ol),
|
||||
.prose :deep(ul ol),
|
||||
.prose :deep(ol ul) {
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
|
||||
/* 代码 */
|
||||
.prose :deep(code) {
|
||||
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 0.875em;
|
||||
padding: 0.15em 0.4em;
|
||||
border-radius: 0.25rem;
|
||||
background-color: rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.dark .prose :deep(code) {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* 代码块 */
|
||||
.prose :deep(pre) {
|
||||
margin: 0.75em 0;
|
||||
padding: 0.875em 1em;
|
||||
border-radius: 0.5rem;
|
||||
background-color: #1e293b;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.prose :deep(pre code) {
|
||||
padding: 0;
|
||||
background: none;
|
||||
color: #e2e8f0;
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 引用块 */
|
||||
.prose :deep(blockquote) {
|
||||
margin: 0.75em 0;
|
||||
padding: 0.5em 0 0.5em 1em;
|
||||
border-left: 3px solid #8b5cf6;
|
||||
background-color: rgba(139, 92, 246, 0.05);
|
||||
border-radius: 0 0.25rem 0.25rem 0;
|
||||
}
|
||||
|
||||
.prose :deep(blockquote p) {
|
||||
margin: 0;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.dark .prose :deep(blockquote p) {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
/* 链接 */
|
||||
.prose :deep(a) {
|
||||
color: #8b5cf6;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
.prose :deep(a:hover) {
|
||||
color: #7c3aed;
|
||||
}
|
||||
|
||||
/* 分割线 */
|
||||
.prose :deep(hr) {
|
||||
margin: 1em 0;
|
||||
border: none;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.dark .prose :deep(hr) {
|
||||
border-top-color: #374151;
|
||||
}
|
||||
|
||||
/* 加粗和斜体 */
|
||||
.prose :deep(strong) {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.prose :deep(em) {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* 表格 */
|
||||
.prose :deep(table) {
|
||||
width: 100%;
|
||||
margin: 0.75em 0;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
.prose :deep(th),
|
||||
.prose :deep(td) {
|
||||
padding: 0.5em 0.75em;
|
||||
border: 1px solid #e5e7eb;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.dark .prose :deep(th),
|
||||
.dark .prose :deep(td) {
|
||||
border-color: #374151;
|
||||
}
|
||||
|
||||
.prose :deep(th) {
|
||||
background-color: #f9fafb;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dark .prose :deep(th) {
|
||||
background-color: #1f2937;
|
||||
}
|
||||
|
||||
/* 用户消息中的样式调整 */
|
||||
.prose-invert :deep(code) {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.prose-invert :deep(a) {
|
||||
color: #c4b5fd;
|
||||
}
|
||||
|
||||
.prose-invert :deep(a:hover) {
|
||||
color: #ddd6fe;
|
||||
}
|
||||
|
||||
.prose-invert :deep(blockquote) {
|
||||
border-left-color: rgba(255, 255, 255, 0.5);
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.prose-invert :deep(blockquote p) {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
</style>
|
||||
<!-- Markdown 样式已提取到全局 src/assets/styles/markdown.css -->
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import type { SQLResult } from './types'
|
||||
import { COLUMN_LABELS } from './types'
|
||||
|
||||
// 创建 markdown-it 实例
|
||||
const md = new MarkdownIt({
|
||||
html: false,
|
||||
breaks: true,
|
||||
linkify: true,
|
||||
typographer: true,
|
||||
})
|
||||
|
||||
// Props
|
||||
const props = defineProps<{
|
||||
result: SQLResult | null
|
||||
error: string | null
|
||||
sql?: string // 当前 SQL 语句
|
||||
prompt?: string // 用户提示词(AI 生成时)
|
||||
}>()
|
||||
|
||||
// Emits
|
||||
@@ -17,6 +29,28 @@ const emit = defineEmits<{
|
||||
const sortColumn = ref<string | null>(null)
|
||||
const sortDirection = ref<'asc' | 'desc'>('asc')
|
||||
|
||||
// AI 总结相关状态
|
||||
const showSummaryModal = ref(false)
|
||||
const isSummarizing = ref(false)
|
||||
const summaryContent = ref('')
|
||||
const summaryError = ref<string | null>(null)
|
||||
const streamingContent = ref('')
|
||||
|
||||
// 获取列的中文标签(尝试匹配所有表的列)
|
||||
function getColumnLabel(columnName: string): string | null {
|
||||
// 处理带表名前缀的情况,如 "message.sender_id" 或 "m.sender_id"
|
||||
const parts = columnName.split('.')
|
||||
const colName = parts.length > 1 ? parts[parts.length - 1] : columnName
|
||||
|
||||
// 遍历所有表查找匹配
|
||||
for (const tableColumns of Object.values(COLUMN_LABELS)) {
|
||||
if (tableColumns[colName]) {
|
||||
return tableColumns[colName]
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// 排序后的行数据
|
||||
const sortedRows = computed(() => {
|
||||
if (!props.result || !sortColumn.value) {
|
||||
@@ -84,6 +118,108 @@ function resetSort() {
|
||||
sortDirection.value = 'asc'
|
||||
}
|
||||
|
||||
// ==================== AI 总结功能 ====================
|
||||
|
||||
// 构建总结结果的数据(取前 50 行避免 token 过多)
|
||||
function buildResultSummary(): string {
|
||||
if (!props.result) return ''
|
||||
|
||||
const maxRows = 50
|
||||
const rows = props.result.rows.slice(0, maxRows)
|
||||
|
||||
// 构建表格形式的结果
|
||||
const header = props.result.columns.join(' | ')
|
||||
const separator = props.result.columns.map(() => '---').join(' | ')
|
||||
const dataRows = rows.map((row) =>
|
||||
row.map((cell) => (cell === null ? 'NULL' : String(cell).slice(0, 50))).join(' | ')
|
||||
)
|
||||
|
||||
let resultText = `| ${header} |\n| ${separator} |\n`
|
||||
resultText += dataRows.map((r) => `| ${r} |`).join('\n')
|
||||
|
||||
if (props.result.rows.length > maxRows) {
|
||||
resultText += `\n\n(仅展示前 ${maxRows} 行,共 ${props.result.rowCount} 行)`
|
||||
}
|
||||
|
||||
return resultText
|
||||
}
|
||||
|
||||
// 打开总结弹窗并开始总结
|
||||
async function openSummaryModal() {
|
||||
showSummaryModal.value = true
|
||||
summaryContent.value = ''
|
||||
summaryError.value = null
|
||||
streamingContent.value = ''
|
||||
await generateSummary()
|
||||
}
|
||||
|
||||
// AI 总结
|
||||
async function generateSummary() {
|
||||
const hasConfig = await window.llmApi.hasConfig()
|
||||
if (!hasConfig) {
|
||||
summaryError.value = '请先在设置中配置 AI 服务'
|
||||
return
|
||||
}
|
||||
|
||||
isSummarizing.value = true
|
||||
summaryError.value = null
|
||||
streamingContent.value = ''
|
||||
|
||||
try {
|
||||
const resultSummary = buildResultSummary()
|
||||
|
||||
let contextInfo = ''
|
||||
if (props.prompt) {
|
||||
contextInfo = `用户的查询意图:${props.prompt}\n\n`
|
||||
}
|
||||
if (props.sql) {
|
||||
contextInfo += `执行的 SQL 语句:\n\`\`\`sql\n${props.sql}\n\`\`\`\n\n`
|
||||
}
|
||||
|
||||
const prompt = `请分析以下 SQL 查询结果,用简洁的中文总结关键发现和洞察。
|
||||
|
||||
${contextInfo}查询结果(共 ${props.result?.rowCount || 0} 行):
|
||||
|
||||
${resultSummary}
|
||||
|
||||
请提供:
|
||||
1. 结果概述(一句话总结)
|
||||
2. 关键发现(2-4 个要点)
|
||||
3. 如有明显的趋势或异常,请指出`
|
||||
|
||||
const result = await window.llmApi.chatStream(
|
||||
[
|
||||
{ role: 'system', content: '你是一个数据分析专家,擅长从查询结果中提取关键信息和洞察。请用简洁清晰的中文回答。' },
|
||||
{ role: 'user', content: prompt },
|
||||
],
|
||||
{ temperature: 0.3, maxTokens: 1000 },
|
||||
(chunk) => {
|
||||
if (chunk.content) {
|
||||
streamingContent.value += chunk.content
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (result.success) {
|
||||
summaryContent.value = streamingContent.value
|
||||
} else {
|
||||
summaryError.value = result.error || '总结失败'
|
||||
}
|
||||
} catch (err: any) {
|
||||
summaryError.value = err.message || String(err)
|
||||
} finally {
|
||||
isSummarizing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭总结弹窗
|
||||
function closeSummaryModal() {
|
||||
showSummaryModal.value = false
|
||||
summaryContent.value = ''
|
||||
summaryError.value = null
|
||||
streamingContent.value = ''
|
||||
}
|
||||
|
||||
defineExpose({ resetSort })
|
||||
</script>
|
||||
|
||||
@@ -116,10 +252,16 @@ defineExpose({ resetSort })
|
||||
{{ result.duration }} ms
|
||||
</span>
|
||||
</div>
|
||||
<UButton variant="ghost" size="xs" @click="copyAsCSV">
|
||||
<UIcon name="i-heroicons-clipboard-document" class="mr-1 h-4 w-4" />
|
||||
复制 CSV
|
||||
</UButton>
|
||||
<div class="flex items-center gap-2">
|
||||
<UButton variant="ghost" size="xs" @click="openSummaryModal">
|
||||
<UIcon name="i-heroicons-sparkles" class="mr-1 h-4 w-4" />
|
||||
总结一下
|
||||
</UButton>
|
||||
<UButton variant="ghost" size="xs" @click="copyAsCSV">
|
||||
<UIcon name="i-heroicons-clipboard-document" class="mr-1 h-4 w-4" />
|
||||
复制 CSV
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 结果表格 -->
|
||||
@@ -130,15 +272,18 @@ defineExpose({ resetSort })
|
||||
<th
|
||||
v-for="(column, index) in result.columns"
|
||||
:key="index"
|
||||
class="cursor-pointer whitespace-nowrap px-4 py-2 text-left text-xs font-medium uppercase text-gray-500 transition-colors hover:bg-gray-200 dark:text-gray-400 dark:hover:bg-gray-700"
|
||||
class="cursor-pointer whitespace-nowrap px-4 py-2 text-left text-xs font-medium transition-colors hover:bg-gray-200 dark:hover:bg-gray-700"
|
||||
@click="handleSort(column)"
|
||||
>
|
||||
<div class="flex items-center gap-1">
|
||||
<span>{{ column }}</span>
|
||||
<div class="flex flex-col">
|
||||
<span class="text-gray-700 dark:text-gray-300">{{ getColumnLabel(column) || column }}</span>
|
||||
<span v-if="getColumnLabel(column)" class="font-mono text-[10px] text-gray-400">{{ column }}</span>
|
||||
</div>
|
||||
<UIcon
|
||||
v-if="sortColumn === column"
|
||||
:name="sortDirection === 'asc' ? 'i-heroicons-chevron-up' : 'i-heroicons-chevron-down'"
|
||||
class="h-3 w-3"
|
||||
class="h-3 w-3 text-gray-500"
|
||||
/>
|
||||
</div>
|
||||
</th>
|
||||
@@ -179,6 +324,51 @@ defineExpose({ resetSort })
|
||||
<p class="mt-1 text-xs text-gray-400">仅支持 SELECT 查询,结果最多返回 1000 行</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AI 总结弹窗 -->
|
||||
<UModal v-model:open="showSummaryModal">
|
||||
<template #content>
|
||||
<div class="max-h-[70vh] overflow-hidden p-6">
|
||||
<div class="mb-4 flex items-center gap-2">
|
||||
<UIcon name="i-heroicons-sparkles" class="h-5 w-5 text-pink-500" />
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">AI 结果总结</h3>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="isSummarizing && !streamingContent" class="flex items-center justify-center py-8">
|
||||
<UIcon name="i-heroicons-arrow-path" class="h-6 w-6 animate-spin text-pink-500" />
|
||||
<span class="ml-2 text-sm text-gray-500">AI 正在分析结果...</span>
|
||||
</div>
|
||||
|
||||
<!-- 流式输出 / 最终结果 -->
|
||||
<div v-else-if="streamingContent || summaryContent" class="max-h-[50vh] overflow-y-auto">
|
||||
<div
|
||||
class="prose prose-sm max-w-none rounded-lg bg-gray-50 p-4 dark:prose-invert dark:bg-gray-900"
|
||||
v-html="md.render(streamingContent || summaryContent)"
|
||||
/>
|
||||
<div v-if="isSummarizing" class="mt-2 flex items-center text-xs text-gray-400">
|
||||
<UIcon name="i-heroicons-arrow-path" class="mr-1 h-3 w-3 animate-spin" />
|
||||
生成中...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 错误提示 -->
|
||||
<div v-if="summaryError" class="rounded-lg bg-red-50 p-4 dark:bg-red-950">
|
||||
<p class="text-sm text-red-600 dark:text-red-400">{{ summaryError }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<div class="mt-4 flex justify-end gap-2">
|
||||
<UButton v-if="!isSummarizing && summaryContent" variant="outline" @click="generateSummary">
|
||||
<UIcon name="i-heroicons-arrow-path" class="mr-1 h-4 w-4" />
|
||||
重新生成
|
||||
</UButton>
|
||||
<UButton variant="ghost" @click="closeSummaryModal">关闭</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</UModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Markdown 样式已提取到全局 src/assets/styles/markdown.css -->
|
||||
|
||||
Reference in New Issue
Block a user