Browse Source

优化前端配色

TDesign-Vue-Next-1.20.6
wanghanlin 1 week ago
parent
commit
45d34e76d1
  1. 1
      frontend/src/main.ts
  2. 17
      frontend/src/styles/shared.css
  3. 75
      frontend/src/styles/tokens.css
  4. 57
      frontend/src/utils/palette.ts
  5. 4
      frontend/src/views/ApiKeyManager.vue
  6. 4
      frontend/src/views/AuditLogManager.vue
  7. 10
      frontend/src/views/CategoryManager.vue
  8. 6
      frontend/src/views/ChatPanel.vue
  9. 2
      frontend/src/views/ConversationManager.vue
  10. 9
      frontend/src/views/DashboardPanel.vue
  11. 34
      frontend/src/views/DocDetail.vue
  12. 2
      frontend/src/views/DocSearch.vue
  13. 6
      frontend/src/views/DocUpload.vue
  14. 6
      frontend/src/views/FeedbackOps.vue
  15. 10
      frontend/src/views/LogViewer.vue
  16. 10
      frontend/src/views/LoginPage.vue
  17. 4
      frontend/src/views/McpServerManager.vue
  18. 94
      frontend/src/views/ModelConfigManager.vue
  19. 63
      frontend/src/views/PipelineFlow.vue
  20. 5
      frontend/src/views/SensitiveWordManager.vue

1
frontend/src/main.ts

@ -6,6 +6,7 @@ import { createApp } from 'vue'
import { createPinia } from 'pinia'
import TDesign from 'tdesign-vue-next'
import 'tdesign-vue-next/es/style/index.css'
import './styles/tokens.css'
import './styles/shared.css'
import App from './App.vue'
import router from './router'

17
frontend/src/styles/shared.css

@ -1,6 +1,9 @@
/**
* 全局共享 CSS 工具类
* 替代各页面中重复的行内样式和 scoped 样式
*
* 颜色统一引用 `tokens.css` 定义的语义 token--color-*
* 不再散落硬编码 hex保证全站配色一致
*/
/* ====== 工具栏 ====== */
@ -23,27 +26,27 @@
/* ====== 统计提示文字 ====== */
.total-hint {
font-size: 13px;
color: #666;
color: var(--color-text-secondary);
}
/* ====== 卡片容器(圆角边框) ====== */
.card-box {
padding: 12px;
background: #fafafa;
border: 1px solid #e7e7e7;
border-radius: 8px;
background: var(--color-bg-subtle);
border: 1px solid var(--color-border);
border-radius: var(--radius-card);
}
/* ====== 描述性文字 ====== */
.desc-text {
font-size: 13px;
color: #999;
color: var(--color-text-tertiary);
}
/* ====== 代码标签/键名展示 ====== */
.code-tag {
font-size: 12px;
background: #f3f4f6;
background: var(--color-bg-code);
padding: 2px 6px;
border-radius: 4px;
border-radius: var(--radius-sm);
}

75
frontend/src/styles/tokens.css

@ -0,0 +1,75 @@
/**
* 全局设计 token
*
* 统一项目的颜色 / 圆角 / 间距 / 阴影来源
* - 语义色主色 / 成功 / 警告 / 错误与文本边框背景色全部对齐 TDesign 主题变量--td-*
* 保证全站与 TDesign 组件同一套主色与语义色
* - 图表 / 分类等多色区分的系列色不走语义色统一收敛到 `utils/palette.ts`
*
* 引入顺序必须在 `tdesign-vue-next/es/style/index.css` 之后`shared.css` 之前
*/
:root {
/* ===== 品牌主色(对齐 TDesign,默认蓝 #0052d9;换主色只需改此一处) ===== */
--color-primary: var(--td-brand-color);
--color-primary-hover: var(--td-brand-color-hover);
--color-primary-active: var(--td-brand-color-active);
--color-primary-light: var(--td-brand-color-light);
/* ===== 语义状态色 ===== */
--color-success: var(--td-success-color);
--color-warning: var(--td-warning-color);
--color-error: var(--td-error-color);
/* ===== 状态色阶(统计卡片/提示条四档:bg 浅 / border / text 主 / text 深) ===== */
--color-success-bg: var(--td-success-color-1);
--color-success-border: var(--td-success-color-3);
--color-success-text: var(--td-success-color-5);
--color-success-text-strong: var(--td-success-color-8);
--color-error-bg: var(--td-error-color-1);
--color-error-border: var(--td-error-color-3);
--color-error-text: var(--td-error-color-6);
--color-error-text-strong: var(--td-error-color-8);
--color-warning-bg: var(--td-warning-color-1);
--color-warning-border: var(--td-warning-color-3);
--color-warning-text: var(--td-warning-color-5);
--color-warning-text-strong: var(--td-warning-color-8);
--color-info-bg: var(--td-brand-color-1);
--color-info-border: var(--td-brand-color-3);
--color-info-text: var(--td-brand-color-7);
--color-info-text-strong: var(--td-brand-color-8);
/* ===== 文本色 ===== */
--color-text-primary: var(--td-text-color-primary);
--color-text-secondary: var(--td-text-color-secondary); /* 次要文字,原 #666 */
--color-text-tertiary: var(--td-text-color-placeholder); /* 弱化文字,原 #999 */
--color-text-disabled: var(--td-text-color-disabled);
--color-text-link: var(--td-text-color-link);
/* ===== 边框色 ===== */
--color-border: var(--td-border-level-1-color); /* 常规边框,原 #e7e7e7 */
--color-border-strong: var(--td-border-level-2-color);
/* ===== 背景色 ===== */
--color-bg-page: var(--td-bg-color-page);
--color-bg-subtle: var(--td-bg-color-secondarycontainer); /* 浅灰底,原 #fafafa */
--color-bg-code: var(--td-bg-color-component); /* 代码/标签底,原 #f3f4f6 */
/* ===== 代码块(深色主题,VS Code 风格,独立于浅色语义色) ===== */
--color-code-block-bg: #1e1e1e;
--color-code-block-text: #d4d4d4;
/* ===== 圆角 ===== */
--radius-sm: var(--td-radius-small); /* 4px */
--radius-md: var(--td-radius-medium); /* 6px */
--radius-lg: var(--td-radius-large); /* 9px */
--radius-card: 8px; /* 卡片容器圆角(shared.css .card-box 原值) */
/* ===== 阴影 ===== */
--shadow-sm: var(--td-shadow-1);
--shadow-md: var(--td-shadow-2);
--shadow-lg: var(--td-shadow-3);
}

57
frontend/src/utils/palette.ts

