Browse Source

refactor(frontend): 统一列表表格为 BaseTable 组件

新增 BaseTable 封装 t-table,统一下发小尺寸/悬浮/占满宽度/固定表头,操作列自动右固定,替换 13 个列表页 t-table,补齐缺失 loading;同步 UI 开发准则与组件声明
Spring-AI-1.1.2
wanghanlin 1 week ago
parent
commit
97f9befc6d
  1. 9
      frontend/UI-DEV-GUIDE.md
  2. 2
      frontend/components.d.ts
  3. 40
      frontend/src/components/BaseTable.vue
  4. 9
      frontend/src/views/AccountManager.vue
  5. 4
      frontend/src/views/ApiKeyManager.vue
  6. 4
      frontend/src/views/AuditLogManager.vue
  7. 10
      frontend/src/views/ConversationManager.vue
  8. 2
      frontend/src/views/DashboardPanel.vue
  9. 4
      frontend/src/views/FaqManager.vue
  10. 4
      frontend/src/views/FeedbackOps.vue
  11. 10
      frontend/src/views/McpServerManager.vue
  12. 6
      frontend/src/views/ModelConfigManager.vue
  13. 14
      frontend/src/views/PromptTracePanel.vue
  14. 4
      frontend/src/views/SensitiveWordManager.vue
  15. 9
      frontend/src/views/UserManager.vue
  16. 4
      frontend/src/views/WebhookManager.vue

9
frontend/UI-DEV-GUIDE.md

