feat: Phase 4 多租户隔离 + 四阶段升级测试 + CSS 统一化
Phase 4: org_id 注入 JWT/API 过滤/组织管理 CRUD/前端组织列 测试: tests/test_phase_upgrades.py 97项全覆盖 CSS: theme-modern.css 共享 mobile-card-list/status-dot/search-bar 等模式 修复: initial_data.py LLM配置 NOT NULL 约束, TopicResponse 含 org_id
This commit is contained in:
+370
-98
@@ -7,69 +7,28 @@
|
||||
<link rel="stylesheet" href="element-plus.css">
|
||||
<link rel="stylesheet" href="theme-modern.css">
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background: #f5f7fa; color: #303133; }
|
||||
.main-content { display: flex; flex: 1; max-width: 1400px; margin: 0 auto; min-height: calc(100vh - 60px); }
|
||||
.content-area { flex: 1; padding: 24px; overflow-y: auto; }
|
||||
.card { background: white; border-radius: 16px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); overflow-x: auto; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); }
|
||||
.card-loading { display: flex; justify-content: center; align-items: center; min-height: 200px; color: #909399; font-size: 14px; }
|
||||
.empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 60px 20px; color: #909399; }
|
||||
.empty-state .empty-icon { font-size: 48px; margin-bottom: 12px; opacity: 0.4; }
|
||||
.empty-state .empty-text { font-size: 14px; margin-bottom: 16px; }
|
||||
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; flex-wrap: wrap; gap: 12px; }
|
||||
.page-title { font-size: 24px; font-weight: 700; color: #303133; display: flex; align-items: center; gap: 8px; }
|
||||
.filter-bar { display: flex; gap: 8px; margin-bottom: 20px; flex-wrap: wrap; }
|
||||
.toolbar { margin-bottom: 16px; display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
||||
.el-table { width: 100%; }
|
||||
.el-table .el-table__cell { word-break: break-word; }
|
||||
.stat-summary { display: flex; gap: 16px; flex-wrap: wrap; margin-bottom: 16px; }
|
||||
.stat-item { background: #f0f5ff; border-radius: 8px; padding: 12px 20px; display: flex; flex-direction: column; align-items: center; min-width: 100px; }
|
||||
.stat-item .num { font-size: 24px; font-weight: 600; color: #409eff; }
|
||||
.stat-item .label { font-size: 12px; color: #909399; margin-top: 4px; }
|
||||
|
||||
.mobile-card-list { display: none; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.content-area { padding: 16px; padding-bottom: 80px; }
|
||||
.card { padding: 16px; }
|
||||
.data-table { display: none; }
|
||||
.mobile-card-list { display: block; }
|
||||
.mobile-card {
|
||||
background: #fafbfc;
|
||||
border-radius: 10px;
|
||||
padding: 14px;
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid #ebeef5;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.mobile-card:active { transform: scale(0.99); }
|
||||
.mobile-card-row { display: flex; justify-content: space-between; padding: 6px 0; font-size: 13px; border-bottom: 1px dashed #f0f0f0; }
|
||||
.mobile-card-row:last-child { border-bottom: none; }
|
||||
.mobile-card-label { color: #909399; flex-shrink: 0; margin-right: 8px; }
|
||||
.mobile-card-value { color: #303133; text-align: right; word-break: break-word; }
|
||||
.mobile-card-actions { display: flex; gap: 8px; justify-content: flex-end; padding-top: 10px; margin-top: 6px; border-top: 1px solid #ebeef5; }
|
||||
.stat-summary { gap: 8px; }
|
||||
.stat-item { min-width: 70px; padding: 8px 12px; }
|
||||
.stat-item .num { font-size: 18px; }
|
||||
}
|
||||
.data-table { display: block; }
|
||||
@media (max-width: 768px) { .data-table { display: none; } }
|
||||
</style>
|
||||
<script src="navigation-component.js"></script>
|
||||
<script src="navbar-component.js"></script>
|
||||
<script src="uni-nav.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<navbar-component title="系统管理" :username="currentUser.username" :is-admin="isAdmin" @logout="logout"></navbar-component>
|
||||
<navigation-component current-page="admin" :is-admin="isAdmin" @navigate="redirectToPage"></navigation-component>
|
||||
<uni-nav title="系统管理" :username="currentUser.username" :is-admin="isAdmin" current-page="admin" @navigate="redirectToPage" @logout="logout">
|
||||
</uni-nav>
|
||||
<div class="main-content">
|
||||
<main class="content-area">
|
||||
<div class="card page-fade">
|
||||
<div class="page-header">
|
||||
<h2 class="page-title">⚙️ 系统管理</h2>
|
||||
<h2 class="page-title"><el-icon style="vertical-align:-2px;"><IconSetting /></el-icon> 系统管理</h2>
|
||||
<div class="filter-bar">
|
||||
<el-button size="default" :type="activeTab === 'cases' ? 'primary' : ''" @click="switchTab('cases')">案例管理</el-button>
|
||||
<el-button size="default" :type="activeTab === 'tasklogs' ? 'primary' : ''" @click="switchTab('tasklogs')">任务日志</el-button>
|
||||
<el-button size="default" :type="activeTab === 'llmconfigs' ? 'primary' : ''" @click="switchTab('llmconfigs')">LLM配置</el-button>
|
||||
<el-button size="default" :type="activeTab === 'systemconfigs' ? 'primary' : ''" @click="switchTab('systemconfigs')">系统配置</el-button>
|
||||
<el-button size="default" :type="activeTab === 'categories' ? 'primary' : ''" @click="switchTab('categories')">采集类别</el-button>
|
||||
<el-button size="default" :type="activeTab === 'sources' ? 'primary' : ''" @click="switchTab('sources')">信息源</el-button>
|
||||
<el-button size="default" :type="activeTab === 'orgs' ? 'primary' : ''" @click="switchTab('orgs')">组织管理</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -80,7 +39,7 @@
|
||||
<div v-if="casesLoading" class="card-loading">加载中...</div>
|
||||
<template v-else-if="cases.length === 0">
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">📋</div>
|
||||
<el-icon style="font-size:48px;color:#c0c4cc;"><IconTopic /></el-icon>
|
||||
<div class="empty-text">暂无案例数据</div>
|
||||
<el-button type="primary" size="small" @click="showCaseDialog()">新增第一个案例</el-button>
|
||||
</div>
|
||||
@@ -103,12 +62,12 @@
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
<div class="mobile-card-list">
|
||||
<div v-for="item in cases" :key="item.id" class="mobile-card">
|
||||
<div class="mobile-card-row"><span class="mobile-card-label">标题</span><span class="mobile-card-value">{{ item.title }}</span></div>
|
||||
<div class="mobile-card-row"><span class="mobile-card-label">领域</span><span class="mobile-card-value">{{ item.field }}</span></div>
|
||||
<div class="mobile-card-row"><span class="mobile-card-label">来源</span><span class="mobile-card-value">{{ item.source }}</span></div>
|
||||
<div class="mobile-card-actions">
|
||||
<div class="card-list-mobile">
|
||||
<div v-for="item in cases" :key="item.id" class="card-item">
|
||||
<div class="card-row"><span class="card-label">标题</span><span class="card-value">{{ item.title }}</span></div>
|
||||
<div class="card-row"><span class="card-label">领域</span><span class="card-value">{{ item.field }}</span></div>
|
||||
<div class="card-row"><span class="card-label">来源</span><span class="card-value">{{ item.source }}</span></div>
|
||||
<div class="card-actions">
|
||||
<el-button size="small" @click="showCaseDialog(item)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="deleteCase(item.id)">删除</el-button>
|
||||
</div>
|
||||
@@ -123,7 +82,7 @@
|
||||
<div v-if="taskLogsLoading" class="card-loading">加载中...</div>
|
||||
<template v-else-if="taskLogs.length === 0">
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">📝</div>
|
||||
<el-icon style="font-size:48px;color:#c0c4cc;"><IconDocument /></el-icon>
|
||||
<div class="empty-text">暂无任务日志</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -139,13 +98,13 @@
|
||||
<el-table-column prop="duration" label="耗时" width="80"></el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
<div class="mobile-card-list">
|
||||
<div v-for="item in taskLogs" :key="item.id" class="mobile-card">
|
||||
<div class="mobile-card-row"><span class="mobile-card-label">任务</span><span class="mobile-card-value">{{ item.task_name }}</span></div>
|
||||
<div class="mobile-card-row"><span class="mobile-card-label">选题</span><span class="mobile-card-value">{{ item.topic_id }}</span></div>
|
||||
<div class="mobile-card-row"><span class="mobile-card-label">状态</span><span class="mobile-card-value">{{ item.status }}</span></div>
|
||||
<div class="mobile-card-row"><span class="mobile-card-label">消息</span><span class="mobile-card-value">{{ item.message }}</span></div>
|
||||
<div class="mobile-card-row"><span class="mobile-card-label">耗时</span><span class="mobile-card-value">{{ item.duration }}s</span></div>
|
||||
<div class="card-list-mobile">
|
||||
<div v-for="item in taskLogs" :key="item.id" class="card-item">
|
||||
<div class="card-row"><span class="card-label">任务</span><span class="card-value">{{ item.task_name }}</span></div>
|
||||
<div class="card-row"><span class="card-label">选题</span><span class="card-value">{{ item.topic_id }}</span></div>
|
||||
<div class="card-row"><span class="card-label">状态</span><span class="card-value">{{ item.status }}</span></div>
|
||||
<div class="card-row"><span class="card-label">消息</span><span class="card-value">{{ item.message }}</span></div>
|
||||
<div class="card-row"><span class="card-label">耗时</span><span class="card-value">{{ item.duration }}s</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -157,7 +116,7 @@
|
||||
<div v-if="llmConfigsLoading" class="card-loading">加载中...</div>
|
||||
<template v-else-if="llmConfigs.length === 0">
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">🤖</div>
|
||||
<el-icon style="font-size:48px;color:#c0c4cc;"><IconDocument /></el-icon>
|
||||
<div class="empty-text">暂无 LLM 配置</div>
|
||||
<el-button type="primary" size="small" @click="showLLMConfigDialog()">新增配置</el-button>
|
||||
</div>
|
||||
@@ -170,7 +129,10 @@
|
||||
<el-table-column prop="temperature" label="温度" width="80"></el-table-column>
|
||||
<el-table-column prop="max_tokens" label="最大Token" width="110"></el-table-column>
|
||||
<el-table-column prop="is_active" label="激活" width="70">
|
||||
<template #default="scope">{{ scope.row.is_active ? '是' : '否' }}</template>
|
||||
<template #default="scope">
|
||||
<el-icon v-if="scope.row.is_active" style="color:#67C23A;"><IconCheck /></el-icon>
|
||||
<el-icon v-else style="color:#F56C6C;"><IconClose /></el-icon>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="140" fixed="right">
|
||||
<template #default="scope">
|
||||
@@ -180,13 +142,13 @@
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
<div class="mobile-card-list">
|
||||
<div v-for="item in llmConfigs" :key="item.id" class="mobile-card">
|
||||
<div class="mobile-card-row"><span class="mobile-card-label">名称</span><span class="mobile-card-value">{{ item.name }}</span></div>
|
||||
<div class="mobile-card-row"><span class="mobile-card-label">模型</span><span class="mobile-card-value">{{ item.model }}</span></div>
|
||||
<div class="mobile-card-row"><span class="mobile-card-label">温度</span><span class="mobile-card-value">{{ item.temperature }}</span></div>
|
||||
<div class="mobile-card-row"><span class="mobile-card-label">激活</span><span class="mobile-card-value">{{ item.is_active ? '是' : '否' }}</span></div>
|
||||
<div class="mobile-card-actions">
|
||||
<div class="card-list-mobile">
|
||||
<div v-for="item in llmConfigs" :key="item.id" class="card-item">
|
||||
<div class="card-row"><span class="card-label">名称</span><span class="card-value">{{ item.name }}</span></div>
|
||||
<div class="card-row"><span class="card-label">模型</span><span class="card-value">{{ item.model }}</span></div>
|
||||
<div class="card-row"><span class="card-label">温度</span><span class="card-value">{{ item.temperature }}</span></div>
|
||||
<div class="card-row"><span class="card-label">激活</span><span class="card-value">{{ item.is_active ? '是' : '否' }}</span></div>
|
||||
<div class="card-actions">
|
||||
<el-button size="small" @click="showLLMConfigDialog(item)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="deleteLLMConfig(item.id)">删除</el-button>
|
||||
</div>
|
||||
@@ -201,7 +163,7 @@
|
||||
<div v-if="systemConfigsLoading" class="card-loading">加载中...</div>
|
||||
<template v-else-if="systemConfigs.length === 0">
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">⚙️</div>
|
||||
<el-icon style="font-size:48px;color:#c0c4cc;"><IconSetting /></el-icon>
|
||||
<div class="empty-text">暂无系统配置</div>
|
||||
<el-button type="primary" size="small" @click="showSystemConfigDialog()">新增配置</el-button>
|
||||
</div>
|
||||
@@ -223,18 +185,162 @@
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
<div class="mobile-card-list">
|
||||
<div v-for="item in systemConfigs" :key="item.key" class="mobile-card">
|
||||
<div class="mobile-card-row"><span class="mobile-card-label">键</span><span class="mobile-card-value">{{ item.key }}</span></div>
|
||||
<div class="mobile-card-row"><span class="mobile-card-label">值</span><span class="mobile-card-value">{{ typeof item.value === 'object' ? JSON.stringify(item.value) : item.value }}</span></div>
|
||||
<div class="mobile-card-row"><span class="mobile-card-label">描述</span><span class="mobile-card-value">{{ item.description }}</span></div>
|
||||
<div class="mobile-card-actions">
|
||||
<div class="card-list-mobile">
|
||||
<div v-for="item in systemConfigs" :key="item.key" class="card-item">
|
||||
<div class="card-row"><span class="card-label">键</span><span class="card-value">{{ item.key }}</span></div>
|
||||
<div class="card-row"><span class="card-label">值</span><span class="card-value">{{ typeof item.value === 'object' ? JSON.stringify(item.value) : item.value }}</span></div>
|
||||
<div class="card-row"><span class="card-label">描述</span><span class="card-value">{{ item.description }}</span></div>
|
||||
<div class="card-actions">
|
||||
<el-button size="small" @click="showSystemConfigDialog(item)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="deleteSystemConfig(item.key)">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="activeTab === 'categories'">
|
||||
<div class="toolbar">
|
||||
<el-button type="primary" size="small" @click="showCategoryDialog()">新增类别</el-button>
|
||||
<el-button size="small" @click="loadCategories">刷新</el-button>
|
||||
</div>
|
||||
<div v-if="categoriesLoading" class="card-loading">加载中...</div>
|
||||
<template v-else-if="categories.length === 0">
|
||||
<div class="empty-state">
|
||||
<el-icon style="font-size:48px;color:#c0c4cc;"><IconPicture /></el-icon>
|
||||
<div class="empty-text">暂无采集类别</div>
|
||||
<el-button type="primary" size="small" @click="showCategoryDialog()">新增类别</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-table :data="categories" border stripe class="data-table" style="width:100%">
|
||||
<el-table-column prop="id" label="ID" width="60"></el-table-column>
|
||||
<el-table-column prop="name" label="名称" min-width="120"></el-table-column>
|
||||
<el-table-column prop="search_query" label="搜索词" min-width="200"></el-table-column>
|
||||
<el-table-column prop="source_count" label="信息源数" width="100"></el-table-column>
|
||||
<el-table-column prop="is_active" label="激活" width="70">
|
||||
<template #default="scope">
|
||||
<el-icon v-if="scope.row.is_active" style="color:#67C23A;"><IconCheck /></el-icon>
|
||||
<el-icon v-else style="color:#F56C6C;"><IconClose /></el-icon>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="140" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button size="small" @click="showCategoryDialog(scope.row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="deleteCategory(scope.row.id)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
<div class="card-list-mobile" v-if="categories.length > 0">
|
||||
<div v-for="item in categories" :key="item.id" class="card-item">
|
||||
<div class="card-row"><span class="card-label">名称</span><span class="card-value">{{ item.name }}</span></div>
|
||||
<div class="card-row"><span class="card-label">搜索词</span><span class="card-value">{{ item.search_query }}</span></div>
|
||||
<div class="card-row"><span class="card-label">源数</span><span class="card-value">{{ item.source_count }}</span></div>
|
||||
<div class="card-actions">
|
||||
<el-button size="small" @click="showCategoryDialog(item)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="deleteCategory(item.id)">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="activeTab === 'orgs'">
|
||||
<div class="toolbar">
|
||||
<el-button type="primary" size="small" @click="showOrgDialog()">新增组织</el-button>
|
||||
<el-button size="small" @click="loadOrgs">刷新</el-button>
|
||||
<span style="font-size:13px;color:#909399;margin-left:8px;">共 {{ orgs.length }} 个</span>
|
||||
</div>
|
||||
<div v-if="orgsLoading" class="card-loading">加载中...</div>
|
||||
<template v-else-if="orgs.length === 0">
|
||||
<div class="empty-state">
|
||||
<el-icon style="font-size:48px;color:#c0c4cc;"><IconSetting /></el-icon>
|
||||
<div class="empty-text">暂无组织</div>
|
||||
<el-button type="primary" size="small" @click="showOrgDialog()">新增组织</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-table :data="orgs" border stripe class="data-table" style="width:100%">
|
||||
<el-table-column prop="org_id" label="组织ID" min-width="120"></el-table-column>
|
||||
<el-table-column prop="name" label="名称" min-width="150"></el-table-column>
|
||||
<el-table-column prop="description" label="描述" min-width="200" :show-overflow-tooltip="true"></el-table-column>
|
||||
<el-table-column prop="user_count" label="用户数" width="80"></el-table-column>
|
||||
<el-table-column prop="topic_count" label="选题数" width="80"></el-table-column>
|
||||
<el-table-column label="默认" width="70">
|
||||
<template #default="scope">
|
||||
<el-icon v-if="scope.row.is_default" style="color:#67C23A;"><IconCheck /></el-icon>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="140" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button size="small" @click="showOrgDialog(scope.row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="deleteOrg(scope.row.org_id)" :disabled="scope.row.is_default">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
<div class="card-list-mobile" v-if="orgs.length > 0">
|
||||
<div v-for="item in orgs" :key="item.org_id" class="card-item">
|
||||
<div class="card-row"><span class="card-label">ID</span><span class="card-value">{{ item.org_id }}</span></div>
|
||||
<div class="card-row"><span class="card-label">名称</span><span class="card-value">{{ item.name }}</span></div>
|
||||
<div class="card-row"><span class="card-label">用户</span><span class="card-value">{{ item.user_count }}</span></div>
|
||||
<div class="card-row"><span class="card-label">选题</span><span class="card-value">{{ item.topic_count }}</span></div>
|
||||
<div class="card-actions">
|
||||
<el-button size="small" @click="showOrgDialog(item)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="deleteOrg(item.org_id)" :disabled="item.is_default">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="activeTab === 'sources'">
|
||||
<div class="toolbar">
|
||||
<el-button type="primary" size="small" @click="showSourceDialog()">新增信息源</el-button>
|
||||
<el-button size="small" @click="loadSources">刷新</el-button>
|
||||
<span style="font-size:13px;color:#909399;margin-left:8px;">共 {{ sources.length }} 个</span>
|
||||
</div>
|
||||
<div v-if="sourcesLoading" class="card-loading">加载中...</div>
|
||||
<template v-else-if="sources.length === 0">
|
||||
<div class="empty-state">
|
||||
<el-icon style="font-size:48px;color:#c0c4cc;"><IconGlobe /></el-icon>
|
||||
<div class="empty-text">暂无信息源</div>
|
||||
<el-button type="primary" size="small" @click="showSourceDialog()">新增信息源</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-table :data="sources" border stripe class="data-table" style="width:100%">
|
||||
<el-table-column prop="id" label="ID" width="50"></el-table-column>
|
||||
<el-table-column prop="name" label="名称" min-width="130"></el-table-column>
|
||||
<el-table-column prop="source_type" label="类型" width="90">
|
||||
<template #default="scope">{{ {rss:'RSS',web_search:'搜索',local:'本地'}[scope.row.source_type] || scope.row.source_type }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="query" label="查询词/URL" min-width="250" :show-overflow-tooltip="true"></el-table-column>
|
||||
<el-table-column prop="focus" label="聚焦" min-width="120"></el-table-column>
|
||||
<el-table-column prop="is_active" label="激活" width="60">
|
||||
<template #default="scope">
|
||||
<el-icon v-if="scope.row.is_active" style="color:#67C23A;"><IconCheck /></el-icon>
|
||||
<el-icon v-else style="color:#F56C6C;"><IconClose /></el-icon>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button size="small" @click="showSourceDialog(scope.row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="deleteSource(scope.row.id)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
<div class="card-list-mobile" v-if="sources.length > 0">
|
||||
<div v-for="item in sources" :key="item.id" class="card-item">
|
||||
<div class="card-row"><span class="card-label">名称</span><span class="card-value">{{ item.name }}</span></div>
|
||||
<div class="card-row"><span class="card-label">类型</span><span class="card-value">{{ item.source_type }}</span></div>
|
||||
<div class="card-row"><span class="card-label">查询</span><span class="card-value">{{ item.query || item.url }}</span></div>
|
||||
<div class="card-actions">
|
||||
<el-button size="small" @click="showSourceDialog(item)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="deleteSource(item.id)">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
@@ -267,25 +373,35 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="llmConfigDialogVisible" :title="llmConfigDialogTitle" width="700px" :close-on-click-modal="false">
|
||||
<el-form :model="llmConfigForm" label-width="100px">
|
||||
<el-dialog v-model="llmConfigDialogVisible" :title="llmConfigDialogTitle" width="750px" :close-on-click-modal="false">
|
||||
<el-form :model="llmConfigForm" label-width="110px">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12"><el-form-item label="名称"><el-input v-model="llmConfigForm.name"/></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="模型"><el-input v-model="llmConfigForm.model"/></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="名称"><el-input v-model="llmConfigForm.name" placeholder="如:default_expand"/></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="供应商">
|
||||
<el-select v-model="llmConfigForm.provider" style="width:100%">
|
||||
<el-option label="opencode-go (deepseek-v4-pro)" value="opencode-go"></el-option>
|
||||
<el-option label="nvidia (gemma-3n)" value="nvidia"></el-option>
|
||||
</el-select>
|
||||
</el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12"><el-form-item label="温度"><el-input-number v-model="llmConfigForm.temperature" :min="0" :max="2" :step="0.1"/></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="最大Token"><el-input-number v-model="llmConfigForm.max_tokens" :min="1" :max="10000"/></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="模型"><el-input v-model="llmConfigForm.model" placeholder="deepseek-v4-pro"/></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="接口地址"><el-input v-model="llmConfigForm.base_url" placeholder="https://opencode.ai/zen/go/v1"/></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="24"><el-form-item label="系统提示词"><el-input type="textarea" v-model="llmConfigForm.system_prompt" :rows="4"/></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="API Key"><el-input v-model="llmConfigForm.api_key" type="password" show-password placeholder="sk-..."/></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="温度"><el-input-number v-model="llmConfigForm.temperature" :min="0" :max="2" :step="0.1" style="width:100%"/></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12"><el-form-item label="最大Token"><el-input-number v-model="llmConfigForm.max_tokens" :min="1" :max="10000" style="width:100%"/></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="激活"><el-switch v-model="llmConfigForm.is_active"/></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="24"><el-form-item label="系统提示词"><el-input type="textarea" v-model="llmConfigForm.system_prompt" :rows="3" placeholder="你是一个专业的内容创作助手。"/></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="24"><el-form-item label="提示模板"><el-input type="textarea" v-model="llmConfigForm.user_prompt_template" :rows="4"/></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="24"><el-form-item label="激活"><el-switch v-model="llmConfigForm.is_active"/></el-form-item></el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="llmConfigDialogVisible=false">取消</el-button>
|
||||
@@ -308,8 +424,63 @@
|
||||
<el-button type="primary" @click="saveSystemConfig">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="categoryDialogVisible" :title="categoryDialogTitle" width="500px" :close-on-click-modal="false">
|
||||
<el-form :model="categoryForm" label-width="80px">
|
||||
<el-form-item label="名称"><el-input v-model="categoryForm.name" placeholder="如:循环消费"/></el-form-item>
|
||||
<el-form-item label="搜索词"><el-input v-model="categoryForm.search_query" placeholder="如:以旧换新 二手交易 闲置 2026"/></el-form-item>
|
||||
<el-form-item label="描述"><el-input type="textarea" v-model="categoryForm.description" :rows="3"/></el-form-item>
|
||||
<el-form-item label="排序"><el-input-number v-model="categoryForm.sort_order" :min="0"/></el-form-item>
|
||||
<el-form-item label="激活"><el-switch v-model="categoryForm.is_active"/></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="categoryDialogVisible=false">取消</el-button>
|
||||
<el-button type="primary" @click="saveCategory">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="sourceDialogVisible" :title="sourceDialogTitle" width="550px" :close-on-click-modal="false">
|
||||
<el-form :model="sourceForm" label-width="80px">
|
||||
<el-form-item label="名称"><el-input v-model="sourceForm.name" placeholder="如:循环消费搜索"/></el-form-item>
|
||||
<el-form-item label="类型">
|
||||
<el-select v-model="sourceForm.source_type" style="width:100%">
|
||||
<el-option label="RSS订阅" value="rss"></el-option>
|
||||
<el-option label="搜索引擎" value="web_search"></el-option>
|
||||
<el-option label="本地文件" value="local"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="查询词/URL"><el-input v-model="sourceForm.query" :placeholder="sourceForm.source_type==='web_search'?'搜索关键词':'RSS URL或本地路径'"/></el-form-item>
|
||||
<el-form-item label="聚焦领域"><el-input v-model="sourceForm.focus"/></el-form-item>
|
||||
<el-form-item label="可信度">
|
||||
<el-select v-model="sourceForm.credibility" style="width:100%">
|
||||
<el-option label="高" value="high"></el-option>
|
||||
<el-option label="中" value="medium"></el-option>
|
||||
<el-option label="低" value="low"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序"><el-input-number v-model="sourceForm.sort_order" :min="0"/></el-form-item>
|
||||
<el-form-item label="激活"><el-switch v-model="sourceForm.is_active"/></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="sourceDialogVisible=false">取消</el-button>
|
||||
<el-button type="primary" @click="saveSource">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="orgDialogVisible" :title="orgDialogTitle" width="500px" :close-on-click-modal="false">
|
||||
<el-form :model="orgForm" label-width="80px">
|
||||
<el-form-item label="组织ID"><el-input v-model="orgForm.org_id" :disabled="!!editingOrgId" placeholder="唯一标识,如:org_a"/></el-form-item>
|
||||
<el-form-item label="名称"><el-input v-model="orgForm.name" placeholder="组织名称"/></el-form-item>
|
||||
<el-form-item label="描述"><el-input type="textarea" v-model="orgForm.description" :rows="3"/></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="orgDialogVisible=false">取消</el-button>
|
||||
<el-button type="primary" @click="saveOrg">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
<script src="vue.global.prod.js"></script>
|
||||
<script src="icon-components.js"></script>
|
||||
<script src="element-plus.full.js"></script>
|
||||
<script>
|
||||
const { createApp, ref, reactive, onMounted } = Vue;
|
||||
@@ -385,7 +556,7 @@
|
||||
const llmConfigsLoading = ref(false);
|
||||
const llmConfigDialogVisible = ref(false);
|
||||
const llmConfigDialogTitle = ref('新增配置');
|
||||
const llmConfigForm = reactive({ id: null, name: '', system_prompt: '', user_prompt_template: '', temperature: 0.7, max_tokens: 2000, model: '', is_active: true });
|
||||
const llmConfigForm = reactive({ id: null, name: '', system_prompt: '', user_prompt_template: '', temperature: 0.7, max_tokens: 2000, model: '', provider: 'opencode-go', base_url: '', api_key: '', is_active: true });
|
||||
const editingLLMConfigId = ref(null);
|
||||
|
||||
const loadLLMConfigs = async () => {
|
||||
@@ -394,10 +565,18 @@
|
||||
finally { llmConfigsLoading.value = false; }
|
||||
};
|
||||
const showLLMConfigDialog = (row = null) => {
|
||||
if (row) { llmConfigDialogTitle.value = '编辑配置'; editingLLMConfigId.value = row.id; Object.assign(llmConfigForm, row); }
|
||||
else {
|
||||
if (row) {
|
||||
llmConfigDialogTitle.value = '编辑配置'; editingLLMConfigId.value = row.id;
|
||||
llmConfigForm.id = row.id; llmConfigForm.name = row.name; llmConfigForm.provider = row.provider || 'opencode-go';
|
||||
llmConfigForm.model = row.model || ''; llmConfigForm.base_url = row.base_url || ''; llmConfigForm.api_key = '';
|
||||
llmConfigForm.temperature = row.temperature ?? 0.7; llmConfigForm.max_tokens = row.max_tokens ?? 2000;
|
||||
llmConfigForm.system_prompt = row.system_prompt || ''; llmConfigForm.user_prompt_template = row.user_prompt_template || '';
|
||||
llmConfigForm.is_active = row.is_active ?? true;
|
||||
} else {
|
||||
llmConfigDialogTitle.value = '新增配置'; editingLLMConfigId.value = null;
|
||||
Object.keys(llmConfigForm).forEach(k => { if (k === 'id') llmConfigForm.id = null; else if (k === 'temperature') llmConfigForm.temperature = 0.7; else if (k === 'max_tokens') llmConfigForm.max_tokens = 2000; else if (k === 'is_active') llmConfigForm.is_active = true; else llmConfigForm[k] = ''; });
|
||||
llmConfigForm.id = null; llmConfigForm.name = ''; llmConfigForm.provider = 'opencode-go'; llmConfigForm.model = 'deepseek-v4-pro';
|
||||
llmConfigForm.base_url = 'https://opencode.ai/zen/go/v1'; llmConfigForm.api_key = ''; llmConfigForm.temperature = 0.7;
|
||||
llmConfigForm.max_tokens = 2000; llmConfigForm.system_prompt = ''; llmConfigForm.user_prompt_template = ''; llmConfigForm.is_active = true;
|
||||
}
|
||||
llmConfigDialogVisible.value = true;
|
||||
};
|
||||
@@ -420,6 +599,50 @@
|
||||
const systemConfigForm = reactive({ key: '', value: '', description: '' });
|
||||
const editingSystemConfigKey = ref(null);
|
||||
|
||||
const categories = ref([]);
|
||||
const categoriesLoading = ref(false);
|
||||
const categoryDialogVisible = ref(false);
|
||||
const categoryDialogTitle = ref('新增类别');
|
||||
const categoryForm = reactive({ name: '', search_query: '', description: '', sort_order: 0, is_active: true });
|
||||
const editingCategoryId = ref(null);
|
||||
|
||||
const sources = ref([]);
|
||||
const sourcesLoading = ref(false);
|
||||
const sourceDialogVisible = ref(false);
|
||||
const sourceDialogTitle = ref('新增信息源');
|
||||
const sourceForm = reactive({ name: '', source_type: 'web_search', query: '', credibility: 'medium', focus: '', sort_order: 0, is_active: true });
|
||||
const editingSourceId = ref(null);
|
||||
|
||||
const orgs = ref([]);
|
||||
const orgsLoading = ref(false);
|
||||
const orgDialogVisible = ref(false);
|
||||
const orgDialogTitle = ref('新增组织');
|
||||
const orgForm = reactive({ org_id: '', name: '', description: '' });
|
||||
const editingOrgId = ref(null);
|
||||
|
||||
const loadOrgs = async () => {
|
||||
orgsLoading.value = true;
|
||||
try { orgs.value = await api.get('/api/admin/orgs'); } catch (e) { ElMessage.error('加载组织失败: ' + e.message); }
|
||||
finally { orgsLoading.value = false; }
|
||||
};
|
||||
const showOrgDialog = (row = null) => {
|
||||
if (row) { orgDialogTitle.value = '编辑组织'; editingOrgId.value = row.org_id; orgForm.org_id = row.org_id; orgForm.name = row.name; orgForm.description = row.description || ''; }
|
||||
else { orgDialogTitle.value = '新增组织'; editingOrgId.value = null; orgForm.org_id = ''; orgForm.name = ''; orgForm.description = ''; }
|
||||
orgDialogVisible.value = true;
|
||||
};
|
||||
const saveOrg = async () => {
|
||||
if (!orgForm.org_id || !orgForm.name) { ElMessage.warning('请填写组织ID和名称'); return; }
|
||||
try {
|
||||
if (editingOrgId.value) { await api.put(`/api/admin/orgs/${editingOrgId.value}`, orgForm); ElMessage.success('更新成功'); }
|
||||
else { await api.post('/api/admin/orgs', orgForm); ElMessage.success('创建成功'); }
|
||||
orgDialogVisible.value = false; await loadOrgs();
|
||||
} catch (e) { ElMessage.error('保存失败: ' + e.message); }
|
||||
};
|
||||
const deleteOrg = async (orgId) => {
|
||||
try { await ElMessageBox.confirm('确定删除该组织吗?关联的用户和选题不会被删除。', '提示', { type: 'warning' }); await api.delete(`/api/admin/orgs/${orgId}`); ElMessage.success('删除成功'); await loadOrgs(); }
|
||||
catch (e) { if (e !== 'cancel') ElMessage.error('删除失败: ' + e.message); }
|
||||
};
|
||||
|
||||
const loadSystemConfigs = async () => {
|
||||
systemConfigsLoading.value = true;
|
||||
try { systemConfigs.value = await api.get('/api/admin/systemconfigs'); } catch (e) { ElMessage.error('加载系统配置失败: ' + e.message); }
|
||||
@@ -442,13 +665,59 @@
|
||||
catch (e) { if (e !== 'cancel') ElMessage.error('删除失败: ' + e.message); }
|
||||
};
|
||||
|
||||
const loadCategories = async () => {
|
||||
categoriesLoading.value = true;
|
||||
try { categories.value = await api.get('/api/admin/collector/categories'); } catch (e) { ElMessage.error('加载类别失败: ' + e.message); }
|
||||
finally { categoriesLoading.value = false; }
|
||||
};
|
||||
const showCategoryDialog = (row = null) => {
|
||||
if (row) { categoryDialogTitle.value = '编辑类别'; editingCategoryId.value = row.id; Object.assign(categoryForm, { name: row.name, search_query: row.search_query || '', description: row.description || '', sort_order: row.sort_order || 0, is_active: row.is_active }); }
|
||||
else { categoryDialogTitle.value = '新增类别'; editingCategoryId.value = null; categoryForm.name = ''; categoryForm.search_query = ''; categoryForm.description = ''; categoryForm.sort_order = 0; categoryForm.is_active = true; }
|
||||
categoryDialogVisible.value = true;
|
||||
};
|
||||
const saveCategory = async () => {
|
||||
try {
|
||||
if (editingCategoryId.value) { await api.put(`/api/admin/collector/categories/${editingCategoryId.value}`, categoryForm); ElMessage.success('更新成功'); }
|
||||
else { await api.post('/api/admin/collector/categories', categoryForm); ElMessage.success('创建成功'); }
|
||||
categoryDialogVisible.value = false; await loadCategories();
|
||||
} catch (e) { ElMessage.error('保存失败: ' + e.message); }
|
||||
};
|
||||
const deleteCategory = async (id) => {
|
||||
try { await ElMessageBox.confirm('确定删除该类别及其关联的信息源吗?', '提示', { type: 'warning' }); await api.delete(`/api/admin/collector/categories/${id}`); ElMessage.success('删除成功'); await loadCategories(); }
|
||||
catch (e) { if (e !== 'cancel') ElMessage.error('删除失败: ' + e.message); }
|
||||
};
|
||||
|
||||
const loadSources = async () => {
|
||||
sourcesLoading.value = true;
|
||||
try { sources.value = await api.get('/api/admin/collector/sources'); } catch (e) { ElMessage.error('加载信息源失败: ' + e.message); }
|
||||
finally { sourcesLoading.value = false; }
|
||||
};
|
||||
const showSourceDialog = (row = null) => {
|
||||
if (row) { sourceDialogTitle.value = '编辑信息源'; editingSourceId.value = row.id; Object.assign(sourceForm, { name: row.name, source_type: row.source_type, query: row.query || '', credibility: row.credibility || 'medium', focus: row.focus || '', sort_order: row.sort_order || 0, is_active: row.is_active }); }
|
||||
else { sourceDialogTitle.value = '新增信息源'; editingSourceId.value = null; sourceForm.name = ''; sourceForm.source_type = 'web_search'; sourceForm.query = ''; sourceForm.credibility = 'medium'; sourceForm.focus = ''; sourceForm.sort_order = 0; sourceForm.is_active = true; }
|
||||
sourceDialogVisible.value = true;
|
||||
};
|
||||
const saveSource = async () => {
|
||||
try {
|
||||
if (editingSourceId.value) { await api.put(`/api/admin/collector/sources/${editingSourceId.value}`, sourceForm); ElMessage.success('更新成功'); }
|
||||
else { await api.post('/api/admin/collector/sources', sourceForm); ElMessage.success('创建成功'); }
|
||||
sourceDialogVisible.value = false; await loadSources();
|
||||
} catch (e) { ElMessage.error('保存失败: ' + e.message); }
|
||||
};
|
||||
const deleteSource = async (id) => {
|
||||
try { await ElMessageBox.confirm('确定删除该信息源吗?', '提示', { type: 'warning' }); await api.delete(`/api/admin/collector/sources/${id}`); ElMessage.success('删除成功'); await loadSources(); }
|
||||
catch (e) { if (e !== 'cancel') ElMessage.error('删除失败: ' + e.message); }
|
||||
};
|
||||
|
||||
const logout = () => { localStorage.removeItem('authToken'); localStorage.removeItem('userRole'); localStorage.removeItem('currentUser'); window.location.href = '/login.html'; };
|
||||
|
||||
const tabLoaders = {
|
||||
cases: loadCases, tasklogs: loadTaskLogs,
|
||||
llmconfigs: loadLLMConfigs, systemconfigs: loadSystemConfigs,
|
||||
categories: loadCategories, sources: loadSources,
|
||||
orgs: loadOrgs,
|
||||
};
|
||||
const loadedTabs = new Set();
|
||||
const loadedTabs = new Set(['cases']);
|
||||
|
||||
const switchTab = (name) => {
|
||||
activeTab.value = name;
|
||||
@@ -469,14 +738,17 @@
|
||||
taskLogs, taskLogsLoading, loadTaskLogs,
|
||||
llmConfigs, llmConfigsLoading, llmConfigDialogVisible, llmConfigForm, llmConfigDialogTitle, showLLMConfigDialog, saveLLMConfig, deleteLLMConfig,
|
||||
systemConfigs, systemConfigsLoading, systemConfigDialogVisible, systemConfigForm, systemConfigDialogTitle, showSystemConfigDialog, saveSystemConfig, deleteSystemConfig,
|
||||
categories, categoriesLoading, categoryDialogVisible, categoryForm, categoryDialogTitle, showCategoryDialog, saveCategory, deleteCategory,
|
||||
sources, sourcesLoading, sourceDialogVisible, sourceForm, sourceDialogTitle, showSourceDialog, saveSource, deleteSource,
|
||||
orgs, orgsLoading, orgDialogVisible, orgForm, orgDialogTitle, showOrgDialog, saveOrg, deleteOrg,
|
||||
logout, currentUser, isAdmin, redirectToPage
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
app.use(ElementPlus);
|
||||
if (window.installNavbar) { window.installNavbar(app); }
|
||||
if (window.installNavigation) { window.installNavigation(app); } else if (window.NavigationComponent) { app.component("navigation-component", window.NavigationComponent); }
|
||||
if (window.installIcons) { window.installIcons(app); }
|
||||
if (window.installUniNav) { window.installUniNav(app); }
|
||||
app.mount('#app');
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user