feat: 内容数据迁移至数据库,合规审查全链路打通

- 文章 HTML 存储从文件系统迁移至 articles 表,删除 releases 目录
- 合规审查从 DB 读取 HTML,审查结果写回 DB,通过后自动推进至待发布
- 新增 todayCount 筛选按钮,与系统概览统计数据一致
- 全屏预览修复:提升 z-index 超过侧边栏,添加退出全屏/关闭按钮
- 统一 '优化' → '审查' 命名,消除前后端术语不一致
- 调度器创作完成后自动触发审查(生成 → 审查 → 待发布)
- 清理旧备份/调试文件、过期大纲和研究笔记
This commit is contained in:
Yuzhiran Dev
2026-05-13 17:33:56 +08:00
parent bc6a302e59
commit 233e23016c
234 changed files with 5670 additions and 10651 deletions
+249 -144
View File
@@ -5,29 +5,31 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>宇之然内容创作平台 - 系统管理</title>
<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; }
.navbar { background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%); color: white; padding: 16px 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
.navbar-content { display: flex; justify-content: space-between; align-items: center; max-width: 1400px; margin: 0 auto; }
.navbar-title { font-size: 20px; font-weight: 600; }
.navbar-user { display: flex; align-items: center; gap: 16px; }
.user-info { display: flex; align-items: center; gap: 8px; }
.avatar { width: 32px; height: 32px; border-radius: 50%; background: rgba(255,255,255,0.2); display: flex; align-items: center; justify-content: center; font-size: 14px; }
.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: 12px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); overflow-x: auto; }
.mobile-nav { display: none; position: fixed; bottom: 0; left: 0; right: 0; background: white; box-shadow: 0 -2px 8px rgba(0,0,0,0.1); padding: 8px 0; z-index: 1000; }
.mobile-nav-btn { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 2px; background: none; border: none; font-size: 10px; color: #909399; cursor: pointer; padding: 4px; }
.mobile-nav-btn.active { color: #409eff; }
.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) {
.mobile-nav { display: flex; }
.content-area { padding: 16px; padding-bottom: 80px; }
.card { padding: 16px; }
.data-table { display: none; }
@@ -46,6 +48,9 @@
.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; }
}
</style>
<script src="navigation-component.js"></script>
@@ -57,18 +62,39 @@
<navigation-component current-page="admin" :is-admin="isAdmin" @navigate="redirectToPage"></navigation-component>
<div class="main-content">
<main class="content-area">
<div class="card">
<el-tabs v-model="activeTab">
<el-tab-pane label="案例管理" name="cases">
<div class="toolbar">
<el-button type="primary" @click="showCaseDialog()">新增案例</el-button>
<div class="card page-fade">
<div class="page-header">
<h2 class="page-title">⚙️ 系统管理</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>
</div>
</div>
<div v-if="activeTab === 'cases'">
<div class="toolbar">
<el-button type="primary" size="small" @click="showCaseDialog()">新增案例</el-button>
</div>
<div v-if="casesLoading" class="card-loading">加载中...</div>
<template v-else-if="cases.length === 0">
<div class="empty-state">
<div class="empty-icon">📋</div>
<div class="empty-text">暂无案例数据</div>
<el-button type="primary" size="small" @click="showCaseDialog()">新增第一个案例</el-button>
</div>
<el-table :data="cases" border stripe class="data-table">
<el-table-column prop="id" label="ID" width="70"/>
<el-table-column prop="title" label="标题" min-width="150"/>
<el-table-column prop="field" label="领域" width="100"/>
<el-table-column prop="summary" label="概述" min-width="200" :show-overflow-tooltip="true"/>
<el-table-column prop="source" label="来源" width="100"/>
</template>
<template v-else>
<div class="stat-summary">
<div class="stat-item"><span class="num">{{ cases.length }}</span><span class="label">总案例</span></div>
</div>
<el-table :data="cases" border stripe class="data-table" style="width:100%">
<el-table-column prop="id" label="ID" width="70"></el-table-column>
<el-table-column prop="title" label="标题" min-width="120" :show-overflow-tooltip="true"></el-table-column>
<el-table-column prop="field" label="领域" width="100"></el-table-column>
<el-table-column prop="summary" label="概述" min-width="200" :show-overflow-tooltip="true"></el-table-column>
<el-table-column prop="source" label="来源" width="100"></el-table-column>
<el-table-column label="操作" width="140" fixed="right">
<template #default="scope">
<el-button size="small" @click="showCaseDialog(scope.row)">编辑</el-button>
@@ -76,52 +102,73 @@
</template>
</el-table-column>
</el-table>
<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">
<el-button size="small" @click="showCaseDialog(item)">编辑</el-button>
<el-button size="small" type="danger" @click="deleteCase(item.id)">删除</el-button>
</div>
</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">
<el-button size="small" @click="showCaseDialog(item)">编辑</el-button>
<el-button size="small" type="danger" @click="deleteCase(item.id)">删除</el-button>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="任务日志" name="tasklogs">
<div class="toolbar">
<el-button @click="loadTaskLogs()">刷新</el-button>
</div>
</div>
<div v-if="activeTab === 'tasklogs'">
<div class="toolbar">
<el-button size="small" @click="loadTaskLogs()">刷新</el-button>
</div>
<div v-if="taskLogsLoading" class="card-loading">加载中...</div>
<template v-else-if="taskLogs.length === 0">
<div class="empty-state">
<div class="empty-icon">📝</div>
<div class="empty-text">暂无任务日志</div>
</div>
<el-table :data="taskLogs" border stripe class="data-table">
<el-table-column prop="id" label="ID" width="70"/>
<el-table-column prop="task_name" label="任务名称" min-width="120"/>
<el-table-column prop="topic_id" label="选题" width="100"/>
<el-table-column prop="status" label="状态" width="80"/>
<el-table-column prop="message" label="消息" min-width="150" :show-overflow-tooltip="true"/>
<el-table-column prop="started_at" label="开始" width="150"/>
<el-table-column prop="finished_at" label="结束" width="150"/>
<el-table-column prop="duration" label="耗时" width="80"/>
</template>
<template v-else>
<el-table :data="taskLogs" border stripe class="data-table" style="width:100%">
<el-table-column prop="id" label="ID" width="70"></el-table-column>
<el-table-column prop="task_name" label="任务名称" min-width="120"></el-table-column>
<el-table-column prop="topic_id" label="选题" width="100"></el-table-column>
<el-table-column prop="status" label="状态" width="80"></el-table-column>
<el-table-column prop="message" label="消息" min-width="150" :show-overflow-tooltip="true"></el-table-column>
<el-table-column prop="started_at" label="开始" width="150"></el-table-column>
<el-table-column prop="finished_at" label="结束" width="150"></el-table-column>
<el-table-column prop="duration" label="耗时" width="80"></el-table-column>
</el-table>
<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>
</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>
</el-tab-pane>
<el-tab-pane label="LLM配置" name="llmconfigs">
<div class="toolbar">
<el-button type="primary" @click="showLLMConfigDialog()">新增配置</el-button>
</div>
</div>
<div v-if="activeTab === 'llmconfigs'">
<div class="toolbar">
<el-button type="primary" size="small" @click="showLLMConfigDialog()">新增配置</el-button>
</div>
<div v-if="llmConfigsLoading" class="card-loading">加载中...</div>
<template v-else-if="llmConfigs.length === 0">
<div class="empty-state">
<div class="empty-icon">🤖</div>
<div class="empty-text">暂无 LLM 配置</div>
<el-button type="primary" size="small" @click="showLLMConfigDialog()">新增配置</el-button>
</div>
<el-table :data="llmConfigs" border stripe class="data-table">
<el-table-column prop="id" label="ID" width="70"/>
<el-table-column prop="name" label="名称" min-width="120"/>
<el-table-column prop="model" label="模型" min-width="180"/>
<el-table-column prop="temperature" label="温度" width="80"/>
<el-table-column prop="max_tokens" label="最大Token" width="110"/>
</template>
<template v-else>
<el-table :data="llmConfigs" border stripe class="data-table" style="width:100%">
<el-table-column prop="id" label="ID" width="70"></el-table-column>
<el-table-column prop="name" label="名称" min-width="120"></el-table-column>
<el-table-column prop="model" label="模型" min-width="180"></el-table-column>
<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>
</el-table-column>
@@ -132,31 +179,42 @@
</template>
</el-table-column>
</el-table>
<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">
<el-button size="small" @click="showLLMConfigDialog(item)">编辑</el-button>
<el-button size="small" type="danger" @click="deleteLLMConfig(item.id)">删除</el-button>
</div>
</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">
<el-button size="small" @click="showLLMConfigDialog(item)">编辑</el-button>
<el-button size="small" type="danger" @click="deleteLLMConfig(item.id)">删除</el-button>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="系统配置" name="systemconfigs">
<div class="toolbar">
<el-button type="primary" @click="showSystemConfigDialog()">新增配置</el-button>
</div>
</div>
<div v-if="activeTab === 'systemconfigs'">
<div class="toolbar">
<el-button type="primary" size="small" @click="showSystemConfigDialog()">新增配置</el-button>
</div>
<div v-if="systemConfigsLoading" class="card-loading">加载中...</div>
<template v-else-if="systemConfigs.length === 0">
<div class="empty-state">
<div class="empty-icon">⚙️</div>
<div class="empty-text">暂无系统配置</div>
<el-button type="primary" size="small" @click="showSystemConfigDialog()">新增配置</el-button>
</div>
<el-table :data="systemConfigs" border stripe class="data-table">
<el-table-column prop="key" label="键" min-width="180"/>
</template>
<template v-else>
<el-table :data="systemConfigs" border stripe class="data-table" style="width:100%">
<el-table-column prop="key" label="键" min-width="180"></el-table-column>
<el-table-column prop="value" label="值" min-width="250">
<template #default="scope">
{{ typeof scope.row.value === 'object' ? JSON.stringify(scope.row.value) : scope.row.value }}
</template>
</el-table-column>
<el-table-column prop="description" label="描述" min-width="200"/>
<el-table-column prop="description" label="描述" min-width="200"></el-table-column>
<el-table-column label="操作" width="140" fixed="right">
<template #default="scope">
<el-button size="small" @click="showSystemConfigDialog(scope.row)">编辑</el-button>
@@ -164,34 +222,44 @@
</template>
</el-table-column>
</el-table>
<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">
<el-button size="small" @click="showSystemConfigDialog(item)">编辑</el-button>
<el-button size="small" type="danger" @click="deleteSystemConfig(item.key)">删除</el-button>
</div>
</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">
<el-button size="small" @click="showSystemConfigDialog(item)">编辑</el-button>
<el-button size="small" type="danger" @click="deleteSystemConfig(item.key)">删除</el-button>
</div>
</div>
</el-tab-pane>
</el-tabs>
</div>
</div>
</div>
</main>
</div>
<el-dialog v-model="caseDialogVisible" :title="caseDialogTitle" width="600px">
<el-dialog v-model="caseDialogVisible" :title="caseDialogTitle" width="700px" :close-on-click-modal="false">
<el-form :model="caseForm" label-width="80px">
<el-form-item label="标题"><el-input v-model="caseForm.title"/></el-form-item>
<el-form-item label="领域"><el-input v-model="caseForm.field"/></el-form-item>
<el-form-item label="概述"><el-input type="textarea" v-model="caseForm.summary"/></el-form-item>
<el-form-item label="关键指标"><el-input v-model="caseForm.key_metrics"/></el-form-item>
<el-form-item label="日期"><el-input v-model="caseForm.date"/></el-form-item>
<el-form-item label="来源"><el-input v-model="caseForm.source"/></el-form-item>
<el-form-item label="来源URL"><el-input v-model="caseForm.source_url"/></el-form-item>
<el-form-item label="可信度"><el-input v-model="caseForm.credibility_rating"/></el-form-item>
<el-form-item label="国内适用性"><el-input v-model="caseForm.china_applicability"/></el-form-item>
<el-row :gutter="16">
<el-col :span="12"><el-form-item label="标题"><el-input v-model="caseForm.title"/></el-form-item></el-col>
<el-col :span="12"><el-form-item label="领域"><el-input v-model="caseForm.field"/></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="caseForm.summary" :rows="3"/></el-form-item></el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12"><el-form-item label="关键指标"><el-input v-model="caseForm.key_metrics"/></el-form-item></el-col>
<el-col :span="12"><el-form-item label="日期"><el-input v-model="caseForm.date"/></el-form-item></el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12"><el-form-item label="来源"><el-input v-model="caseForm.source"/></el-form-item></el-col>
<el-col :span="12"><el-form-item label="来源URL"><el-input v-model="caseForm.source_url"/></el-form-item></el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12"><el-form-item label="可信度"><el-input v-model="caseForm.credibility_rating"/></el-form-item></el-col>
<el-col :span="12"><el-form-item label="国内适用性"><el-input v-model="caseForm.china_applicability"/></el-form-item></el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="caseDialogVisible=false">取消</el-button>
@@ -199,17 +267,25 @@
</template>
</el-dialog>
<el-dialog v-model="llmConfigDialogVisible" :title="llmConfigDialogTitle" width="600px">
<el-form :model="llmConfigForm" label-width="120px">
<el-form-item label="名称"><el-input v-model="llmConfigForm.name"/></el-form-item>
<el-form-item label="系统提示词"><el-input type="textarea" v-model="llmConfigForm.system_prompt"/></el-form-item>
<el-form-item label="用户提示模板"><el-input type="textarea" v-model="llmConfigForm.user_prompt_template"/></el-form-item>
<el-form-item label="温度"><el-input-number v-model="llmConfigForm.temperature" :min="0" :max="2" :step="0.1"/></el-form-item>
<el-form-item label="最大Token"><el-input-number v-model="llmConfigForm.max_tokens" :min="1" :max="10000"/></el-form-item>
<el-form-item label="模型"><el-input v-model="llmConfigForm.model"/></el-form-item>
<el-form-item label="激活">
<el-switch v-model="llmConfigForm.is_active"/>
</el-form-item>
<el-dialog v-model="llmConfigDialogVisible" :title="llmConfigDialogTitle" width="700px" :close-on-click-modal="false">
<el-form :model="llmConfigForm" label-width="100px">
<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-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-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-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>
@@ -217,11 +293,15 @@
</template>
</el-dialog>
<el-dialog v-model="systemConfigDialogVisible" :title="systemConfigDialogTitle" width="500px">
<el-form :model="systemConfigForm" label-width="100px">
<el-form-item label="键"><el-input v-model="systemConfigForm.key" :disabled="!!editingSystemConfigKey"/></el-form-item>
<el-form-item label=""><el-input v-model="systemConfigForm.value" type="textarea"/></el-form-item>
<el-form-item label="描述"><el-input v-model="systemConfigForm.description"/></el-form-item>
<el-dialog v-model="systemConfigDialogVisible" :title="systemConfigDialogTitle" width="600px" :close-on-click-modal="false">
<el-form :model="systemConfigForm" label-width="80px">
<el-row :gutter="16">
<el-col :span="12"><el-form-item label=""><el-input v-model="systemConfigForm.key" :disabled="!!editingSystemConfigKey"/></el-form-item></el-col>
<el-col :span="12"><el-form-item label="描述"><el-input v-model="systemConfigForm.description"/></el-form-item></el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="24"><el-form-item label="值"><el-input v-model="systemConfigForm.value" type="textarea" :rows="4"/></el-form-item></el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="systemConfigDialogVisible=false">取消</el-button>
@@ -232,7 +312,7 @@
<script src="vue.global.prod.js"></script>
<script src="element-plus.full.js"></script>
<script>
const { createApp, ref, reactive, onMounted, watch } = Vue;
const { createApp, ref, reactive, onMounted } = Vue;
const { ElMessage, ElMessageBox } = ElementPlus;
const app = createApp({
@@ -240,38 +320,42 @@
const token = localStorage.getItem('authToken');
if (!token) { window.location.href = 'login.html'; return {}; }
const apiBase = '';
const api = {
get: (url) => fetch(url, { headers: { 'Authorization': `Bearer ${token}` } }).then(r => { if (!r.ok) throw new Error(r.statusText); return r.json(); }),
post: (url, body) => fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify(body) }).then(r => { if (!r.ok) throw new Error(r.statusText); return r.json(); }),
put: (url, body) => fetch(url, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify(body) }).then(r => { if (!r.ok) throw new Error(r.statusText); return r.json(); }),
delete: (url) => fetch(url, { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}` } }).then(r => { if (!r.ok) throw new Error(r.statusText); return r.json(); }),
get: (url) => fetch(apiBase + url, { headers: { 'Authorization': `Bearer ${token}` } }).then(r => { if (!r.ok) throw new Error(r.statusText); return r.json(); }),
post: (url, body) => fetch(apiBase + url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify(body) }).then(r => { if (!r.ok) throw new Error(r.statusText); return r.json(); }),
put: (url, body) => fetch(apiBase + url, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify(body) }).then(r => { if (!r.ok) throw new Error(r.statusText); return r.json(); }),
delete: (url) => fetch(apiBase + url, { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}` } }).then(r => { if (!r.ok) throw new Error(r.statusText); return r.json(); }),
};
const activeTab = ref('cases');
const currentUser = ref({ username: '' });
const isAdmin = ref(false);
const isLoggedIn = ref(false);
fetch('/api/auth/me', { headers: { 'Authorization': 'Bearer ' + token } })
.then(response => response.ok ? response.json() : Promise.reject())
.then(r => r.ok ? r.json() : Promise.reject())
.then(data => {
const user = data.user || { username: '', role: 'user' };
currentUser.value = user;
isAdmin.value = user.role === 'admin';
isLoggedIn.value = true;
if (!isAdmin.value) { ElMessage.warning('需要管理员权限'); window.location.href = '/login.html'; }
})
.catch(() => { localStorage.removeItem('authToken'); localStorage.removeItem('userRole'); localStorage.removeItem('currentUser'); window.location.href = '/login.html'; });
.catch(() => { localStorage.removeItem('authToken'); window.location.href = '/login.html'; });
const redirectToPage = (page) => { window.location.href = '/' + page; };
const cases = ref([]);
const casesLoading = ref(false);
const caseDialogVisible = ref(false);
const caseDialogTitle = ref('新增案例');
const caseForm = reactive({ id: null, title: '', field: '', summary: '', key_metrics: '', date: '', source: '', source_url: '', credibility_rating: '', china_applicability: '' });
const editingCaseId = ref(null);
const loadCases = async () => { try { cases.value = await api.get('/api/admin/cases'); } catch (e) { ElMessage.error('加载案例失败: ' + e.message); } };
const loadCases = async () => {
casesLoading.value = true;
try { cases.value = await api.get('/api/admin/cases'); } catch (e) { ElMessage.error('加载案例失败: ' + e.message); }
finally { casesLoading.value = false; }
};
const showCaseDialog = (row = null) => {
if (row) { caseDialogTitle.value = '编辑案例'; editingCaseId.value = row.id; Object.assign(caseForm, row); }
else { caseDialogTitle.value = '新增案例'; editingCaseId.value = null; Object.keys(caseForm).forEach(k => { if (k === 'id') caseForm.id = null; else caseForm[k] = ''; }); }
@@ -290,15 +374,25 @@
};
const taskLogs = ref([]);
const loadTaskLogs = async () => { try { taskLogs.value = await api.get('/api/admin/tasklogs'); } catch (e) { ElMessage.error('加载任务日志失败: ' + e.message); } };
const taskLogsLoading = ref(false);
const loadTaskLogs = async () => {
taskLogsLoading.value = true;
try { taskLogs.value = await api.get('/api/admin/tasklogs'); } catch (e) { ElMessage.error('加载任务日志失败: ' + e.message); }
finally { taskLogsLoading.value = false; }
};
const llmConfigs = ref([]);
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 editingLLMConfigId = ref(null);
const loadLLMConfigs = async () => { try { llmConfigs.value = await api.get('/api/admin/llmconfigs'); } catch (e) { ElMessage.error('加载LLM配置失败: ' + e.message); } };
const loadLLMConfigs = async () => {
llmConfigsLoading.value = true;
try { llmConfigs.value = await api.get('/api/admin/llmconfigs'); } catch (e) { ElMessage.error('加载LLM配置失败: ' + e.message); }
finally { llmConfigsLoading.value = false; }
};
const showLLMConfigDialog = (row = null) => {
if (row) { llmConfigDialogTitle.value = '编辑配置'; editingLLMConfigId.value = row.id; Object.assign(llmConfigForm, row); }
else {
@@ -320,12 +414,17 @@
};
const systemConfigs = ref([]);
const systemConfigsLoading = ref(false);
const systemConfigDialogVisible = ref(false);
const systemConfigDialogTitle = ref('新增配置');
const systemConfigForm = reactive({ key: '', value: '', description: '' });
const editingSystemConfigKey = ref(null);
const loadSystemConfigs = async () => { try { systemConfigs.value = await api.get('/api/admin/systemconfigs'); } catch (e) { 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); }
finally { systemConfigsLoading.value = false; }
};
const showSystemConfigDialog = (row = null) => {
if (row) { systemConfigDialogTitle.value = '编辑配置'; editingSystemConfigKey.value = row.key; systemConfigForm.key = row.key; systemConfigForm.value = (typeof row.value === 'object') ? JSON.stringify(row.value) : (row.value || ''); systemConfigForm.description = row.description || ''; }
else { systemConfigDialogTitle.value = '新增配置'; editingSystemConfigKey.value = null; systemConfigForm.key = ''; systemConfigForm.value = ''; systemConfigForm.description = ''; }
@@ -345,26 +444,32 @@
const logout = () => { localStorage.removeItem('authToken'); localStorage.removeItem('userRole'); localStorage.removeItem('currentUser'); window.location.href = '/login.html'; };
watch(activeTab, (t) => {
if (t === 'cases' && !cases.value.length) loadCases();
if (t === 'tasklogs' && !taskLogs.value.length) loadTaskLogs();
if (t === 'llmconfigs' && !llmConfigs.value.length) loadLLMConfigs();
if (t === 'systemconfigs' && !systemConfigs.value.length) loadSystemConfigs();
});
const tabLoaders = {
cases: loadCases, tasklogs: loadTaskLogs,
llmconfigs: loadLLMConfigs, systemconfigs: loadSystemConfigs,
};
const loadedTabs = new Set();
const switchTab = (name) => {
activeTab.value = name;
if (!loadedTabs.has(name)) {
loadedTabs.add(name);
tabLoaders[name]();
}
};
onMounted(() => {
if (activeTab.value === 'cases') loadCases();
else if (activeTab.value === 'tasklogs') loadTaskLogs();
else if (activeTab.value === 'llmconfigs') loadLLMConfigs();
else if (activeTab.value === 'systemconfigs') loadSystemConfigs();
loadedTabs.add('cases');
loadCases();
});
return {
activeTab, cases, caseDialogVisible, caseForm, caseDialogTitle, showCaseDialog, saveCase, deleteCase,
taskLogs, loadTaskLogs,
llmConfigs, llmConfigDialogVisible, llmConfigForm, llmConfigDialogTitle, showLLMConfigDialog, saveLLMConfig, deleteLLMConfig,
systemConfigs, systemConfigDialogVisible, systemConfigForm, systemConfigDialogTitle, showSystemConfigDialog, saveSystemConfig, deleteSystemConfig,
logout, currentUser, isAdmin, isLoggedIn, redirectToPage
activeTab, switchTab,
cases, casesLoading, caseDialogVisible, caseForm, caseDialogTitle, showCaseDialog, saveCase, deleteCase,
taskLogs, taskLogsLoading, loadTaskLogs,
llmConfigs, llmConfigsLoading, llmConfigDialogVisible, llmConfigForm, llmConfigDialogTitle, showLLMConfigDialog, saveLLMConfig, deleteLLMConfig,
systemConfigs, systemConfigsLoading, systemConfigDialogVisible, systemConfigForm, systemConfigDialogTitle, showSystemConfigDialog, saveSystemConfig, deleteSystemConfig,
logout, currentUser, isAdmin, redirectToPage
};
}
});