@ -0,0 +1,57 @@
/**
* /
*
* Chart.jsmermaid
* `styles/tokens.css` //
* - CSS `var(--color-success)`
* -
*
* hex rgb rgb `rgba(..., 0.1)`
*/
export const palette = {
// ===== 系列色(数据可视化 / 分类) =====
blue: '#3b82f6',
blueRgb: '59, 130, 246',
purple: '#8b5cf6',
purpleRgb: '139, 92, 246',
green: '#10b981',
greenRgb: '16, 185, 129',
red: '#ef4444',
redRgb: '239, 68, 68',
orange: '#f59e0b',
orangeRgb: '245, 158, 11',
indigo: '#6366f1',
indigoRgb: '99, 102, 241',
pink: '#e83e8c',
pinkRgb: '232, 62, 140',
gray: '#6c757d',
grayRgb: '108, 117, 125',
// ===== 中性灰阶(流程图文本 / 边框 / 线条) =====
neutralText: '#1f2937',
neutralLine: '#6b7280',
neutralBorder: '#d1d5db',
} as const
/** 系列色浅色底(图表填充 / 流程图节点背景) */
export const paletteBg = {
blue: '#dbeafe',
green: '#d1fae5',
orange: '#fef3c7',
red: '#fee2e2',
indigo: '#e0e7ff',
pink: '#fce7f3',
gray: '#f3f4f6',
} as const
/** 系列色浅色描边(流程图节点边框,浅一档用于区分背景) */
export const paletteBorder = {
blue: '#3b82f6',
green: '#10b981',
orange: '#f59e0b',
red: '#ef4444',
indigo: '#a5b4fc',
pink: '#f9a8d4',
gray: '#d1d5db',
} as const

4
frontend/src/views/ApiKeyManager.vue

