Files
yu-zhi-ran/platform/frontend/admin.html
T
Yuzhiran Dev 1855f190f5 配置全面迁移数据库:PromptConfig、TaskConfig动态调度、敏感词/清洗规则/趋势映射/平台标签/痛点模板全部可编辑
- 新增 PromptConfig 模型 + API,支持提示词在线编辑(16条默认)
- 调度器动态读取 TaskConfig.schedule,admin 可调执行时间
- 新增 KeywordDomainMap、SensitiveWord、ContentCleanRule、TrendFieldMapping 表
- DOMAINS、TREND_DOMAIN_MAP、PLATFORM_TAGS、china_pains、RSS关键词、priority_weights 全部迁移到 DB
- tasks.html 重构:卡片网格+配置/产出/历史/提示词四个Tab,折叠显示
- 清理冗余代码:DEFAULT_PROMPTS死代码、collector.py unreachable代码、compliance_checker bug
- strip_thinking_html 改用 DB 规则优先
2026-05-22 11:18:23 +08:00

786 lines
58 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<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>
.data-table { display: block; }
@media (max-width: 768px) { .data-table { display: none; } }
</style>
<script src="uni-nav.js"></script>
</head>
<body>
<div id="app">
<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"><el-icon style="vertical-align:-2px;"><IconSetting /></el-icon> 系统管理</h2>
<div class="filter-bar">
<el-button size="default" :type="activeTab === 'users' ? 'primary' : ''" @click="switchTab('users')">用户管理</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 === 'platformconfigs' ? 'primary' : ''" @click="switchTab('platformconfigs')">平台配置</el-button>
<el-button size="default" :type="activeTab === 'systemconfigs' ? 'primary' : ''" @click="switchTab('systemconfigs')">系统配置</el-button>
<el-button size="default" :type="activeTab === 'orgs' ? 'primary' : ''" @click="switchTab('orgs')">组织管理</el-button>
<el-button size="default" :type="activeTab === 'logs' ? 'primary' : ''" @click="switchTab('logs')">运行日志</el-button>
<el-button size="default" :type="activeTab === 'assistant' ? 'primary' : ''" @click="switchTab('assistant')">AI 助手</el-button>
</div>
</div>
<div v-if="activeTab === 'users'">
<div class="toolbar">
<el-button type="primary" size="small" @click="addUser">新增用户</el-button>
<span style="font-size:13px;color:#909399;margin-left:8px;">共 {{ users.length }} 个</span>
</div>
<div v-if="usersLoading" class="card-loading">加载中...</div>
<template v-else-if="users.length === 0">
<div class="empty-state">
<el-icon style="font-size:48px;color:#c0c4cc;"><IconUser /></el-icon>
<div class="empty-text">暂无用户数据</div>
</div>
</template>
<template v-else>
<el-table :data="paginatedUsers" border stripe class="data-table" style="width:100%">
<el-table-column prop="id" label="ID" width="70"></el-table-column>
<el-table-column prop="username" label="用户名" min-width="120"></el-table-column>
<el-table-column prop="role" label="角色" width="80"></el-table-column>
<el-table-column label="操作" width="120" fixed="right">
<template #default="scope">
<el-button size="small" type="danger" @click="deleteUser(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div v-if="users.length > userPageSize" style="display:flex;justify-content:center;margin:12px 0;">
<el-pagination background layout="prev, pager, next" :total="users.length" :page-size="userPageSize" :current-page="userPage" @current-change="userPage = $event"></el-pagination>
</div>
</template>
<div class="card-list-mobile">
<div v-for="item in paginatedUsers" :key="item.id" class="card-item">
<div class="card-row"><span class="card-label">用户名</span><span class="card-value">{{ item.username }}</span></div>
<div class="card-row"><span class="card-label">角色</span><span class="card-value">{{ item.role }}</span></div>
<div class="card-actions">
<el-button size="small" type="danger" @click="deleteUser(item.id)">删除</el-button>
</div>
</div>
</div>
<el-dialog v-model="userDialogVisible" title="新增用户" width="450px" :close-on-click-modal="false">
<el-form :model="userForm" label-width="80px">
<el-form-item label="用户名"><el-input v-model="userForm.username" placeholder="至少2个字符"/></el-form-item>
<el-form-item label="密码"><el-input v-model="userForm.password" type="password" show-password placeholder="至少6位"/></el-form-item>
<el-form-item label="角色">
<el-select v-model="userForm.role" style="width:100%">
<el-option label="编辑" value="editor"></el-option>
<el-option label="管理员" value="admin"></el-option>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="userDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitUser" :loading="userSubmitting">创建</el-button>
</template>
</el-dialog>
</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">
<el-icon style="font-size:48px;color:#c0c4cc;"><IconDocument /></el-icon>
<div class="empty-text">暂无任务日志</div>
</div>
</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>
</template>
<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>
<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">
<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>
</template>
<template v-else>
<el-table :data="llmConfigs" 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="100"></el-table-column>
<el-table-column prop="provider" label="供应商" width="110"></el-table-column>
<el-table-column prop="model" label="模型" min-width="160"></el-table-column>
<el-table-column prop="base_url" label="接口地址" min-width="200">
<template #default="scope">{{ scope.row.base_url ? scope.row.base_url.replace(/^https?:\/\//, '') : '-' }}</template>
</el-table-column>
<el-table-column prop="temperature" label="温度" width="70"></el-table-column>
<el-table-column prop="max_tokens" label="最大Token" width="100"></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="系统提示词" min-width="150">
<template #default="scope">{{ (scope.row.system_prompt || '').slice(0, 30) }}{{ (scope.row.system_prompt || '').length > 30 ? '...' : '' }}</template>
</el-table-column>
<el-table-column label="操作" width="130" fixed="right">
<template #default="scope">
<el-button size="small" @click="showLLMConfigDialog(scope.row)">编辑</el-button>
<el-button size="small" type="danger" @click="deleteLLMConfig(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
</template>
<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.provider }}</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">温度/最大Token</span><span class="card-value">{{ item.temperature }} / {{ item.max_tokens }}</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>
</div>
</div>
</div>
<div v-if="activeTab === 'platformconfigs'">
<div class="page-header">
<h2 class="page-title"><el-icon style="vertical-align:-2px;"><IconGlobe /></el-icon> 平台配置</h2>
<div class="toolbar">
<el-button-group>
<el-button :type="pcShowActiveOnly ? 'primary' : ''" @click="pcShowActiveOnly = true; loadPlatformConfigs()">启用中</el-button>
<el-button :type="!pcShowActiveOnly ? 'primary' : ''" @click="pcShowActiveOnly = false; loadPlatformConfigs()">全部</el-button>
</el-button-group>
<el-button type="primary" size="small" @click="showPcDialogFn()">新增配置</el-button>
</div>
</div>
<div v-if="platformConfigsLoading" class="card-loading">加载中...</div>
<template v-else-if="!platformConfigs || platformConfigs.length === 0">
<div class="empty-state">
<el-icon style="font-size:48px;color:#c0c4cc;"><IconDocument /></el-icon>
<div class="empty-text">暂无平台配置</div>
</div>
</template>
<el-table v-else :data="platformConfigs" border stripe style="width:100%">
<el-table-column prop="platform" label="标识" width="120"></el-table-column>
<el-table-column prop="name" label="名称" width="120"></el-table-column>
<el-table-column prop="icon" label="图标" width="100">
<template #default="scope"><span>{{ scope.row.icon || '—' }}</span></template>
</el-table-column>
<el-table-column prop="website_url" label="平台网址" min-width="160">
<template #default="scope"><a v-if="scope.row.website_url" :href="scope.row.website_url" target="_blank" style="color:#409eff;">{{ scope.row.website_url }}</a><span v-else></span></template>
</el-table-column>
<el-table-column prop="api_endpoint" label="API地址" min-width="160">
<template #default="scope"><span style="color:#606266;">{{ scope.row.api_endpoint || '—' }}</span></template>
</el-table-column>
<el-table-column label="字数范围" width="120">
<template #default="scope">{{ scope.row.min_words }} - {{ scope.row.max_words }}</template>
</el-table-column>
<el-table-column label="配图" width="120">
<template #default="scope">{{ scope.row.requires_image ? `${scope.row.image_count_min}-${scope.row.image_count_max}张` : '不需要' }}</template>
</el-table-column>
<el-table-column prop="default_format" label="默认格式" min-width="180">
<template #default="scope">{{ (scope.row.default_format || '').slice(0, 50) }}{{ (scope.row.default_format || '').length > 50 ? '...' : '' }}</template>
</el-table-column>
<el-table-column prop="is_active" label="激活" width="70">
<template #default="scope"><span>{{ scope.row.is_active ? '是' : '否' }}</span></template>
</el-table-column>
<el-table-column label="操作" width="80" fixed="right">
<template #default="scope">
<el-button size="small" @click="showPcDialogFn(scope.row)">编辑</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog v-model="showPcDialog" :title="pcDialogTitle" width="750px" :close-on-click-modal="false">
<el-form :model="pcForm" label-width="100px">
<el-row :gutter="16">
<el-col :span="12"><el-form-item label="标识"><el-input v-model="pcForm.platform" placeholder="唯一标识,如 weixin" :disabled="!!editingPcPlatform"/></el-form-item></el-col>
<el-col :span="12"><el-form-item label="名称"><el-input v-model="pcForm.name" placeholder="微信公众号"/></el-form-item></el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12"><el-form-item label="图标"><el-input v-model="pcForm.icon" placeholder="IconWechat"/></el-form-item></el-col>
<el-col :span="12"><el-form-item label="官网"><el-input v-model="pcForm.website_url" placeholder="https://mp.weixin.qq.com"/></el-form-item></el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="24"><el-form-item label="API地址"><el-input v-model="pcForm.api_endpoint" placeholder="https://..."/></el-form-item></el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12"><el-form-item label="最少字数"><el-input-number v-model="pcForm.min_words" :min="0" :max="99999" style="width:100%"/></el-form-item></el-col>
<el-col :span="12"><el-form-item label="最多字数"><el-input-number v-model="pcForm.max_words" :min="0" :max="99999" style="width:100%"/></el-form-item></el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="8"><el-form-item label="需要配图"><el-switch v-model="pcForm.requires_image"/></el-form-item></el-col>
<el-col :span="8"><el-form-item label="最少张数"><el-input-number v-model="pcForm.image_count_min" :min="0" :max="99" style="width:100%"/></el-form-item></el-col>
<el-col :span="8"><el-form-item label="最多张数"><el-input-number v-model="pcForm.image_count_max" :min="0" :max="99" style="width:100%"/></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="pcForm.default_format" :rows="4" placeholder="文章格式要求"/></el-form-item></el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12"><el-form-item label="激活"><el-switch v-model="pcForm.is_active"/></el-form-item></el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="showPcDialog = false">取消</el-button>
<el-button type="primary" @click="savePcForm">确定</el-button>
</template>
</el-dialog>
</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">
<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>
</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>
<el-table-column label="操作" width="140" fixed="right">
<template #default="scope">
<el-button size="small" @click="showSystemConfigDialog(scope.row)">编辑</el-button>
<el-button size="small" type="danger" @click="deleteSystemConfig(scope.row.key)">删除</el-button>
</template>
</el-table-column>
</el-table>
</template>
<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 === '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="paginatedOrgs" 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>
<div v-if="orgs.length > orgPageSize" style="display:flex;justify-content:center;margin:12px 0;">
<el-pagination background layout="prev, pager, next" :total="orgs.length" :page-size="orgPageSize" :current-page="orgPage" @current-change="orgPage = $event"></el-pagination>
</div>
</template>
<div class="card-list-mobile" v-if="orgs.length > 0">
<div v-for="item in paginatedOrgs" :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 === 'logs'">
<div class="toolbar">
<el-select v-model="logType" placeholder="日志类型" style="width:200px;">
<el-option label="创作日志" value="creator"></el-option>
<el-option label="审查日志" value="optimizer"></el-option>
<el-option label="研究日志" value="research"></el-option>
<el-option label="大纲日志" value="outline"></el-option>
<el-option label="写作日志" value="writer"></el-option>
<el-option label="发布日志" value="publisher"></el-option>
<el-option label="收集日志" value="collector"></el-option>
<el-option label="通知日志" value="notifier"></el-option>
<el-option label="趋势日志" value="trends"></el-option>
</el-select>
<el-date-picker v-model="logDate" type="date" placeholder="选择日期" format="YYYY-MM-DD" value-format="YYYY-MM-DD" style="width:200px;"></el-date-picker>
<el-button type="primary" @click="fetchLogs" :loading="logsLoading">加载日志</el-button>
</div>
<div v-if="logsLoading" class="card-loading">加载中...</div>
<div v-else-if="!logContent" class="empty-state">
<el-icon style="font-size:48px;color:#c0c4cc;"><IconDocument /></el-icon>
<div class="empty-text">选择日志类型和日期后点击加载</div>
</div>
<div v-else class="log-container" style="max-height:600px;overflow-y:auto;background:#f9fafb;border:1px solid #e5e7eb;border-radius:8px;">
<pre style="margin:0;padding:16px;white-space:pre-wrap;word-wrap:break-word;font-size:13px;line-height:1.6;color:#303133;">{{ logContent }}</pre>
</div>
</div>
<div v-if="activeTab === 'assistant'">
<div class="toolbar">
<el-button type="primary" size="small" @click="saveAssistantPrompt" :loading="assistantSaving">保存配置</el-button>
<span style="font-size:13px;color:#909399;margin-left:8px;">AI 助手在各页面右下角显示悬浮按钮,点击可打开对话窗口</span>
</div>
<el-form label-width="120px" style="max-width:700px;margin-top:16px;">
<el-form-item label="系统提示词">
<el-input type="textarea" v-model="assistantPrompt" :rows="12" placeholder="设置 AI 助手的角色和行为说明,留空则使用默认提示词"></el-input>
</el-form-item>
<el-form-item label="启用助手" v-if="false">
<el-switch v-model="assistantEnabled"></el-switch>
</el-form-item>
</el-form>
</div>
</div>
</div>
</main>
</div>
<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" 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 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="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="999999" 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-form>
<template #footer>
<el-button @click="llmConfigDialogVisible=false">取消</el-button>
<el-button type="primary" @click="saveLLMConfig">确定</el-button>
</template>
</el-dialog>
<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>
<el-button type="primary" @click="saveSystemConfig">确定</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, computed, onMounted } = Vue;
const { ElMessage, ElMessageBox } = ElementPlus;
const app = createApp({
setup() {
const token = localStorage.getItem('authToken');
if (!token) { window.location.href = 'login.html'; return {}; }
const apiBase = '';
const api = {
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('users');
const currentUser = ref({ username: '' });
const isAdmin = ref(false);
fetch('/api/auth/me', { headers: { 'Authorization': 'Bearer ' + token } })
.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';
if (!isAdmin.value) { ElMessage.warning('需要管理员权限'); window.location.href = '/login.html'; }
})
.catch(() => { localStorage.removeItem('authToken'); window.location.href = '/login.html'; });
const redirectToPage = (page) => { window.location.href = page.startsWith('/') ? page : '/' + page; };
const taskLogs = ref([]);
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: 131072, model: '', provider: 'opencode-go', base_url: '', api_key: '', is_active: true });
const editingLLMConfigId = ref(null);
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;
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 ?? 131072;
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;
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 = 131072; llmConfigForm.system_prompt = ''; llmConfigForm.user_prompt_template = ''; llmConfigForm.is_active = true;
}
llmConfigDialogVisible.value = true;
};
const saveLLMConfig = async () => {
try {
if (editingLLMConfigId.value) { await api.put(`/api/admin/llmconfigs/${editingLLMConfigId.value}`, llmConfigForm); ElMessage.success('更新成功'); }
else { await api.post('/api/admin/llmconfigs', llmConfigForm); ElMessage.success('创建成功'); }
llmConfigDialogVisible.value = false; await loadLLMConfigs();
} catch (e) { ElMessage.error('保存失败: ' + e.message); }
};
const deleteLLMConfig = async (id) => {
try { await ElMessageBox.confirm('确定删除该配置吗?', '提示', { type: 'warning' }); await api.delete(`/api/admin/llmconfigs/${id}`); ElMessage.success('删除成功'); await loadLLMConfigs(); }
catch (e) { if (e !== 'cancel') ElMessage.error('删除失败: ' + e.message); }
};
const platformConfigs = ref([]);
const platformConfigsLoading = ref(false);
const platformConfigsRequested = ref(false);
const pcShowActiveOnly = ref(true);
const showPcDialog = ref(false);
const pcDialogTitle = ref('新增配置');
const pcForm = ref({ platform: '', name: '', icon: '', website_url: '', api_endpoint: '', min_words: 300, max_words: 3000, requires_image: false, image_count_min: 0, image_count_max: 0, default_format: '', is_active: true });
const editingPcPlatform = ref(null);
const loadPlatformConfigs = async () => {
platformConfigsLoading.value = true;
platformConfigsRequested.value = true;
try {
const url = pcShowActiveOnly.value ? '/api/platform-config?active_only=true' : '/api/platform-config';
const data = await api.get(url);
platformConfigs.value = data;
} catch (e) {
ElMessage.error('加载平台配置失败: ' + e.message);
}
finally { platformConfigsLoading.value = false; }
};
const showPcDialogFn = (row = null) => {
try {
if (row) {
pcDialogTitle.value = '编辑配置'; editingPcPlatform.value = row.platform;
pcForm.value = { ...row };
} else {
pcDialogTitle.value = '新增配置'; editingPcPlatform.value = null;
pcForm.value = { platform: '', name: '', icon: '', website_url: '', api_endpoint: '', min_words: 300, max_words: 3000, requires_image: false, image_count_min: 0, image_count_max: 0, default_format: '', is_active: true };
}
showPcDialog.value = true;
} catch (e) { console.error('showPcDialogFn error:', e); }
};
const savePcForm = async () => {
try {
if (editingPcPlatform.value) { await api.put(`/api/platform-config/${pcForm.value.platform}`, pcForm.value); ElMessage.success('更新成功'); }
else { await api.post('/api/platform-config', pcForm.value); ElMessage.success('创建成功'); }
showPcDialog.value = false; await loadPlatformConfigs();
} catch (e) { ElMessage.error('保存失败: ' + e.message); }
};
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 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 orgPage = ref(1);
const orgPageSize = 10;
const paginatedOrgs = computed(() => { const s = (orgPage.value - 1) * orgPageSize; return orgs.value.slice(s, s + orgPageSize); });
const users = ref([]);
const usersLoading = ref(false);
const userDialogVisible = ref(false);
const userSubmitting = ref(false);
const userForm = reactive({ username: '', password: '', role: 'editor' });
const userPage = ref(1);
const userPageSize = 10;
const paginatedUsers = computed(() => {
const start = (userPage.value - 1) * userPageSize;
return users.value.slice(start, start + userPageSize);
});
const fetchUsers = async () => {
usersLoading.value = true;
try { users.value = await api.get('/api/admin/users'); } catch (e) { ElMessage.error('加载用户失败: ' + e.message); }
finally { usersLoading.value = false; }
};
const addUser = () => { userForm.username = ''; userForm.password = ''; userForm.role = 'editor'; userDialogVisible.value = true; };
const submitUser = async () => {
if (!userForm.username || userForm.username.length < 2) { ElMessage.warning('用户名至少2个字符'); return; }
if (!userForm.password || userForm.password.length < 6) { ElMessage.warning('密码至少6位'); return; }
userSubmitting.value = true;
try {
const u = await api.post('/api/admin/users', { ...userForm });
users.value.push(u);
ElMessage.success('创建成功');
userDialogVisible.value = false;
} catch (e) { ElMessage.error('创建失败: ' + e.message); }
finally { userSubmitting.value = false; }
};
const deleteUser = async (id) => {
if (id === 'admin') { ElMessage.warning('不能删除管理员用户'); return; }
try {
await ElMessageBox.confirm('确定删除该用户?', '提示', { type: 'warning' });
await api.delete(`/api/admin/users/${encodeURIComponent(id)}`);
users.value = users.value.filter(u => u.id !== id);
ElMessage.success('删除成功');
} catch (e) { if (e !== 'cancel') ElMessage.error('删除失败: ' + e.message); }
};
const logType = ref('creator');
const logDate = ref(new Date().toISOString().slice(0, 10));
const logContent = ref('');
const logsLoading = ref(false);
const fetchLogs = async () => {
logsLoading.value = true;
try {
const resp = await fetch(`/api/system/logs/${logDate.value}?log_type=${logType.value}`, {
headers: { 'Authorization': `Bearer ${localStorage.getItem('authToken')}` }
});
if (resp.ok) {
const data = await resp.json();
const lines = data.content || [];
logContent.value = lines.join('\n');
} else if (resp.status === 404) {
logContent.value = '该日暂无日志';
} else {
logContent.value = `请求失败: ${resp.status}`;
}
} catch (e) {
logContent.value = '加载失败: ' + e.message;
} finally { logsLoading.value = false; }
};
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 = ''; }
systemConfigDialogVisible.value = true;
};
const saveSystemConfig = async () => {
try {
if (editingSystemConfigKey.value) { await api.put(`/api/admin/systemconfigs/${editingSystemConfigKey.value}`, systemConfigForm); ElMessage.success('更新成功'); }
else { await api.post('/api/admin/systemconfigs', systemConfigForm); ElMessage.success('创建成功'); }
systemConfigDialogVisible.value = false; await loadSystemConfigs();
} catch (e) { ElMessage.error('保存失败: ' + e.message); }
};
const deleteSystemConfig = async (key) => {
try { await ElMessageBox.confirm('确定删除该配置吗?', '提示', { type: 'warning' }); await api.delete(`/api/admin/systemconfigs/${key}`); ElMessage.success('删除成功'); await loadSystemConfigs(); }
catch (e) { if (e !== 'cancel') ElMessage.error('删除失败: ' + e.message); }
};
const assistantPrompt = ref('');
const assistantEnabled = ref(true);
const assistantSaving = ref(false);
const loadAssistantConfig = async () => {
try {
const configs = await api.get('/api/admin/systemconfigs');
const cfg = configs.find(c => c.key === 'assistant_system_prompt');
assistantPrompt.value = cfg ? (typeof cfg.value === 'string' ? cfg.value : '') : '';
} catch (e) { console.error('加载AI助手配置失败', e); }
};
const saveAssistantPrompt = async () => {
assistantSaving.value = true;
try {
await api.post('/api/admin/systemconfigs', { key: 'assistant_system_prompt', value: assistantPrompt.value, description: 'AI 助手系统提示词' });
ElementPlus.ElMessage.success('保存成功');
} catch (e) { ElementPlus.ElMessage.error('保存失败: ' + e.message); }
finally { assistantSaving.value = false; }
};
const formatDate = (dateStr) => { if (!dateStr) return '-'; return new Date(dateStr.replace(' ', 'T')).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); };
const logout = () => { localStorage.removeItem('authToken'); localStorage.removeItem('userRole'); localStorage.removeItem('currentUser'); window.location.href = '/login.html'; };
const tabLoaders = {
tasklogs: loadTaskLogs,
llmconfigs: loadLLMConfigs, platformconfigs: loadPlatformConfigs, systemconfigs: loadSystemConfigs,
users: fetchUsers,
orgs: loadOrgs, assistant: loadAssistantConfig,
};
const loadedTabs = new Set([]);
const switchTab = (name) => {
console.log('[switchTab]', name, 'loadedTabs has?', loadedTabs.has(name));
activeTab.value = name;
if (!loadedTabs.has(name)) {
loadedTabs.add(name);
if (tabLoaders[name]) tabLoaders[name]();
else console.warn('[switchTab] no loader for', name);
}
};
onMounted(() => {
const hash = window.location.hash.replace('#tab=', '');
if (hash && tabLoaders[hash]) { switchTab(hash); }
else { switchTab('users'); }
});
return {
activeTab, switchTab,
taskLogs, taskLogsLoading, loadTaskLogs,
llmConfigs, llmConfigsLoading, llmConfigDialogVisible, llmConfigForm, llmConfigDialogTitle, showLLMConfigDialog, saveLLMConfig, deleteLLMConfig,
platformConfigs, platformConfigsLoading, platformConfigsRequested, pcShowActiveOnly, showPcDialog, pcForm, pcDialogTitle, showPcDialogFn, savePcForm, editingPcPlatform,
systemConfigs, systemConfigsLoading, systemConfigDialogVisible, systemConfigForm, systemConfigDialogTitle, showSystemConfigDialog, saveSystemConfig, deleteSystemConfig,
orgs, orgsLoading, orgDialogVisible, orgForm, orgDialogTitle, showOrgDialog, saveOrg, deleteOrg,
orgPage, orgPageSize, paginatedOrgs,
logout, currentUser, isAdmin, redirectToPage,
users, usersLoading, userDialogVisible, userSubmitting, userForm, userPage, userPageSize, paginatedUsers,
fetchUsers, addUser, submitUser, deleteUser,
logType, logDate, logContent, logsLoading, fetchLogs,
assistantPrompt, assistantEnabled, assistantSaving, saveAssistantPrompt,
};
}
});
app.use(ElementPlus);
if (window.installIcons) { window.installIcons(app); }
if (window.installUniNav) { window.installUniNav(app); }
app.mount('#app');
</script>
<script src="ai-assistant.js"></script>
</body>
</html>