@ -133,7 +133,7 @@ import { palette, paletteBg, paletteBorder } from '@/utils/palette'
</div> </div>
<!-- ② 表格 --> <!-- ② 表格 -->
<t-table
<BaseTable
:data="pagination.data.value" :data="pagination.data.value"
:columns="columns" :columns="columns"
:loading="pagination.loading.value" :loading="pagination.loading.value"
@ -152,7 +152,7 @@ import { palette, paletteBg, paletteBorder } from '@/utils/palette'
<t-button size="small" variant="text" theme="danger" @click="onDelete(row)">删除</t-button> <t-button size="small" variant="text" theme="danger" @click="onDelete(row)">删除</t-button>
</t-space> </t-space>
</template> </template>
</t-table>
</BaseTable>
</t-card> </t-card>
<!-- ③ 弹窗表单 --> <!-- ③ 弹窗表单 -->
@ -229,8 +229,9 @@ const columns = [
| 点 | 规范 | | 点 | 规范 |
| --- | --- | | --- | --- |
| 列表表格组件 | 统一用 `BaseTable` 组件(自动 `size="small"` + 悬浮 `hover` + 占满容器宽度 `table-layout="fixed"` + 固定表头 `max-height`);操作列 `colKey:'op'` 自动右固定 `fixed:'right'`,无需手动设置 |
| 表格列定义 | 统一写在 `<script>``columns` 数组 + `:columns`;**禁用** `<t-table-column>` 标签写法 | | 表格列定义 | 统一写在 `<script>``columns` 数组 + `:columns`;**禁用** `<t-table-column>` 标签写法 |
| 分页 | 统一用 `<t-table>` 内置 `:pagination` + `@page-change` + `showJumper`;**禁用**独立 `<t-pagination>` 组件 |
| 分页 | 统一用 `<BaseTable>` 内置 `:pagination` + `@page-change` + `showJumper`;**禁用**独立 `<t-pagination>` 组件 |
| 列排序 | 后端分页表格:列 `sorter: true` + 表格 `:sort="sortInfo"` + `@sort-change` 透传服务端 `sortField/sortOrder`;前端全量表格:仅列 `sorter: true`,数值列用 `sorter: (a,b)=>(a.x??0)-(b.x??0)` 函数 | | 列排序 | 后端分页表格:列 `sorter: true` + 表格 `:sort="sortInfo"` + `@sort-change` 透传服务端 `sortField/sortOrder`;前端全量表格:仅列 `sorter: true`,数值列用 `sorter: (a,b)=>(a.x??0)-(b.x??0)` 函数 |
| 表格 loading | 必须绑定 `:loading`(现状有 4 个页面缺失) | | 表格 loading | 必须绑定 `:loading`(现状有 4 个页面缺失) |
| pageSize | 默认统一 20(现状 10/20/30 三档不一) | | pageSize | 默认统一 20(现状 10/20/30 三档不一) |
@ -398,7 +399,7 @@ try {
### 一致性偏差 ### 一致性偏差
8. 页头 3 种写法并存:`title` 属性(多数) vs `#title`+`<h2>`(`ModelConfigManager`) vs `#title`+图标(`SystemConfigManager`)。 8. 页头 3 种写法并存:`title` 属性(多数) vs `#title`+`<h2>`(`ModelConfigManager`) vs `#title`+图标(`SystemConfigManager`)。
9. 表格缺 `:loading`:`UserManager` / `AccountManager` / `McpServerManager` / `ConversationManager`
9. ~~表格缺 `:loading`:`UserManager` / `AccountManager` / `McpServerManager` / `ConversationManager`。~~(已补齐,且列表表格统一由 `BaseTable` 承载)
10. 分页缺失/不统一:`AccountManager` 无分页;pageSize 10/20/30 三档。 10. 分页缺失/不统一:`AccountManager` 无分页;pageSize 10/20/30 三档。
11. 表单校验分裂:全无 `:rules`,手写 `if``:disabled` 两种土办法并存。 11. 表单校验分裂:全无 `:rules`,手写 `if``:disabled` 两种土办法并存。
12. 图标体系分裂:emoji 主导,仅 `SystemConfigManager` 用 tdesign-icons;`ModelConfigManager` 操作列纯 emoji 按钮。 12. 图标体系分裂:emoji 主导,仅 `SystemConfigManager` 用 tdesign-icons;`ModelConfigManager` 操作列纯 emoji 按钮。

2
frontend/components.d.ts

@ -7,6 +7,7 @@ export {}
/* prettier-ignore */ /* prettier-ignore */
declare module 'vue' { declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
BaseTable: typeof import('./src/components/BaseTable.vue')['default']
FormDialog: typeof import('./src/components/FormDialog.vue')['default'] FormDialog: typeof import('./src/components/FormDialog.vue')['default']
FormDrawer: typeof import('./src/components/FormDrawer.vue')['default'] FormDrawer: typeof import('./src/components/FormDrawer.vue')['default']
MessageSources: typeof import('./src/components/MessageSources.vue')['default'] MessageSources: typeof import('./src/components/MessageSources.vue')['default']
@ -24,6 +25,7 @@ declare module 'vue' {
TDialog: typeof import('tdesign-vue-next')['Dialog'] TDialog: typeof import('tdesign-vue-next')['Dialog']
TDivider: typeof import('tdesign-vue-next')['Divider'] TDivider: typeof import('tdesign-vue-next')['Divider']
TDrawer: typeof import('tdesign-vue-next')['Drawer'] TDrawer: typeof import('tdesign-vue-next')['Drawer']
TDropdown: typeof import('tdesign-vue-next')['Dropdown']
TEmpty: typeof import('tdesign-vue-next')['Empty'] TEmpty: typeof import('tdesign-vue-next')['Empty']
TForm: typeof import('tdesign-vue-next')['Form'] TForm: typeof import('tdesign-vue-next')['Form']
TFormItem: typeof import('tdesign-vue-next')['FormItem'] TFormItem: typeof import('tdesign-vue-next')['FormItem']

40
frontend/src/components/BaseTable.vue

@ -0,0 +1,40 @@
<script setup lang="ts">
import { computed } from 'vue'
defineOptions({ inheritAttrs: false })
const props = withDefaults(
defineProps<{
columns?: any[]
/** 统一最大高度,传 '' 或 0 关闭固定表头 */
maxHeight?: string | number
}>(),
{
columns: () => [],
maxHeight: 'calc(100vh - 240px)',
},
)
// colKey === 'op' fixed fixed:'right'
const resolvedColumns = computed(() =>
props.columns.map((col) =>
col && col.colKey === 'op' && !col.fixed ? { ...col, fixed: 'right' } : col,
),
)
</script>
<template>
<t-table
v-bind="$attrs"
size="small"
hover
table-layout="fixed"
:max-height="maxHeight"
:columns="resolvedColumns"
>
<!-- 动态透传所有具名插槽op / title / fileType / ... 及作用域插槽 -->
<template v-for="(_, name) in $slots" #[name]="slotProps">
<slot :name="name" v-bind="slotProps || {}" />
</template>
</t-table>
</template>

9
frontend/src/views/AccountManager.vue

@ -5,7 +5,7 @@
<t-button theme="primary" size="small" @click="openCreate"> 新建账号</t-button> <t-button theme="primary" size="small" @click="openCreate"> 新建账号</t-button>
<t-button variant="outline" size="small" @click="loadList">🔄 刷新</t-button> <t-button variant="outline" size="small" @click="loadList">🔄 刷新</t-button>
</div> </div>
<t-table :data="accounts" :columns="columns" row-key="id">
<BaseTable :data="accounts" :columns="columns" row-key="id" :loading="loading">
<template #roles="{row}"><t-tag v-for="r in (row.roleNames||[])" :key="r" size="small" variant="light" theme="primary" style="margin-right:4px;">{{ r }}</t-tag></template> <template #roles="{row}"><t-tag v-for="r in (row.roleNames||[])" :key="r" size="small" variant="light" theme="primary" style="margin-right:4px;">{{ r }}</t-tag></template>
<template #op="{row}"> <template #op="{row}">
<t-space :size="4"> <t-space :size="4">
@ -13,7 +13,7 @@
<t-button size="small" variant="text" theme="danger" @click="doDelete(row.id)">删除</t-button> <t-button size="small" variant="text" theme="danger" @click="doDelete(row.id)">删除</t-button>
</t-space> </t-space>
</template> </template>
</t-table>
</BaseTable>
<t-dialog v-model:visible="modal.visible" :header="modal.isEdit?'编辑账号':'新建账号'" width="480px" :footer="false"> <t-dialog v-model:visible="modal.visible" :header="modal.isEdit?'编辑账号':'新建账号'" width="480px" :footer="false">
<t-form label-align="top"> <t-form label-align="top">
@ -35,7 +35,7 @@ import { useConfirm } from '@/composables/useConfirm'
const { confirm } = useConfirm() const { confirm } = useConfirm()
const accounts=ref<any[]>([])
const accounts=ref<any[]>([]); const loading=ref(false)
const columns=[{colKey:'name',title:'账号名称',width:160,sorter:true},{colKey:'roles',title:'绑定角色',width:240,sorter:true},{colKey:'op',title:'操作',width:120}] const columns=[{colKey:'name',title:'账号名称',width:160,sorter:true},{colKey:'roles',title:'绑定角色',width:240,sorter:true},{colKey:'op',title:'操作',width:120}]
@ -43,7 +43,8 @@ const modal=reactive({visible:false,isEdit:false,id:null as any,name:''})
onMounted(()=>loadList()) onMounted(()=>loadList())
async function loadList(){try{const r=await getAccountList();if(r.success)accounts.value=r.data||[]}catch(e:any){toast('加载失败:'+e.message,'error')}}
async function loadList(){loading.value=true
try{const r=await getAccountList();if(r.success)accounts.value=r.data||[]}catch(e:any){toast('加载失败:'+e.message,'error')}finally{loading.value=false}}
function openCreate(){Object.assign(modal,{visible:true,isEdit:false,id:null,name:''})} function openCreate(){Object.assign(modal,{visible:true,isEdit:false,id:null,name:''})}
function openEdit(a:any){Object.assign(modal,{visible:true,isEdit:true,id:a.id,name:a.name||''})} function openEdit(a:any){Object.assign(modal,{visible:true,isEdit:true,id:a.id,name:a.name||''})}

4
frontend/src/views/ApiKeyManager.vue

@ -5,7 +5,7 @@
<div style="flex:1;" /><span class="total-hint"> {{ total }} Key</span> <div style="flex:1;" /><span class="total-hint"> {{ total }} Key</span>
</div> </div>
<t-table :data="keys" :columns="columns" row-key="id" :loading="loading" :sort="sortInfo"
<BaseTable :data="keys" :columns="columns" row-key="id" :loading="loading" :sort="sortInfo"
:pagination="{current:page,total:total,pageSize:pageSize,showJumper:true}" @page-change="onPageChange" @sort-change="onSortChange"> :pagination="{current:page,total:total,pageSize:pageSize,showJumper:true}" @page-change="onPageChange" @sort-change="onSortChange">
<template #keyValue="{row}"> <template #keyValue="{row}">
<code class="key-code">{{ maskKey(row.keyValue) }}</code> <code class="key-code">{{ maskKey(row.keyValue) }}</code>
@ -25,7 +25,7 @@
<t-button size="small" variant="text" theme="danger" @click="doDelete(row.id)">删除</t-button> <t-button size="small" variant="text" theme="danger" @click="doDelete(row.id)">删除</t-button>
</t-space> </t-space>
</template> </template>
</t-table>
</BaseTable>
<!-- 新建弹窗 --> <!-- 新建弹窗 -->
<t-dialog v-model:visible="showCreateDialog" header="新建 API Key" width="520px" :footer="false"> <t-dialog v-model:visible="showCreateDialog" header="新建 API Key" width="520px" :footer="false">

4
frontend/src/views/AuditLogManager.vue

@ -1,13 +1,13 @@
<template> <template>
<t-card title="📋 内容审计日志" :bordered="false"> <t-card title="📋 内容审计日志" :bordered="false">
<p class="desc-text">记录所有敏感词命中事件包含输入/输出方向违规内容命中词及处理方式</p> <p class="desc-text">记录所有敏感词命中事件包含输入/输出方向违规内容命中词及处理方式</p>
<t-table :data="logs" :columns="columns" row-key="id" :loading="loading" :sort="sortInfo"
<BaseTable :data="logs" :columns="columns" row-key="id" :loading="loading" :sort="sortInfo"
:pagination="{current:page,total:total,pageSize:pageSize,showJumper:true}" @page-change="onPageChange" @sort-change="onSortChange"> :pagination="{current:page,total:total,pageSize:pageSize,showJumper:true}" @page-change="onPageChange" @sort-change="onSortChange">
<template #direction="{row}"><span :style="{color:row.direction==='INPUT'?'var(--color-primary)':'var(--color-success)'}">{{ 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 #originalText="{row}"><span :title="row.originalText" class="ellipsis-td">{{ row.originalText }}</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 #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> <template #hitWords="{row}"><span class="hit-words">{{ formatHitWords(row.hitWords) }}</span></template>
</t-table>
</BaseTable>
</t-card> </t-card>
</template> </template>

10
frontend/src/views/ConversationManager.vue

@ -17,7 +17,7 @@
<t-button variant="outline" size="small" @click="load()">刷新</t-button> <t-button variant="outline" size="small" @click="load()">刷新</t-button>
</div> </div>
<t-table :data="conversations" :columns="columns" row-key="conversationId" :sort="sortInfo"
<BaseTable :data="conversations" :columns="columns" row-key="conversationId" :loading="loading" :sort="sortInfo"
:pagination="{current:page,total:total,pageSize:pageSize,showJumper:true}" @page-change="onPageChange" @sort-change="onSortChange"> :pagination="{current:page,total:total,pageSize:pageSize,showJumper:true}" @page-change="onPageChange" @sort-change="onSortChange">
<template #op="{row}"> <template #op="{row}">
<t-space :size="4"> <t-space :size="4">
@ -26,7 +26,7 @@
<t-button size="small" variant="text" theme="danger" @click="remove(row.conversationId)">删除</t-button> <t-button size="small" variant="text" theme="danger" @click="remove(row.conversationId)">删除</t-button>
</t-space> </t-space>
</template> </template>
</t-table>
</BaseTable>
<!-- 消息详情弹窗 --> <!-- 消息详情弹窗 -->
<t-dialog v-model:visible="msgModal.visible" header="会话消息详情" width="900px" :footer="false"> <t-dialog v-model:visible="msgModal.visible" header="会话消息详情" width="900px" :footer="false">
@ -87,7 +87,7 @@ import { useConfirm } from '@/composables/useConfirm'
const { confirm } = useConfirm() const { confirm } = useConfirm()
const conversations=ref<any[]>([]);const page=ref(1);const pageSize=ref(10);const total=ref(0)
const conversations=ref<any[]>([]);const page=ref(1);const pageSize=ref(10);const total=ref(0);const loading=ref(false)
const keyword=ref('');const accountFilter=ref('');const roleFilter=ref('') const keyword=ref('');const accountFilter=ref('');const roleFilter=ref('')
const roles=ref<any[]>([]);const stats=ref<any>(null) const roles=ref<any[]>([]);const stats=ref<any>(null)
const msgModal=ref({visible:false,conversationId:'',roleName:'',messages:[] as any[]}) const msgModal=ref({visible:false,conversationId:'',roleName:'',messages:[] as any[]})
@ -131,8 +131,8 @@ function reasonLabel(c: string): string {
return map[c] || c return map[c] || c
} }
async function load(p=1){page.value=p
try{const r=await listConversations(p,pageSize.value,keyword.value||undefined,accountFilter.value||undefined,roleFilter.value||undefined,sortInfo.value?.sortBy||undefined,sortInfo.value?(sortInfo.value.descending?'desc':'asc'):undefined);if(r.success){conversations.value=r.data||[];total.value=r.total||0}else toast(r.message||'查询失败','error')}catch(e:any){toast('加载失败:'+e.message,'error')}}
async function load(p=1){page.value=p;loading.value=true
try{const r=await listConversations(p,pageSize.value,keyword.value||undefined,accountFilter.value||undefined,roleFilter.value||undefined,sortInfo.value?.sortBy||undefined,sortInfo.value?(sortInfo.value.descending?'desc':'asc'):undefined);if(r.success){conversations.value=r.data||[];total.value=r.total||0}else toast(r.message||'查询失败','error')}catch(e:any){toast('加载失败:'+e.message,'error')}finally{loading.value=false}}
function onPageChange(i:{current:number;pageSize:number}){pageSize.value=i.pageSize;load(i.current)} function onPageChange(i:{current:number;pageSize:number}){pageSize.value=i.pageSize;load(i.current)}
function onSortChange(s:any){sortInfo.value=s&&s.sortBy?{sortBy:s.sortBy,descending:!!s.descending}:null;load(1)} function onSortChange(s:any){sortInfo.value=s&&s.sortBy?{sortBy:s.sortBy,descending:!!s.descending}:null;load(1)}

2
frontend/src/views/DashboardPanel.vue

@ -34,7 +34,7 @@
<t-row :gutter="16"> <t-row :gutter="16">
<t-col :span="6"> <t-col :span="6">
<t-card title="📚 文档命中排行 TOP-10" :bordered="true"> <t-card title="📚 文档命中排行 TOP-10" :bordered="true">
<t-table :data="topHitDocuments" :columns="hitColumns" row-key="idx" v-if="topHitDocuments.length" />
<BaseTable :data="topHitDocuments" :columns="hitColumns" row-key="idx" v-if="topHitDocuments.length" :max-height="''" />
<t-empty v-else description="暂无数据" /> <t-empty v-else description="暂无数据" />
</t-card> </t-card>
</t-col> </t-col>

4
frontend/src/views/FaqManager.vue

@ -19,7 +19,7 @@
</div> </div>
<!-- FAQ 表格 --> <!-- FAQ 表格 -->
<t-table :data="faqs" :columns="columns" row-key="id" :loading="loading" :sort="sortInfo"
<BaseTable :data="faqs" :columns="columns" row-key="id" :loading="loading" :sort="sortInfo"
:pagination="{ current: page, total: total, pageSize: pageSize, showJumper: true }" @page-change="onPageChange" @sort-change="onSortChange"> :pagination="{ current: page, total: total, pageSize: pageSize, showJumper: true }" @page-change="onPageChange" @sort-change="onSortChange">
<template #status="{ row }"> <template #status="{ row }">
<span @click="toggleStatus(row)" style="cursor:pointer;" :style="{color: row.status === 'ENABLED' ? 'var(--td-success-color)' : 'var(--td-error-color)'}"> <span @click="toggleStatus(row)" style="cursor:pointer;" :style="{color: row.status === 'ENABLED' ? 'var(--td-success-color)' : 'var(--td-error-color)'}">
@ -32,7 +32,7 @@
<t-button size="small" variant="text" theme="danger" @click="removeFaq(row.id)">删除</t-button> <t-button size="small" variant="text" theme="danger" @click="removeFaq(row.id)">删除</t-button>
</t-space> </t-space>
</template> </template>
</t-table>
</BaseTable>
<!-- 新增/编辑抽屉富文本 Markdown 编辑器需宽幅用抽屉替代窄弹窗 --> <!-- 新增/编辑抽屉富文本 Markdown 编辑器需宽幅用抽屉替代窄弹窗 -->
<FormDrawer <FormDrawer

4
frontend/src/views/FeedbackOps.vue

@ -11,7 +11,7 @@
</div> </div>
<!-- 反馈列表 --> <!-- 反馈列表 -->
<t-table :data="feedbacks" :columns="columns" row-key="conversationId" :loading="loading" :sort="sortInfo"
<BaseTable :data="feedbacks" :columns="columns" row-key="conversationId" :loading="loading" :sort="sortInfo"
:pagination="{ current: page, total: total, pageSize: pageSize, showJumper: true }" @page-change="onPageChange" @sort-change="onSortChange" :pagination="{ current: page, total: total, pageSize: pageSize, showJumper: true }" @page-change="onPageChange" @sort-change="onSortChange"
empty="暂无反馈数据,用户在 SDK 端对 AI 回复点赞或点踩后将在此展示。"> empty="暂无反馈数据,用户在 SDK 端对 AI 回复点赞或点踩后将在此展示。">
<template #feedbackType="{ row }"> <template #feedbackType="{ row }">
@ -45,7 +45,7 @@
<t-button size="small" variant="text" theme="danger" @click="removeFeedback(row)">删除</t-button> <t-button size="small" variant="text" theme="danger" @click="removeFeedback(row)">删除</t-button>
</t-space> </t-space>
</template> </template>
</t-table>
</BaseTable>
<!-- 消息详情弹窗 --> <!-- 消息详情弹窗 -->
<t-dialog v-model:visible="detailModal.visible" header="会话消息详情" width="800px" :footer="false"> <t-dialog v-model:visible="detailModal.visible" header="会话消息详情" width="800px" :footer="false">

10
frontend/src/views/McpServerManager.vue

@ -9,7 +9,7 @@
<span class="total-hint"> {{ total }} 条配置</span> <span class="total-hint"> {{ total }} 条配置</span>
</div> </div>
<t-table :data="servers" :columns="columns" row-key="id" :sort="sortInfo"
<BaseTable :data="servers" :columns="columns" row-key="id" :loading="loading" :sort="sortInfo"
:pagination="{ current: currentPage, total: total, pageSize: pageSize, showJumper: true }" @page-change="onPageChange" @sort-change="onSortChange"> :pagination="{ current: currentPage, total: total, pageSize: pageSize, showJumper: true }" @page-change="onPageChange" @sort-change="onSortChange">
<template #transport_type="{ row }"><t-tag size="small" :theme="row.transport_type==='sse'?'primary':'default'" variant="light">{{ row.transport_type==='sse'?'SSE':'Stdio' }}</t-tag></template> <template #transport_type="{ row }"><t-tag size="small" :theme="row.transport_type==='sse'?'primary':'default'" variant="light">{{ row.transport_type==='sse'?'SSE':'Stdio' }}</t-tag></template>
<template #displayUrl="{ row }"><span class="ellipsis" :title="getDisplayUrl(row)">{{ getDisplayUrl(row)||'-' }}</span></template> <template #displayUrl="{ row }"><span class="ellipsis" :title="getDisplayUrl(row)">{{ getDisplayUrl(row)||'-' }}</span></template>
@ -21,7 +21,7 @@
<t-button size="small" variant="text" theme="danger" @click="remove(row.id,row.name)">删除</t-button> <t-button size="small" variant="text" theme="danger" @click="remove(row.id,row.name)">删除</t-button>
</t-space> </t-space>
</template> </template>
</t-table>
</BaseTable>
<!-- 测试结果 --> <!-- 测试结果 -->
<div v-if="testResult.visible" class="test-toast" :style="{background:testResult.success?'var(--color-success-text)':'var(--color-error-text)'}"> <div v-if="testResult.visible" class="test-toast" :style="{background:testResult.success?'var(--color-success-text)':'var(--color-error-text)'}">
@ -68,7 +68,7 @@ import { useConfirm } from '@/composables/useConfirm'
const { confirm } = useConfirm() const { confirm } = useConfirm()
const servers = ref<any[]>([]); const currentPage=ref(1); const pageSize=ref(10); const total=ref(0)
const servers = ref<any[]>([]); const currentPage=ref(1); const pageSize=ref(10); const total=ref(0); const loading=ref(false)
const showModal=ref(false); const modalMode=ref('add'); const editId=ref<any>(null); const saving=ref(false) const showModal=ref(false); const modalMode=ref('add'); const editId=ref<any>(null); const saving=ref(false)
const form=ref({name:'',transportType:'sse',url:'',command:'',argsText:'',envText:'',headersText:'',description:''}) const form=ref({name:'',transportType:'sse',url:'',command:'',argsText:'',envText:'',headersText:'',description:''})
const testLoading=ref<Record<string,boolean>>({}); const testResult=ref({visible:false,success:false,message:'' as any}); let testTimer:any=null const testLoading=ref<Record<string,boolean>>({}); const testResult=ref({visible:false,success:false,message:'' as any}); let testTimer:any=null
@ -80,8 +80,8 @@ const sortInfo = ref<{ sortBy: string; descending: boolean } | null>(null)
onMounted(()=>load()) onMounted(()=>load())
onUnmounted(()=>{if(testTimer)clearTimeout(testTimer)}) onUnmounted(()=>{if(testTimer)clearTimeout(testTimer)})
async function load(p=1){currentPage.value=p
try{const r=await api.listMcpServers(p,pageSize.value,sortInfo.value?.sortBy||undefined,sortInfo.value?(sortInfo.value.descending?'desc':'asc'):undefined);if(r.success){servers.value=(r.data||[]).map((s:any)=>({...s,_active:s.is_active}));total.value=r.total||0}else toast(r.message||'查询失败','error')}catch(e:any){toast('加载失败:'+e.message,'error')}}
async function load(p=1){currentPage.value=p;loading.value=true
try{const r=await api.listMcpServers(p,pageSize.value,sortInfo.value?.sortBy||undefined,sortInfo.value?(sortInfo.value.descending?'desc':'asc'):undefined);if(r.success){servers.value=(r.data||[]).map((s:any)=>({...s,_active:s.is_active}));total.value=r.total||0}else toast(r.message||'查询失败','error')}catch(e:any){toast('加载失败:'+e.message,'error')}finally{loading.value=false}}
function onPageChange(i:{current:number;pageSize:number}){pageSize.value=i.pageSize;load(i.current)} function onPageChange(i:{current:number;pageSize:number}){pageSize.value=i.pageSize;load(i.current)}
function onSortChange(s:any){sortInfo.value=s&&s.sortBy?{sortBy:s.sortBy,descending:!!s.descending}:null;load(1)} function onSortChange(s:any){sortInfo.value=s&&s.sortBy?{sortBy:s.sortBy,descending:!!s.descending}:null;load(1)}

6
frontend/src/views/ModelConfigManager.vue

@ -67,8 +67,7 @@
</div> </div>
<!-- 配置列表表格 --> <!-- 配置列表表格 -->
<div style="overflow-x:auto;">
<t-table :data="configs" :columns="tableColumns" row-key="id" :loading="tableLoading" hover stripe :sort="sortInfo"
<BaseTable :data="configs" :columns="tableColumns" row-key="id" :loading="tableLoading" :sort="sortInfo"
:pagination="{ current: currentPage, total: total, pageSize: pageSize, showJumper: true }" :pagination="{ current: currentPage, total: total, pageSize: pageSize, showJumper: true }"
@page-change="onPageChange" @sort-change="onSortChange"> @page-change="onPageChange" @sort-change="onSortChange">
<template #app_type="{ row }"> <template #app_type="{ row }">
@ -104,8 +103,7 @@
<t-button v-show="!row.is_active" size="small" variant="text" theme="danger" @click="remove(row.id, row.name)" title="删除">🗑</t-button> <t-button v-show="!row.is_active" size="small" variant="text" theme="danger" @click="remove(row.id, row.name)" title="删除">🗑</t-button>
</t-space> </t-space>
</template> </template>
</t-table>
</div>
</BaseTable>
<!-- F1: 测试结果提示 --> <!-- F1: 测试结果提示 -->
<div v-if="testResult.visible" class="test-result-toast" <div v-if="testResult.visible" class="test-result-toast"

14
frontend/src/views/PromptTracePanel.vue

@ -12,7 +12,7 @@
<div class="stat-card"><span class="stat-num">{{ statSummary.avgLatency }}</span><span class="stat-label">平均耗时(ms)</span></div> <div class="stat-card"><span class="stat-num">{{ statSummary.avgLatency }}</span><span class="stat-label">平均耗时(ms)</span></div>
<div class="stat-card"><span class="stat-num">{{ statSummary.faqRate }}</span><span class="stat-label">FAQ 命中率</span></div> <div class="stat-card"><span class="stat-num">{{ statSummary.faqRate }}</span><span class="stat-label">FAQ 命中率</span></div>
</div> </div>
<t-table :data="stats" :columns="statsColumns" row-key="key" size="small" :loading="statsLoading" :pagination="false" style="margin-bottom:16px;" />
<BaseTable :data="stats" :columns="statsColumns" row-key="key" :loading="statsLoading" :pagination="false" :max-height="''" style="margin-bottom:16px;" />
<!-- 筛选栏 --> <!-- 筛选栏 -->
<div class="toolbar"> <div class="toolbar">
@ -32,7 +32,7 @@
</div> </div>
<!-- 调用列表 --> <!-- 调用列表 -->
<t-table :data="traces" :columns="columns" row-key="id" :loading="loading" :sort="sortInfo"
<BaseTable :data="traces" :columns="columns" row-key="id" :loading="loading" :sort="sortInfo"
:selected-row-keys="selectedRowKeys" :selected-row-keys="selectedRowKeys"
:pagination="{ current: page, total: total, pageSize: pageSize, showJumper: true }" :pagination="{ current: page, total: total, pageSize: pageSize, showJumper: true }"
@page-change="onPageChange" @select-change="onSelectChange" @sort-change="onSortChange"> @page-change="onPageChange" @select-change="onSelectChange" @sort-change="onSortChange">
@ -59,7 +59,7 @@
<t-button size="small" variant="text" @click="openDetail(row)">详情</t-button> <t-button size="small" variant="text" @click="openDetail(row)">详情</t-button>
<t-button size="small" variant="text" @click="openTimeline(row)">时间线</t-button> <t-button size="small" variant="text" @click="openTimeline(row)">时间线</t-button>
</template> </template>
</t-table>
</BaseTable>
<!-- 详情抽屉 --> <!-- 详情抽屉 -->
<t-drawer v-model:visible="detailVisible" size="large" header="调用详情" :footer="false"> <t-drawer v-model:visible="detailVisible" size="large" header="调用详情" :footer="false">
@ -97,7 +97,7 @@
</t-tab-panel> </t-tab-panel>
<t-tab-panel value="rag" label="RAG 资料"> <t-tab-panel value="rag" label="RAG 资料">
<div class="muted" style="margin-bottom:8px;">检索模式{{ detail.searchMode || '-' }} · 命中{{ detail.hitCount ?? '-' }} </div> <div class="muted" style="margin-bottom:8px;">检索模式{{ detail.searchMode || '-' }} · 命中{{ detail.hitCount ?? '-' }} </div>
<t-table v-if="detailRagHits.length" :data="detailRagHits" :columns="ragHitsColumns" size="small" :pagination="false" style="margin-bottom:12px;" />
<BaseTable v-if="detailRagHits.length" :data="detailRagHits" :columns="ragHitsColumns" :pagination="false" :max-height="''" style="margin-bottom:12px;" />
<div class="block-title">注入的 RAG 上下文</div> <div class="block-title">注入的 RAG 上下文</div>
<pre class="full-prompt">{{ detail.ragContext || '(无 RAG 资料)' }}</pre> <pre class="full-prompt">{{ detail.ragContext || '(无 RAG 资料)' }}</pre>
</t-tab-panel> </t-tab-panel>
@ -111,18 +111,18 @@
</div> </div>
</t-tab-panel> </t-tab-panel>
<t-tab-panel value="tools" label="工具调用"> <t-tab-panel value="tools" label="工具调用">
<t-table v-if="detailToolCalls.length" :data="detailToolCalls" :columns="toolCallsColumns" size="small" :pagination="false">
<BaseTable v-if="detailToolCalls.length" :data="detailToolCalls" :columns="toolCallsColumns" :pagination="false" :max-height="''">
<template #latencyMs="{ row }"><span>{{ row.latencyMs ?? '-' }}ms</span></template> <template #latencyMs="{ row }"><span>{{ row.latencyMs ?? '-' }}ms</span></template>
<template #error="{ row }"> <template #error="{ row }">
<t-tag v-if="row.error" size="small" variant="light" theme="danger">失败</t-tag> <t-tag v-if="row.error" size="small" variant="light" theme="danger">失败</t-tag>
<span v-else class="muted">-</span> <span v-else class="muted">-</span>
</template> </template>
</t-table>
</BaseTable>
<t-empty v-else description="本次调用未触发 MCP 工具" /> <t-empty v-else description="本次调用未触发 MCP 工具" />
</t-tab-panel> </t-tab-panel>
<t-tab-panel value="context" label="对话上下文"> <t-tab-panel value="context" label="对话上下文">
<div v-if="detail.historyTurns" class="muted" style="margin-bottom:8px;">历史轮数{{ detail.historyTurns }}</div> <div v-if="detail.historyTurns" class="muted" style="margin-bottom:8px;">历史轮数{{ detail.historyTurns }}</div>
<t-table v-if="detailHistory.length" :data="detailHistory" :columns="historyColumns" size="small" :pagination="false" />
<BaseTable v-if="detailHistory.length" :data="detailHistory" :columns="historyColumns" :pagination="false" :max-height="''" />
<t-empty v-else description="无多轮历史记录" /> <t-empty v-else description="无多轮历史记录" />
</t-tab-panel> </t-tab-panel>
</t-tabs> </t-tabs>

4
frontend/src/views/SensitiveWordManager.vue

@ -12,7 +12,7 @@
<span class="total-hint"> {{ total }} </span> <span class="total-hint"> {{ total }} </span>
</div> </div>
<t-table :data="words" :columns="columns" row-key="id" :loading="loading" :sort="sortInfo"
<BaseTable :data="words" :columns="columns" row-key="id" :loading="loading" :sort="sortInfo"
:pagination="{ current: page, total: total, pageSize: pageSize, showJumper: true }" @page-change="onPageChange" @sort-change="onSortChange"> :pagination="{ current: page, total: total, pageSize: pageSize, showJumper: true }" @page-change="onPageChange" @sort-change="onSortChange">
<template #category="{ row }"> <template #category="{ row }">
<t-tag size="small" :style="{ background: catColor(row.category) }" variant="light">{{ catLabel(row.category) }}</t-tag> <t-tag size="small" :style="{ background: catColor(row.category) }" variant="light">{{ catLabel(row.category) }}</t-tag>
@ -29,7 +29,7 @@
<t-button size="small" variant="text" theme="danger" @click="doDelete(row)">删除</t-button> <t-button size="small" variant="text" theme="danger" @click="doDelete(row)">删除</t-button>
</t-space> </t-space>
</template> </template>
</t-table>
</BaseTable>
<!-- 新增/编辑弹窗 --> <!-- 新增/编辑弹窗 -->
<t-dialog v-model:visible="showFormDialog" :header="editingId ? '编辑敏感词' : '添加敏感词'" width="480px" :footer="false"> <t-dialog v-model:visible="showFormDialog" :header="editingId ? '编辑敏感词' : '添加敏感词'" width="480px" :footer="false">

9
frontend/src/views/UserManager.vue

@ -9,7 +9,7 @@
<t-button theme="primary" size="small" @click="openCreate">+ 新建用户</t-button> <t-button theme="primary" size="small" @click="openCreate">+ 新建用户</t-button>
</div> </div>
<t-table :data="users" :columns="columns" row-key="id" :sort="sortInfo"
<BaseTable :data="users" :columns="columns" row-key="id" :loading="loading" :sort="sortInfo"
:pagination="{ current: page, total: total, pageSize: pageSize, showJumper: true }" @page-change="onPageChange" @sort-change="onSortChange"> :pagination="{ current: page, total: total, pageSize: pageSize, showJumper: true }" @page-change="onPageChange" @sort-change="onSortChange">
<template #role_names="{ row }"> <template #role_names="{ row }">
<t-tag v-for="r in (row.role_names||'').split(',').filter(Boolean)" :key="r" size="small" variant="light" theme="primary" style="margin-right:2px;">{{ r }}</t-tag> <t-tag v-for="r in (row.role_names||'').split(',').filter(Boolean)" :key="r" size="small" variant="light" theme="primary" style="margin-right:2px;">{{ r }}</t-tag>
@ -26,7 +26,7 @@
<t-button size="small" variant="text" :theme="row.enabled?'warning':'success'" @click="handleToggle(row)">{{ row.enabled?'禁用':'启用' }}</t-button> <t-button size="small" variant="text" :theme="row.enabled?'warning':'success'" @click="handleToggle(row)">{{ row.enabled?'禁用':'启用' }}</t-button>
</t-space> </t-space>
</template> </template>
</t-table>
</BaseTable>
<!-- 新建/编辑弹窗 --> <!-- 新建/编辑弹窗 -->
<t-dialog v-model:visible="modal.visible" :header="modal.isEdit?'编辑用户':'新建用户'" width="480px" :footer="false"> <t-dialog v-model:visible="modal.visible" :header="modal.isEdit?'编辑用户':'新建用户'" width="480px" :footer="false">
@ -75,7 +75,7 @@ import { useConfirm } from '@/composables/useConfirm'
const { confirm } = useConfirm() const { confirm } = useConfirm()
const users = ref<any[]>([]); const total=ref(0); const page=ref(1); const pageSize=ref(20) const users = ref<any[]>([]); const total=ref(0); const page=ref(1); const pageSize=ref(20)
const keyword=ref(''); const filterEnabled=ref<boolean|null>(null); const allRoles=ref<any[]>([]); const saving=ref(false)
const keyword=ref(''); const filterEnabled=ref<boolean|null>(null); const allRoles=ref<any[]>([]); const saving=ref(false); const loading=ref(false)
const statusOpts=[{label:'全部状态',value:null},{label:'已启用',value:true},{label:'已禁用',value:false}] const statusOpts=[{label:'全部状态',value:null},{label:'已启用',value:true},{label:'已禁用',value:false}]
const columns = [ const columns = [
@ -91,7 +91,8 @@ const pwdModal = reactive({visible:false,userId:null as any,username:'',newPassw
onMounted(()=>{loadData();loadRoles()}) onMounted(()=>{loadData();loadRoles()})
async function loadData(){try{const r=await listSysUsers(page.value,pageSize.value,keyword.value,filterEnabled.value as any,sortInfo.value?.sortBy||undefined,sortInfo.value?(sortInfo.value.descending?'desc':'asc'):undefined);if(r.success){users.value=r.data.records||[];total.value=r.data.total||0}}catch(e){console.error(e)}}
async function loadData(){loading.value=true
try{const r=await listSysUsers(page.value,pageSize.value,keyword.value,filterEnabled.value as any,sortInfo.value?.sortBy||undefined,sortInfo.value?(sortInfo.value.descending?'desc':'asc'):undefined);if(r.success){users.value=r.data.records||[];total.value=r.data.total||0}}catch(e){console.error(e)}finally{loading.value=false}}
async function loadRoles(){try{const r=await getAllSysRoles();if(r.success)allRoles.value=r.data||[]}catch(e){console.error(e)}} async function loadRoles(){try{const r=await getAllSysRoles();if(r.success)allRoles.value=r.data||[]}catch(e){console.error(e)}}
function onPageChange(i:{current:number;pageSize:number}){page.value=i.current;pageSize.value=i.pageSize;loadData()} function onPageChange(i:{current:number;pageSize:number}){page.value=i.current;pageSize.value=i.pageSize;loadData()}
function onSortChange(s:any){sortInfo.value=s&&s.sortBy?{sortBy:s.sortBy,descending:!!s.descending}:null;loadData()} function onSortChange(s:any){sortInfo.value=s&&s.sortBy?{sortBy:s.sortBy,descending:!!s.descending}:null;loadData()}

4
frontend/src/views/WebhookManager.vue

@ -5,7 +5,7 @@
<div style="flex:1;" /><span class="total-hint"> {{ total }} </span> <div style="flex:1;" /><span class="total-hint"> {{ total }} </span>
</div> </div>
<t-table :data="webhooks" :columns="columns" row-key="id" :loading="loading" :sort="sortInfo"
<BaseTable :data="webhooks" :columns="columns" row-key="id" :loading="loading" :sort="sortInfo"
:pagination="{current:page,total:total,pageSize:pageSize,showJumper:true}" @page-change="onPageChange" @sort-change="onSortChange"> :pagination="{current:page,total:total,pageSize:pageSize,showJumper:true}" @page-change="onPageChange" @sort-change="onSortChange">
<template #events="{row}"> <template #events="{row}">
<t-tag v-for="ev in getEvents(row)" :key="ev" size="small" variant="light" style="margin-right:4px;">{{ ev }}</t-tag> <t-tag v-for="ev in getEvents(row)" :key="ev" size="small" variant="light" style="margin-right:4px;">{{ ev }}</t-tag>
@ -18,7 +18,7 @@
<t-button size="small" variant="text" theme="danger" @click="doDelete(row.id)">删除</t-button> <t-button size="small" variant="text" theme="danger" @click="doDelete(row.id)">删除</t-button>
</t-space> </t-space>
</template> </template>
</t-table>
</BaseTable>
<t-dialog v-model:visible="showFormDialog" :header="editingId?'编辑 Webhook':'新建 Webhook'" width="520px" :footer="false"> <t-dialog v-model:visible="showFormDialog" :header="editingId?'编辑 Webhook':'新建 Webhook'" width="520px" :footer="false">
<t-form label-align="top"> <t-form label-align="top">

Loading…
Cancel
Save