@ -13,7 +13,7 @@
</template>
<template #roleIds="{row}">
<span v-if="getBoundRoleCount(row.roleIds)" style="color:var(--td-brand-color);">{{ getBoundRoleCount(row.roleIds) }} 个角色</span>
<span v-else style="color:#999;">不限</span>
<span v-else style="color:var(--color-text-tertiary);">不限</span>
</template>
<template #enabled="{row}"><span :style="{color:row.enabled?'var(--td-success-color)':'var(--td-error-color)'}">{{ row.enabled?'✅ 有效':'❌ 已吊销' }}</span></template>
<template #op="{row}">
@ -128,4 +128,4 @@ async function doUpdate(){try{const r=await updateApiKey(editingKeyId.value,edit
async function copyKey(k:string){try{await navigator.clipboard.writeText(k);toast('已复制','success')}catch{toast('复制失败','error')}}
</script>
<style scoped>.key-code{font-size:12px;background:#f3f4f6;padding:2px 6px;border-radius:4px;}.created-key-box{margin-top:12px;padding:12px;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:8px;}.created-key-box p{font-size:12px;color:#166534;margin-bottom:6px;}.created-key{font-size:13px;word-break:break-all;display:block;margin-bottom:6px;}</style>
<style scoped>.key-code{font-size:12px;background:var(--color-bg-code);padding:2px 6px;border-radius:4px;}.created-key-box{margin-top:12px;padding:12px;background:var(--color-success-bg);border:1px solid var(--color-success-border);border-radius:8px;}.created-key-box p{font-size:12px;color:var(--color-success-text-strong);margin-bottom:6px;}.created-key{font-size:13px;word-break:break-all;display:block;margin-bottom:6px;}</style>

4
frontend/src/views/AuditLogManager.vue

@ -3,9 +3,9 @@
<p class="desc-text">记录所有敏感词命中事件包含输入/输出方向违规内容命中词及处理方式</p>
<t-table :data="logs" :columns="columns" row-key="id" :loading="loading"
:pagination="{current:page,total:total,pageSize:pageSize,showJumper:true}" @page-change="onPageChange">
<template #direction="{row}"><span :style="{color:row.direction==='INPUT'?'#007bff':'#28a745'}">{{ row.direction==='INPUT'?'输入':'输出' }}</span></template>
<template #direction="{row}"><span :style="{color:row.direction==='INPUT'?'var(--color-primary)':'var(--color-success)'}">{{ row.direction==='INPUT'?'输入':'输出' }}</span></template>
<template #originalText="{row}"><span :title="row.originalText" class="ellipsis-td">{{ row.originalText }}</span></template>
<template #actionTaken="{row}"><span :style="{color:row.actionTaken==='BLOCK'?'#dc3545':row.actionTaken==='MASK'?'#ffc107':'#28a745'}">{{ row.actionTaken==='BLOCK'?'拦截':row.actionTaken==='MASK'?'脱敏':'通过' }}</span></template>
<template #actionTaken="{row}"><span :style="{color:row.actionTaken==='BLOCK'?'var(--color-error)':row.actionTaken==='MASK'?'var(--color-warning)':'var(--color-success)'}">{{ row.actionTaken==='BLOCK'?'拦截':row.actionTaken==='MASK'?'脱敏':'通过' }}</span></template>
<template #hitWords="{row}"><span class="hit-words">{{ formatHitWords(row.hitWords) }}</span></template>
</t-table>
</t-card>

10
frontend/src/views/CategoryManager.vue

@ -114,11 +114,11 @@ function countDescendants(node:any):number{if(!node.children?.length)return 0;le
</script>
<style scoped>
.create-title{font-size:13px;font-weight:600;margin-bottom:8px;}
.tree-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;}.tree-count{font-size:13px;color:#999;}
.tree-row{padding:10px 12px;border-bottom:1px solid #e7e7e7;display:flex;justify-content:space-between;align-items:center;}
.tree-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;}.tree-count{font-size:13px;color:var(--color-text-tertiary);}
.tree-row{padding:10px 12px;border-bottom:1px solid var(--color-border);display:flex;justify-content:space-between;align-items:center;}
.tree-left{display:flex;align-items:center;gap:6px;min-width:0;flex:1;}
.tree-arrow{cursor:pointer;user-select:none;font-size:12px;width:16px;text-align:center;color:#999;}
.tree-arrow{cursor:pointer;user-select:none;font-size:12px;width:16px;text-align:center;color:var(--color-text-tertiary);}
.tree-name{font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.tree-desc{color:#999;font-size:12px;margin-left:8px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.tree-doc-count{color:#999;font-size:11px;margin-left:8px;white-space:nowrap;}
.tree-desc{color:var(--color-text-tertiary);font-size:12px;margin-left:8px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.tree-doc-count{color:var(--color-text-tertiary);font-size:11px;margin-left:8px;white-space:nowrap;}
</style>

6
frontend/src/views/ChatPanel.vue

@ -693,7 +693,7 @@ onBeforeUnmount(() => {
}
.agent-avatar {
width: 40px; height: 40px; border-radius: 10px; background: var(--td-brand-color);
color: #fff; display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 14px;
color: var(--td-text-color-anti); display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 14px;
}
.agent-name { font-weight: 600; font-size: 14px; }
.agent-status { font-size: 12px; color: var(--td-success-color); display: flex; align-items: center; gap: 4px; }
@ -741,7 +741,7 @@ onBeforeUnmount(() => {
.msg { margin-bottom: 16px; }
.msg.user .msg-content { display: flex; flex-direction: column; align-items: flex-end; }
.msg.user .msg-bubble {
background: var(--td-brand-color); color: #fff; border-radius: 12px 12px 4px 12px;
background: var(--td-brand-color); color: var(--td-text-color-anti); border-radius: 12px 12px 4px 12px;
padding: 10px 14px; max-width: 70%; font-size: 14px; line-height: 1.6; white-space: pre-wrap;
}
.msg.assistant .msg-bubble {
@ -762,7 +762,7 @@ onBeforeUnmount(() => {
.msg-bubble :deep(th), .msg-bubble :deep(td) { border: 1px solid var(--td-border-level-2-color); padding: 6px 10px; text-align: left; font-size: 13px; }
.msg-bubble :deep(th) { background: var(--td-bg-color-secondarycontainer); font-weight: 600; }
.msg-bubble :deep(code) { background: rgba(0,0,0,.06); padding: 2px 6px; border-radius: 3px; font-size: 13px; }
.msg-bubble :deep(pre) { background: #1e1e1e; color: #d4d4d4; padding: 12px; border-radius: 8px; overflow-x: auto; }
.msg-bubble :deep(pre) { background: var(--color-code-block-bg); color: var(--color-code-block-text); padding: 12px; border-radius: 8px; overflow-x: auto; }
.msg-bubble :deep(pre code) { background: none; padding: 0; color: inherit; }
/* ==================== MCP 工具调用 ==================== */

2
frontend/src/views/ConversationManager.vue

@ -256,7 +256,7 @@ async function submitFaqAction() {
}
}
</script>
<style scoped>.msg-item{padding:12px;border-radius:8px;margin-bottom:12px;}.msg-user{background:#eff6ff;}.msg-assistant{background:#f9fafb;}.msg-system{background:#fef3c7;}.msg-meta{display:flex;align-items:center;gap:8px;margin-bottom:6px;}.msg-time{font-size:11px;color:#999;}.msg-body{font-size:13px;line-height:1.6;white-space:pre-wrap;word-break:break-word;}
<style scoped>.msg-item{padding:12px;border-radius:8px;margin-bottom:12px;}.msg-user{background:var(--color-info-bg);}.msg-assistant{background:var(--color-bg-subtle);}.msg-system{background:var(--color-warning-bg);}.msg-meta{display:flex;align-items:center;gap:8px;margin-bottom:6px;}.msg-time{font-size:11px;color:var(--color-text-tertiary);}.msg-body{font-size:13px;line-height:1.6;white-space:pre-wrap;word-break:break-word;}
.msg-faq-actions { margin-top: 8px; display: flex; gap: 6px; }
.dialog-footer { margin-top: 16px; display: flex; justify-content: flex-end; gap: 8px; }
</style>

9
frontend/src/views/DashboardPanel.vue

@ -60,6 +60,7 @@
<script setup lang="ts">
import { ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue'
import { getDashboardOverview, getDashboardTrend, getDashboardKnowledge, getDashboardCustom } from '@/api/dashboard'
import { palette } from '@/utils/palette'
const loading = ref(false)
const rangeDays = ref(7)
@ -128,8 +129,8 @@ async function renderCharts() {
if (conversationChartRef.value) {
conversationChart = new Chart(conversationChartRef.value, {
type: 'line', data: { labels, datasets: [
{ label: '对话数', data: trendData.value.map((s: any) => s.conversationCount ?? s.conversation_count ?? 0), borderColor: '#3b82f6', backgroundColor: 'rgba(59,130,246,0.1)', fill: true, tension: 0.3, pointRadius: 4 },
{ label: '消息数', data: trendData.value.map((s: any) => s.messageCount ?? s.message_count ?? 0), borderColor: '#8b5cf6', backgroundColor: 'rgba(139,92,246,0.1)', fill: true, tension: 0.3, pointRadius: 4 }] },
{ label: '对话数', data: trendData.value.map((s: any) => s.conversationCount ?? s.conversation_count ?? 0), borderColor: palette.blue, backgroundColor: `rgba(${palette.blueRgb},0.1)`, fill: true, tension: 0.3, pointRadius: 4 },
{ label: '消息数', data: trendData.value.map((s: any) => s.messageCount ?? s.message_count ?? 0), borderColor: palette.purple, backgroundColor: `rgba(${palette.purpleRgb},0.1)`, fill: true, tension: 0.3, pointRadius: 4 }] },
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom', labels: { boxWidth: 12, font: { size: 12 } } } }, scales: { y: { beginAtZero: true }, x: { grid: { display: false } } } },
})
}
@ -138,8 +139,8 @@ async function renderCharts() {
if (satisfactionChartRef.value) {
satisfactionChart = new Chart(satisfactionChartRef.value, {
type: 'line', data: { labels, datasets: [
{ label: '👍 有帮助', data: trendData.value.map((s: any) => s.thumbsUpCount ?? s.thumbs_up_count ?? 0), borderColor: '#10b981', backgroundColor: 'rgba(16,185,129,0.1)', fill: true, tension: 0.3, pointRadius: 4 },
{ label: '👎 没帮助', data: trendData.value.map((s: any) => s.thumbsDownCount ?? s.thumbs_down_count ?? 0), borderColor: '#ef4444', backgroundColor: 'rgba(239,68,68,0.1)', fill: true, tension: 0.3, pointRadius: 4 }] },
{ label: '👍 有帮助', data: trendData.value.map((s: any) => s.thumbsUpCount ?? s.thumbs_up_count ?? 0), borderColor: palette.green, backgroundColor: `rgba(${palette.greenRgb},0.1)`, fill: true, tension: 0.3, pointRadius: 4 },
{ label: '👎 没帮助', data: trendData.value.map((s: any) => s.thumbsDownCount ?? s.thumbs_down_count ?? 0), borderColor: palette.red, backgroundColor: `rgba(${palette.redRgb},0.1)`, fill: true, tension: 0.3, pointRadius: 4 }] },
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom', labels: { boxWidth: 12, font: { size: 12 } } } }, scales: { y: { beginAtZero: true }, x: { grid: { display: false } } } },
})
}

34
frontend/src/views/DocDetail.vue

@ -100,31 +100,31 @@ function getTags(): string[] {
<style scoped>
/* 基本信息 */
.info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 24px; }
.info-item { display: flex; align-items: center; padding: 6px 0; border-bottom: 1px solid #f0f0f0; }
.info-label { width: 70px; flex: none; font-size: 13px; color: #999; }
.info-val { font-size: 13px; color: #333; word-break: break-all; }
.info-item { display: flex; align-items: center; padding: 6px 0; border-bottom: 1px solid var(--color-border); }
.info-label { width: 70px; flex: none; font-size: 13px; color: var(--color-text-tertiary); }
.info-val { font-size: 13px; color: var(--color-text-primary); word-break: break-all; }
.info-tags { margin-top: 12px; display: flex; align-items: center; }
.info-error { margin-top: 8px; display: flex; align-items: flex-start; gap: 8px; }
.error-text { color: var(--td-error-color, #e34d59); font-size: 12px; }
.error-text { color: var(--color-error); font-size: 12px; }
/* 原文 */
.raw-content { background: #fafafa; border: 1px solid #e5e7eb; border-radius: 8px; overflow: hidden; }
.raw-header { display: flex; align-items: center; justify-content: space-between; padding: 10px 14px; background: #f3f4f6; border-bottom: 1px solid #e5e7eb; }
.raw-content { background: var(--color-bg-subtle); border: 1px solid var(--color-border); border-radius: 8px; overflow: hidden; }
.raw-header { display: flex; align-items: center; justify-content: space-between; padding: 10px 14px; background: var(--color-bg-code); border-bottom: 1px solid var(--color-border); }
.raw-title { font-weight: 600; font-size: 13px; }
.raw-body { padding: 14px; margin: 0; font-size: 13px; line-height: 1.8; white-space: pre-wrap; word-break: break-word; max-height: 500px; overflow-y: auto; color: #374151; }
.raw-body { padding: 14px; margin: 0; font-size: 13px; line-height: 1.8; white-space: pre-wrap; word-break: break-word; max-height: 500px; overflow-y: auto; color: var(--color-text-primary); }
/* 分块列表 */
.chunks-wrapper { max-height: 520px; overflow-y: auto; }
.chunks-summary { font-size: 12px; color: #999; margin-bottom: 10px; }
.chunk-card { background: #fafbfc; border: 1px solid #e5e7eb; border-radius: 8px; padding: 12px 14px; margin-bottom: 10px; transition: border-color .2s; }
.chunk-card:hover { border-color: var(--td-brand-color, #0052d9); }
.chunks-summary { font-size: 12px; color: var(--color-text-tertiary); margin-bottom: 10px; }
.chunk-card { background: var(--color-bg-subtle); border: 1px solid var(--color-border); border-radius: 8px; padding: 12px 14px; margin-bottom: 10px; transition: border-color .2s; }
.chunk-card:hover { border-color: var(--color-primary); }
.chunk-header { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.chunk-badge { display: inline-block; background: var(--td-brand-color, #0052d9); color: #fff; font-size: 12px; font-weight: 600; padding: 2px 10px; border-radius: 10px; }
.chunk-meta { font-size: 11px; color: #999; }
.chunk-body { font-size: 13px; line-height: 1.75; color: #374151; word-break: break-word; }
.chunk-footer { margin-top: 8px; padding-top: 6px; border-top: 1px dashed #e5e7eb; display: flex; justify-content: flex-end; }
.chunk-len { font-size: 11px; color: #bbb; }
.chunk-badge { display: inline-block; background: var(--color-primary); color: var(--td-text-color-anti); font-size: 12px; font-weight: 600; padding: 2px 10px; border-radius: 10px; }
.chunk-meta { font-size: 11px; color: var(--color-text-tertiary); }
.chunk-body { font-size: 13px; line-height: 1.75; color: var(--color-text-primary); word-break: break-word; }
.chunk-footer { margin-top: 8px; padding-top: 6px; border-top: 1px dashed var(--color-border); display: flex; justify-content: flex-end; }
.chunk-len { font-size: 11px; color: var(--color-text-disabled); }
.error-msg { color: var(--td-error-color, #e34d59); padding: 8px 0; }
.empty-detail { color: #999; padding: 40px; text-align: center; }
.error-msg { color: var(--color-error); padding: 8px 0; }
.empty-detail { color: var(--color-text-tertiary); padding: 40px; text-align: center; }
</style>

2
frontend/src/views/DocSearch.vue

@ -9,7 +9,7 @@
<t-card title="搜索结果" :bordered="true" v-for="(chunk, idx) in results" :key="idx" style="margin-bottom:8px;">
<t-tag size="small" theme="primary" variant="light">#{{ idx+1 }} · {{ chunk.metadata?.sourceName||chunk.documentId }}</t-tag>
<span style="font-size:11px;color:#999;margin-left:8px;">相似度: {{ chunk.similarity?.toFixed(4)||'-' }}</span>
<span style="font-size:11px;color:var(--color-text-tertiary);margin-left:8px;">相似度: {{ chunk.similarity?.toFixed(4)||'-' }}</span>
<div class="chunk-content" v-html="renderMarkdown(chunk.content?.substring(0,500)||'')" />
</t-card>
<t-empty v-if="results.length===0&&searched" description="无结果" />

6
frontend/src/views/DocUpload.vue

@ -1,6 +1,6 @@
<template>
<t-card title="文档上传" :bordered="false">
<p style="color:#999;font-size:13px;margin-bottom:16px;">上传文档到 RAG 知识库自动分词 向量化 存入 PGVector</p>
<p style="color:var(--color-text-tertiary);font-size:13px;margin-bottom:16px;">上传文档到 RAG 知识库自动分词 向量化 存入 PGVector</p>
<!-- 上传元信息 -->
<div class="upload-meta">
@ -142,6 +142,6 @@ async function uploadText() {
</script>
<style scoped>
.upload-meta { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; padding: 12px; background: #fafafa; border-radius: 8px; border: 1px solid #e7e7e7; margin-bottom: 16px; }
.tag-input-wrapper { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; flex: 1; padding: 4px 8px; border: 1px solid var(--td-border-level-1-color, #dcdcdc); border-radius: 4px; background: #fff; }
.upload-meta { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; padding: 12px; background: var(--color-bg-subtle); border-radius: 8px; border: 1px solid var(--color-border); margin-bottom: 16px; }
.tag-input-wrapper { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; flex: 1; padding: 4px 8px; border: 1px solid var(--color-border); border-radius: 4px; background: var(--td-bg-color-container); }
</style>

6
frontend/src/views/FeedbackOps.vue

@ -305,9 +305,9 @@ async function submitFaqAction() {
</script>
<style scoped>
.msg-item { padding: 12px; border-radius: 8px; margin-bottom: 12px; }
.msg-user { background: #eff6ff; }
.msg-assistant { background: #f9fafb; }
.msg-system { background: #fef3c7; }
.msg-user { background: var(--color-info-bg); }
.msg-assistant { background: var(--color-bg-subtle); }
.msg-system { background: var(--color-warning-bg); }
.msg-meta { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
.msg-body { font-size: 13px; line-height: 1.6; white-space: pre-wrap; word-break: break-word; }
.dialog-footer { margin-top: 16px; display: flex; justify-content: flex-end; gap: 8px; }

10
frontend/src/views/LogViewer.vue

@ -165,7 +165,7 @@ const highlightContent = computed(() => {
if (searchKeyword.value.trim()) {
const kw = searchKeyword.value.trim()
const regex = new RegExp(`(${escapeRegex(kw)})`, 'gi')
return escaped.replace(regex, '<mark style="background:#fff3cd;color:#000;">$1</mark>')
return escaped.replace(regex, '<mark style="background:var(--color-warning-bg);color:var(--color-text-primary);">$1</mark>')
}
return escaped
})
@ -403,8 +403,8 @@ function restartAutoRefresh() {
.content-area {
flex: 1;
overflow: auto;
background: #1e1e1e;
color: #d4d4d4;
background: var(--color-code-block-bg);
color: var(--color-code-block-text);
position: relative;
}
@ -430,8 +430,8 @@ function restartAutoRefresh() {
/* 搜索高亮标记 */
.log-content :deep(mark) {
background: #fff3cd;
color: #000;
background: var(--color-warning-bg);
color: var(--color-text-primary);
padding: 0 2px;
border-radius: 2px;
}

10
frontend/src/views/LoginPage.vue

@ -65,14 +65,14 @@ async function handleLogin() {
<style scoped>
.login-container {
display: flex; flex-direction: column; align-items: center; justify-content: center;
min-height: 100vh; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh; background: linear-gradient(135deg, var(--td-brand-color) 0%, var(--td-brand-color-8) 100%);
}
.login-card {
background: #fff; border-radius: 12px; padding: 40px; width: 380px; max-width: 90vw;
background: var(--td-bg-color-container); border-radius: 12px; padding: 40px; width: 380px; max-width: 90vw;
box-shadow: 0 20px 60px rgba(0,0,0,.15);
}
.login-card h2 { text-align: center; margin-bottom: 6px; color: #333; }
.login-subtitle { text-align: center; color: #999; font-size: 13px; margin-bottom: 32px; }
.login-error { color: var(--td-error-color, #e34d59); font-size: 13px; text-align: center; margin-top: 8px; }
.login-card h2 { text-align: center; margin-bottom: 6px; color: var(--color-text-primary); }
.login-subtitle { text-align: center; color: var(--color-text-tertiary); font-size: 13px; margin-bottom: 32px; }
.login-error { color: var(--color-error); font-size: 13px; text-align: center; margin-top: 8px; }
.login-footer { color: rgba(255,255,255,.6); font-size: 12px; margin-top: 24px; }
</style>

4
frontend/src/views/McpServerManager.vue

@ -24,7 +24,7 @@
</t-table>
<!-- 测试结果 -->
<div v-if="testResult.visible" class="test-toast" :style="{background:testResult.success?'#16a34a':'#dc2626'}">
<div v-if="testResult.visible" class="test-toast" :style="{background:testResult.success?'var(--color-success-text)':'var(--color-error-text)'}">
<strong>{{ testResult.success?'连接成功':'连接失败' }}</strong>
<span v-if="testResult.message" class="test-msg">{{ testResult.message }}</span>
</div>
@ -114,4 +114,4 @@ async function refreshConnections(){refreshLoading.value=true
function getDisplayUrl(s:any){if(s.transport_type==='sse')return s.server_url||'-';let d=s.command||'';if(s.args)d+=' '+(Array.isArray(s.args)?s.args.join(' '):s.args);return d||'-'}
</script>
<style scoped>
.ellipsis{max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:block;}.test-toast{position:fixed;bottom:20px;right:20px;padding:12px 20px;border-radius:8px;z-index:9999;color:#fff;max-width:400px;box-shadow:0 4px 12px rgba(0,0,0,.15);}.test-msg{font-size:12px;margin-top:4px;opacity:.9;display:block;}</style>
.ellipsis{max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:block;}.test-toast{position:fixed;bottom:20px;right:20px;padding:12px 20px;border-radius:8px;z-index:9999;color:var(--td-text-color-anti);max-width:400px;box-shadow:0 4px 12px rgba(0,0,0,.15);}.test-msg{font-size:12px;margin-top:4px;opacity:.9;display:block;}</style>

94
frontend/src/views/ModelConfigManager.vue

@ -4,21 +4,21 @@
<!-- F4: 健康状态汇总卡片 -->
<div v-if="healthSummary" class="health-summary" style="display:flex;gap:12px;margin:12px 0;flex-wrap:wrap;">
<div style="padding:10px 16px;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:8px;min-width:100px;text-align:center;">
<div style="font-size:22px;font-weight:700;color:#16a34a;">{{ healthSummary.online }}</div>
<div style="font-size:11px;color:#166534;">🟢 在线</div>
<div style="padding:10px 16px;background:var(--color-success-bg);border:1px solid var(--color-success-border);border-radius:8px;min-width:100px;text-align:center;">
<div style="font-size:22px;font-weight:700;color:var(--color-success-text);">{{ healthSummary.online }}</div>
<div style="font-size:11px;color:var(--color-success-text-strong);">🟢 在线</div>
</div>
<div style="padding:10px 16px;background:#fef2f2;border:1px solid #fecaca;border-radius:8px;min-width:100px;text-align:center;">
<div style="font-size:22px;font-weight:700;color:#dc2626;">{{ healthSummary.offline }}</div>
<div style="font-size:11px;color:#991b1b;">🔴 离线</div>
<div style="padding:10px 16px;background:var(--color-error-bg);border:1px solid var(--color-error-border);border-radius:8px;min-width:100px;text-align:center;">
<div style="font-size:22px;font-weight:700;color:var(--color-error-text);">{{ healthSummary.offline }}</div>
<div style="font-size:11px;color:var(--color-error-text-strong);">🔴 离线</div>
</div>
<div v-if="healthSummary.unknown" style="padding:10px 16px;background:#fffbeb;border:1px solid #fde68a;border-radius:8px;min-width:100px;text-align:center;">
<div style="font-size:22px;font-weight:700;color:#d97706;">{{ healthSummary.unknown }}</div>
<div style="font-size:11px;color:#92400e;">🟡 未检测</div>
<div v-if="healthSummary.unknown" style="padding:10px 16px;background:var(--color-warning-bg);border:1px solid var(--color-warning-border);border-radius:8px;min-width:100px;text-align:center;">
<div style="font-size:22px;font-weight:700;color:var(--color-warning-text);">{{ healthSummary.unknown }}</div>
<div style="font-size:11px;color:var(--color-warning-text-strong);">🟡 未检测</div>
</div>
<div style="padding:10px 16px;background:#f9fafb;border:1px solid #e5e7eb;border-radius:8px;min-width:100px;text-align:center;">
<div style="font-size:22px;font-weight:700;color:#374151;">{{ healthSummary.total }}</div>
<div style="font-size:11px;color:#6b7280;">📊 总计</div>
<div style="padding:10px 16px;background:var(--color-bg-subtle);border:1px solid var(--color-border);border-radius:8px;min-width:100px;text-align:center;">
<div style="font-size:22px;font-weight:700;color:var(--color-text-primary);">{{ healthSummary.total }}</div>
<div style="font-size:11px;color:var(--color-text-secondary);">📊 总计</div>
</div>
</div>
@ -34,34 +34,34 @@
<!-- F6: Fallback 链可视化 -->
<div v-if="fallbackChains.length > 0" class="fallback-section">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
<span style="font-size:13px;font-weight:600;color:#1e40af;">🔗 Fallback 按优先级排列</span>
<span style="font-size:13px;font-weight:600;color:var(--color-info-text-strong);">🔗 Fallback 按优先级排列</span>
<t-tag size="small" variant="light" theme="primary" style="cursor:pointer;" @click="showFallbackHelp = !showFallbackHelp"> 帮助</t-tag>
</div>
<div v-if="showFallbackHelp" class="fallback-help">
<div style="font-weight:600;color:#1e40af;margin-bottom:8px;">📖 Fallback 链使用说明</div>
<p>Fallback 链是<span style="color:#059669;font-weight:600;">高可用性机制</span>主模型故障时自动切换到备用模型</p>
<div style="font-weight:600;color:var(--color-info-text-strong);margin-bottom:8px;">📖 Fallback 链使用说明</div>
<p>Fallback 链是<span style="color:var(--color-success-text-strong);font-weight:600;">高可用性机制</span>主模型故障时自动切换到备用模型</p>
<p> 勾选设为活跃配置启用 | 📊 调整优先级数值越大越优先| 🔀 拖拽列表项快速调整</p>
</div>
<div v-for="chain in fallbackChains" :key="chain.appType" style="margin-bottom:8px;">
<div style="font-size:12px;color:#1e40af;font-weight:600;">{{ getAppTypeLabel(chain.appType) }}</div>
<div style="font-size:12px;color:var(--color-info-text-strong);font-weight:600;">{{ getAppTypeLabel(chain.appType) }}</div>
<div v-for="(item, idx) in chain.configs" :key="item.id"
style="display:flex;align-items:center;gap:6px;padding:4px 8px;margin:2px 0;background:white;border-radius:4px;font-size:12px;cursor:grab;border:1px solid #e5e7eb;"
style="display:flex;align-items:center;gap:6px;padding:4px 8px;margin:2px 0;background:white;border-radius:4px;font-size:12px;cursor:grab;border:1px solid var(--color-border);"
draggable="true"
@dragstart="onDragStart($event, item, chain)"
@dragover.prevent
@drop="onDrop($event, item, chain)">
<span style="color:#6b7280;font-weight:600;">#{{ idx + 1 }}</span>
<span v-if="idx < chain.configs.length - 1" style="color:#93c5fd;"></span>
<span v-else style="color:#d1d5db;">·</span>
<span style="color:var(--color-text-secondary);font-weight:600;">#{{ idx + 1 }}</span>
<span v-if="idx < chain.configs.length - 1" style="color:var(--color-info-border);"></span>
<span v-else style="color:var(--color-border);">·</span>
<strong>{{ item.name }}</strong>
<code style="font-size:11px;background:#f3f4f6;padding:1px 4px;border-radius:3px;">{{ item.model_name }}</code>
<span style="color:#6b7280;">P={{ item.priority }}</span>
<code style="font-size:11px;background:var(--color-bg-code);padding:1px 4px;border-radius:3px;">{{ item.model_name }}</code>
<span style="color:var(--color-text-secondary);">P={{ item.priority }}</span>
<span v-if="healthMap[item.id]" :title="getHealthTooltip(item.id)"
:style="{ color: healthMap[item.id].status === 'ONLINE' ? '#16a34a' : '#dc2626' }">
:style="{ color: healthMap[item.id].status === 'ONLINE' ? 'var(--color-success-text)' : 'var(--color-error-text)' }">
{{ healthMap[item.id].status === 'ONLINE' ? '🟢' : healthMap[item.id].status === 'UNKNOWN' ? '🟡' : '🔴' }}
<span v-if="healthMap[item.id].latencyMs" style="font-size:10px;">{{ healthMap[item.id].latencyMs }}ms</span>
</span>
<span v-else style="color:#d1d5db;" title="未检测">🟡</span>
<span v-else style="color:var(--color-border);" title="未检测">🟡</span>
</div>
</div>
</div>
@ -75,22 +75,22 @@
<t-tag size="small" variant="light" theme="primary">{{ getAppTypeLabel(row.app_type) }}</t-tag>
</template>
<template #model_name="{ row }">
<code style="font-size:12px;background:#f3f4f6;padding:2px 6px;border-radius:4px;">{{ row.model_name }}</code>
<code style="font-size:12px;background:var(--color-bg-code);padding:2px 6px;border-radius:4px;">{{ row.model_name }}</code>
</template>
<template #base_url="{ row }">
<span style="font-size:12px;color:#6b7280;max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:block;" :title="row.base_url || getProviderBaseUrl(row.provider)">
<span style="font-size:12px;color:var(--color-text-secondary);max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:block;" :title="row.base_url || getProviderBaseUrl(row.provider)">
{{ row.base_url || getProviderBaseUrl(row.provider) || '-' }}
</span>
</template>
<template #api_key="{ row }">
<code style="font-size:12px;background:#f3f4f6;padding:2px 6px;border-radius:4px;">{{ row.api_key || '-' }}</code>
<code style="font-size:12px;background:var(--color-bg-code);padding:2px 6px;border-radius:4px;">{{ row.api_key || '-' }}</code>
</template>
<template #status="{ row }">
<t-tag size="small" :theme="row.is_active ? 'success' : 'default'" variant="light">
{{ row.is_active ? '🟢 活跃' : '⚫ 未激活' }}
</t-tag>
<span v-if="row.is_active && healthMap[row.id]" :title="getHealthTooltip(row.id)"
:style="{ cursor: 'help', color: healthMap[row.id].status === 'ONLINE' ? '#16a34a' : healthMap[row.id].status === 'UNKNOWN' ? '#d97706' : '#dc2626', marginLeft: '4px' }">
:style="{ cursor: 'help', color: healthMap[row.id].status === 'ONLINE' ? 'var(--color-success-text)' : healthMap[row.id].status === 'UNKNOWN' ? 'var(--color-warning-text)' : 'var(--color-error-text)', marginLeft: '4px' }">
{{ healthMap[row.id].status === 'ONLINE' ? '●' : '●' }}
</span>
</template>
@ -110,7 +110,7 @@
<!-- F1: 测试结果提示 -->
<div v-if="testResult.visible" class="test-result-toast"
:style="{ position: 'fixed', bottom: '20px', right: '20px', padding: '12px 20px', borderRadius: '8px', zIndex: 9999, color: 'white', maxWidth: '400px', boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
background: testResult.success ? '#16a34a' : '#dc2626' }">
background: testResult.success ? 'var(--color-success-text)' : 'var(--color-error-text)' }">
<div style="font-weight:600;">{{ testResult.success ? '✅ 连接成功' : '❌ 连接失败' }}</div>
<div style="font-size:12px;margin-top:4px;">延迟{{ testResult.latencyMs }}ms<span v-if="testResult.dimensions"> · 维度{{ testResult.dimensions }}</span></div>
<div v-if="testResult.errorMessage" style="font-size:11px;margin-top:4px;opacity:0.9;">{{ testResult.errorMessage }}</div>
@ -124,7 +124,7 @@
:footer="false"
:close-on-overlay-click="false"
>
<div style="font-size:12px;color:#6b7280;margin-bottom:16px;">💡 核心三要素API Key + API 地址 + 模型名称填入即可使用</div>
<div style="font-size:12px;color:var(--color-text-secondary);margin-bottom:16px;">💡 核心三要素API Key + API 地址 + 模型名称填入即可使用</div>
<t-form label-align="top">
<!-- 应用类型 -->
<t-form-item label="应用类型">
@ -142,7 +142,7 @@
<t-button variant="outline" size="small" @click="showApiKey = !showApiKey">{{ showApiKey ? '🙈 隐藏' : '👁️ 显示' }}</t-button>
</t-space>
<template #help>
<span v-if="editModal.mode === 'edit'" style="font-size:11px;color:#6b7280;">💡 API Key 已脱敏留空保留原值</span>
<span v-if="editModal.mode === 'edit'" style="font-size:11px;color:var(--color-text-secondary);">💡 API Key 已脱敏留空保留原值</span>
</template>
</t-form-item>
@ -151,9 +151,9 @@
<t-input v-model="editModal.form.base_url" :placeholder="providerBaseUrlPlaceholder || '输入 API 地址'" @blur="onBaseUrlBlur" />
<template #help>
<t-space size="small" style="font-size:12px;margin-top:4px;align-items:center;">
<span style="color:#6b7280;">快捷选择</span>
<span style="color:var(--color-text-secondary);">快捷选择</span>
<t-select v-model="editModal.form.provider" :options="providerOptions" style="max-width:200px;" size="small" @change="onProviderChange" />
<span v-show="providerTip" style="color:#3b82f6;">💡 {{ providerTip }}</span>
<span v-show="providerTip" style="color:var(--color-info-text);">💡 {{ providerTip }}</span>
</t-space>
</template>
</t-form-item>
@ -167,7 +167,7 @@
@keydown.enter.prevent="onDropdownKeyDown('enter')" @keydown.escape="modelDropdownOpen = false">
<template #suffix>
<t-button v-show="editModal.form.model_name" variant="text" size="small"
@click="editModal.form.model_name = ''; modelDropdownOpen = false; updateNameSuggestion()" style="color:#9ca3af;"></t-button>
@click="editModal.form.model_name = ''; modelDropdownOpen = false; updateNameSuggestion()" style="color:var(--color-text-tertiary);"></t-button>
</template>
</t-input>
<t-button variant="outline" size="small" @click="fetchModelsFromApi" :disabled="fetchLoading"
@ -177,19 +177,19 @@
</t-space>
<!-- 自定义下拉列表 -->
<div v-if="modelDropdownOpen && filteredModels.length > 0" class="model-dropdown"
style="position:relative;top:100%;left:0;right:0;z-index:100;background:white;border:1px solid #d1d5db;border-radius:8px;max-height:240px;overflow-y:auto;margin-top:4px;box-shadow:0 4px 12px rgba(0,0,0,0.12);">
style="position:relative;top:100%;left:0;right:0;z-index:100;background:white;border:1px solid var(--color-border);border-radius:8px;max-height:240px;overflow-y:auto;margin-top:4px;box-shadow:0 4px 12px rgba(0,0,0,0.12);">
<div v-for="(m, idx) in filteredModels" :key="m.id"
@click="selectModel(m)" @mouseenter="modelDropdownHighlight = idx"
:style="{ padding: '8px 12px', cursor: 'pointer', fontSize: '13px',
borderBottom: idx < filteredModels.length - 1 ? '1px solid #f3f4f6' : 'none',
background: idx === modelDropdownHighlight ? '#eff6ff' : 'white' }">
borderBottom: idx < filteredModels.length - 1 ? '1px solid var(--color-bg-code)' : 'none',
background: idx === modelDropdownHighlight ? 'var(--color-info-bg)' : 'white' }">
<div style="font-weight:500;">{{ m.id }}</div>
<div v-if="m.owned_by" style="font-size:11px;color:#6b7280;margin-top:1px;">{{ m.owned_by }}</div>
<div v-if="m.owned_by" style="font-size:11px;color:var(--color-text-secondary);margin-top:1px;">{{ m.owned_by }}</div>
</div>
</div>
<template #help>
<div style="font-size:11px;color:#6b7280;margin-top:4px;">💡 填入 API Key 和地址后点击获取模型自动填充</div>
<div v-if="fetchedModels.length > 0" style="font-size:11px;color:#16a34a;margin-top:2px;"> 已获取 {{ fetchedModels.length }} 个模型</div>
<div style="font-size:11px;color:var(--color-text-secondary);margin-top:4px;">💡 填入 API Key 和地址后点击获取模型自动填充</div>
<div v-if="fetchedModels.length > 0" style="font-size:11px;color:var(--color-success-text);margin-top:2px;"> 已获取 {{ fetchedModels.length }} 个模型</div>
</template>
</t-form-item>
@ -228,7 +228,7 @@
<t-form-item v-if="editModal.form.app_type === 'EMBEDDING'" label="📐 向量维度(必填)">
<t-input-number v-model="editModal.form.embeddingDimensions" :min="1" :max="8192" placeholder="1024" style="max-width:240px;" />
<template #help>
<span style="font-size:11px;color:#d97706;"> 修改维度后需重建向量表</span>
<span style="font-size:11px;color:var(--color-warning-text);"> 修改维度后需重建向量表</span>
</template>
</t-form-item>
@ -236,7 +236,7 @@
<t-form-item label="📝 配置名称(必填)">
<t-input v-model="editModal.form.name" :placeholder="nameSuggestion || '如:生产环境-DeepSeek对话'" />
<template #help>
<span v-if="nameSuggestion && !editModal.form.name" style="font-size:11px;color:#3b82f6;">
<span v-if="nameSuggestion && !editModal.form.name" style="font-size:11px;color:var(--color-info-text);">
💡 建议<a @click.prevent="editModal.form.name = nameSuggestion" style="color:var(--td-brand-color);cursor:pointer;text-decoration:underline;">{{ nameSuggestion }}</a>
</span>
</template>
@ -276,10 +276,10 @@
<!-- F7: 导入弹窗 -->
<t-dialog v-model:visible="showImportDialog" header="📥 导入模型配置" width="600px" :footer="false">
<t-upload :auto-upload="false" theme="file" accept=".json" @change="onImportFileSelect" />
<div v-if="importPreview" style="margin:12px 0;padding:12px;background:#f9fafb;border-radius:8px;">
<div v-if="importPreview" style="margin:12px 0;padding:12px;background:var(--color-bg-subtle);border-radius:8px;">
<div style="font-size:13px;font-weight:600;margin-bottom:8px;">导入预览</div>
<div style="font-size:12px;"> {{ importPreview.configs.length }} 条配置</div>
<div v-for="c in importPreview.configs.slice(0, 5)" :key="c.name" style="font-size:11px;color:#6b7280;margin:2px 0;">
<div v-for="c in importPreview.configs.slice(0, 5)" :key="c.name" style="font-size:11px;color:var(--color-text-secondary);margin:2px 0;">
· {{ c.name }} ({{ c.appType || c.app_type }}) - {{ c.modelName || c.model_name }}
</div>
</div>
@ -287,7 +287,7 @@
<t-select v-model="importConflict" :options="importConflictOptions" style="max-width:300px;" />
</t-form-item>
<div v-if="importResult" style="margin:12px 0;padding:12px;border-radius:8px;"
:style="{ background: importResult.errors?.length ? '#fef2f2' : '#f0fdf4' }">
:style="{ background: importResult.errors?.length ? 'var(--color-error-bg)' : 'var(--color-success-bg)' }">
<div style="font-size:13px;font-weight:600;">导入完成</div>
<div style="font-size:12px;margin-top:4px;">新增 {{ importResult.added }} · 覆盖 {{ importResult.overwritten }} · 跳过 {{ importResult.skipped }}</div>
</div>
@ -713,7 +713,7 @@ onUnmounted(() => { clearInterval(healthTimer); clearTimeout(testResultTimer); d
</script>
<style scoped>
.fallback-section { margin:12px 0; padding:12px; background:#eff6ff; border:1px solid #bfdbfe; border-radius:8px; }
.fallback-help { margin-bottom:12px; padding:12px; background:white; border:1px solid #93c5fd; border-radius:6px; font-size:12px; line-height:1.6; color:#374151; }
.fallback-section { margin:12px 0; padding:12px; background:var(--color-info-bg); border:1px solid var(--color-info-border); border-radius:8px; }
.fallback-help { margin-bottom:12px; padding:12px; background:white; border:1px solid var(--color-info-border); border-radius:6px; font-size:12px; line-height:1.6; color:var(--color-text-primary); }
.fallback-help p { margin:4px 0; }
</style>

63
frontend/src/views/PipelineFlow.vue

@ -10,13 +10,10 @@
<!-- 图例 -->
<div class="pipeline-legend">
<div class="legend-item"><span class="legend-dot legend-input" /> 用户请求 / 入口</div>
<div class="legend-item"><span class="legend-dot legend-decision" /> 决策分支</div>
<div class="legend-item"><span class="legend-dot legend-process" /> 处理步骤</div>
<div class="legend-item"><span class="legend-dot legend-rag" /> RAG 检索</div>
<div class="legend-item"><span class="legend-dot legend-advisor" /> Advisor </div>
<div class="legend-item"><span class="legend-dot legend-output" /> 最终输出</div>
<div class="legend-item"><span class="legend-dot legend-llm" /> LLM 调用</div>
<div v-for="item in legendItems" :key="item.label" class="legend-item">
<span class="legend-dot" :class="{ 'legend-diamond': item.diamond }" :style="{ background: item.bg, border: `1px solid ${item.border}` }" />
{{ item.label }}
</div>
</div>
<!-- 流程图 -->
@ -36,18 +33,30 @@
<script setup lang="ts">
import { ref, onMounted, nextTick } from 'vue'
import mermaid from 'mermaid'
import { palette, paletteBg, paletteBorder } from '@/utils/palette'
// palette hex
const legendItems = [
{ label: '用户请求 / 入口', bg: paletteBg.blue, border: paletteBorder.blue },
{ label: '决策分支', bg: paletteBg.orange, border: paletteBorder.orange, diamond: true },
{ label: '处理步骤', bg: paletteBg.gray, border: paletteBorder.gray },
{ label: 'RAG 检索', bg: paletteBg.indigo, border: paletteBorder.indigo },
{ label: 'Advisor 链', bg: paletteBg.pink, border: paletteBorder.pink },
{ label: '最终输出', bg: paletteBg.green, border: paletteBorder.green },
{ label: 'LLM 调用', bg: paletteBg.orange, border: paletteBorder.orange },
]
// Mermaid UI
mermaid.initialize({
startOnLoad: false,
theme: 'base',
themeVariables: {
primaryColor: '#f3f4f6',
primaryTextColor: '#1f2937',
primaryBorderColor: '#d1d5db',
lineColor: '#6b7280',
secondaryColor: '#dbeafe',
tertiaryColor: '#fef3c7',
primaryColor: paletteBg.gray,
primaryTextColor: palette.neutralText,
primaryBorderColor: palette.neutralBorder,
lineColor: palette.neutralLine,
secondaryColor: paletteBg.blue,
tertiaryColor: paletteBg.orange,
fontSize: '13px',
},
flowchart: {
@ -123,13 +132,13 @@ flowchart TD
SG["<b>SuggestionGenerator</b><br/>独立 ChatClient(无 MCP 工具)<br/>基于最近 10 条历史<br/>生成 3 条推荐问题<br/>超时 15s · 结果缓存"]
end
style A fill:#dbeafe,stroke:#3b82f6,stroke-width:2px
style AB fill:#d1fae5,stroke:#10b981,stroke-width:2px
style Y fill:#fef3c7,stroke:#f59e0b,stroke-width:2px
style CB fill:#fef3c7,stroke:#f59e0b,stroke-width:2px
style FALLBACK fill:#fee2e2,stroke:#ef4444,stroke-width:2px,stroke-dasharray:5
style M fill:#e0e7ff,stroke:#a5b4fc,stroke-width:2px
style S fill:#e0e7ff,stroke:#a5b4fc,stroke-width:2px
style A fill:${paletteBg.blue},stroke:${paletteBorder.blue},stroke-width:2px
style AB fill:${paletteBg.green},stroke:${paletteBorder.green},stroke-width:2px
style Y fill:${paletteBg.orange},stroke:${paletteBorder.orange},stroke-width:2px
style CB fill:${paletteBg.orange},stroke:${paletteBorder.orange},stroke-width:2px
style FALLBACK fill:${paletteBg.red},stroke:${paletteBorder.red},stroke-width:2px,stroke-dasharray:5
style M fill:${paletteBg.indigo},stroke:${paletteBorder.indigo},stroke-width:2px
style S fill:${paletteBg.indigo},stroke:${paletteBorder.indigo},stroke-width:2px
`
const svg = ref('')
@ -169,7 +178,7 @@ onMounted(() => {
align-items: center;
gap: 8px;
font-size: 12px;
color: var(--td-text-color-placeholder, #999);
color: var(--color-text-tertiary);
}
.legend-dot {
width: 14px;
@ -177,13 +186,7 @@ onMounted(() => {
border-radius: 3px;
flex: none;
}
.legend-input { background: #dbeafe; border: 1px solid #3b82f6; }
.legend-decision { background: #fef3c7; border: 1px solid #f59e0b; transform: rotate(45deg); border-radius: 2px; }
.legend-process { background: #f3f4f6; border: 1px solid #d1d5db; }
.legend-rag { background: #e0e7ff; border: 1px solid #a5b4fc; }
.legend-advisor { background: #fce7f3; border: 1px solid #f9a8d4; }
.legend-output { background: #d1fae5; border: 1px solid #10b981; }
.legend-llm { background: #fef3c7; border: 1px solid #f59e0b; }
.legend-diamond { transform: rotate(45deg); border-radius: 2px; }
.pipeline-diagram {
width: 100%;
@ -199,13 +202,13 @@ onMounted(() => {
.pipeline-loading {
text-align: center;
padding: 40px;
color: var(--td-text-color-placeholder, #999);
color: var(--color-text-tertiary);
font-size: 14px;
}
.pipeline-error {
text-align: center;
padding: 24px;
color: var(--td-error-color, #e34d59);
color: var(--color-error);
font-size: 14px;
}
</style>

5
frontend/src/views/SensitiveWordManager.vue

@ -69,6 +69,7 @@ import { toast } from '@/utils/toast'
import { formatDate } from '@/utils/format'
import { useConfirm } from '@/composables/useConfirm'
import { useDebounce } from '@/composables/useDebounce'
import { palette } from '@/utils/palette'
const { confirm } = useConfirm()
const { debounce } = useDebounce()
@ -85,7 +86,7 @@ const levelOpts = [
{ label: '拦截(阻断回复)', value: 2 },
]
const CATEGORY_LABEL: Record<string, string> = { politics: '政治', porn: '色情', abuse: '辱骂', custom: '自定义' }
const CATEGORY_COLOR: Record<string, string> = { politics: '#dc3545', porn: '#e83e8c', abuse: '#fd7e14', custom: '#6c757d' }
const CATEGORY_COLOR: Record<string, string> = { politics: 'var(--color-error)', porn: palette.pink, abuse: 'var(--color-warning)', custom: 'var(--color-text-secondary)' }
//
const categoryFilterOpts = [{ label: '全部分类', value: '' }, ...categoryOpts]
@ -253,5 +254,5 @@ async function doImport() {
}
function catLabel(c: string) { return CATEGORY_LABEL[c] || c }
function catColor(c: string) { return CATEGORY_COLOR[c] || '#6c757d' }
function catColor(c: string) { return CATEGORY_COLOR[c] || 'var(--color-text-secondary)' }
</script>
Loading…
Cancel
Save