7e953afbd2
- 新增创作工作台 studio.html:合并选题/内容工厂/文章管理为单一 tab 入口(iframe embed 模式) - 新增 AI味检测模块(ai_slop API + 页面,合规软硬问题分级) - 新增白标品牌配置(branding API + 页面 + deploy 私有化交付包) - 发布闭环:publishing 放宽至 editor + records/mark-published 接口 - 移动端响应式补全(admin/calendar/ai-slop 表格卡片兜底) - 修复菜单幂等播种缺陷(按 path 对齐,避免功能页孤立) - 新增短视频脚本 shortvideo.py 与 2026 市场调研简报
1766 lines
136 KiB
HTML
1766 lines
136 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" v-cloak>
|
|
<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 === '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 === 'roles' ? 'primary' : ''" @click="switchTab('roles')">角色管理</el-button>
|
|
<el-button size="default" :type="activeTab === 'menus' ? 'primary' : ''" @click="switchTab('menus')">菜单管理</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>
|
|
<el-button size="default" :type="activeTab === 'searchproviders' ? 'primary' : ''" @click="switchTab('searchproviders')">搜索API</el-button>
|
|
<el-button size="default" :type="activeTab === 'searchrankings' ? 'primary' : ''" @click="switchTab('searchrankings')">搜索排名</el-button>
|
|
<el-button size="default" :type="activeTab === 'geo' ? 'primary' : ''" @click="switchTab('geo')">GEO 就绪度</el-button>
|
|
<el-button size="default" :type="activeTab === 'configitems' ? 'primary' : ''" @click="switchTab('configitems')">配置管理</el-button>
|
|
<el-button size="default" :type="activeTab === 'taskmanagement' ? 'primary' : ''" @click="switchTab('taskmanagement')">任务管理</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="60"></el-table-column>
|
|
<el-table-column prop="username" label="用户名" min-width="100"></el-table-column>
|
|
<el-table-column prop="role" label="角色" width="80">
|
|
<template #default="scope">
|
|
<el-tag :type="scope.row.role === 'admin' ? 'danger' : 'warning'" size="small">{{ scope.row.role }}</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="org_id" label="组织" width="70"></el-table-column>
|
|
<el-table-column label="创建时间" width="140">
|
|
<template #default="scope">{{ scope.row.created_at ? new Date(scope.row.created_at).toLocaleString('zh-CN') : '-' }}</template>
|
|
</el-table-column>
|
|
<el-table-column label="最后登录" width="140">
|
|
<template #default="scope">{{ scope.row.last_login ? new Date(scope.row.last_login).toLocaleString('zh-CN') : '-' }}</template>
|
|
</el-table-column>
|
|
<el-table-column label="操作" width="150" fixed="right">
|
|
<template #default="scope">
|
|
<el-button size="small" @click="showEditUserDialog(scope.row)">编辑</el-button>
|
|
<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-row"><span class="card-label">组织</span><span class="card-value">{{ item.org_id }}</span></div>
|
|
<div class="card-row"><span class="card-label">创建</span><span class="card-value">{{ item.created_at ? new Date(item.created_at).toLocaleDateString('zh-CN') : '-' }}</span></div>
|
|
<div class="card-actions">
|
|
<el-button size="small" @click="showEditUserDialog(item)">编辑</el-button>
|
|
<el-button size="small" type="danger" @click="deleteUser(item.id)">删除</el-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<el-dialog v-model="userDialogVisible" :title="userDialogTitle" 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个字符" :disabled="userDialogTitle !== '新增用户'"/></el-form-item>
|
|
<el-form-item :label="userDialogTitle === '新增用户' ? '密码' : '新密码'"><el-input v-model="userForm.password" type="password" show-password :placeholder="userDialogTitle === '新增用户' ? '至少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">{{ userDialogTitle === '新增用户' ? '创建' : '保存' }}</el-button>
|
|
</template>
|
|
</el-dialog>
|
|
</div>
|
|
|
|
<div v-if="activeTab === 'llmconfigs'">
|
|
<div style="margin-bottom:12px;padding:10px 16px;background:#f0f9eb;border-radius:6px;border:1px solid #e1f3d8;font-size:13px;display:flex;align-items:center;gap:8px;flex-wrap:wrap">
|
|
<span>🤖 LLM 路由:</span>
|
|
<el-tag type="success" size="small" effect="dark">{{ defaultProvider }} ({{ defaultModel }})</el-tag>
|
|
<span style="color:#999">← 默认(趋势/采集/创作/优化)</span>
|
|
<span style="margin:0 4px;color:#ccc">|</span>
|
|
<el-tag type="warning" size="small" effect="dark">{{ complianceProvider }} ({{ complianceModel }})</el-tag>
|
|
<span style="color:#999">← 合规审查固定</span>
|
|
</div>
|
|
<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 label="调用上限" width="110">
|
|
<template #default="scope">{{ scope.row.rate_limit ? scope.row.rate_limit + '次/' + (scope.row.rate_limit_window_minutes || 300) + '分' : '不限' }}</template>
|
|
</el-table-column>
|
|
<el-table-column prop="is_active" label="状态" width="80">
|
|
<template #default="scope">
|
|
<el-tag v-if="scope.row.is_active" type="success" size="small" effect="dark">启用</el-tag>
|
|
<el-tag v-else type="info" size="small">停用</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="is_default" label="默认" width="70">
|
|
<template #default="scope">
|
|
<el-tag v-if="scope.row.is_default" type="warning" size="small" effect="dark">默认</el-tag>
|
|
</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="150" 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.rate_limit ? item.rate_limit + '次/' + (item.rate_limit_window_minutes || 300) + '分' : '不限' }}</span></div>
|
|
<div class="card-row"><span class="card-label">状态</span><span class="card-value"><el-tag v-if="item.is_active" type="success" size="small" effect="dark">当前使用</el-tag><el-tag v-else type="info" size="small">未启用</el-tag></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>
|
|
<template v-else>
|
|
<el-table class="data-table" :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="150" fixed="right">
|
|
<template #default="scope">
|
|
<el-button size="small" @click="showPcDialogFn(scope.row)">编辑</el-button>
|
|
<el-button size="small" type="danger" @click="deletePlatformConfig(scope.row.platform)">删除</el-button>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
<div class="card-list-mobile">
|
|
<div v-for="pc in platformConfigs" :key="pc.platform" class="card-item">
|
|
<div class="card-row"><span class="card-label">标识</span><span class="card-value">{{ pc.platform }}</span></div>
|
|
<div class="card-row"><span class="card-label">名称</span><span class="card-value">{{ pc.name }}</span></div>
|
|
<div class="card-row"><span class="card-label">图标</span><span class="card-value">{{ pc.icon || '—' }}</span></div>
|
|
<div class="card-row"><span class="card-label">网址</span><span class="card-value" style="word-break:break-all;">{{ pc.website_url || '—' }}</span></div>
|
|
<div class="card-row"><span class="card-label">字数</span><span class="card-value">{{ pc.min_words }} - {{ pc.max_words }}</span></div>
|
|
<div class="card-row"><span class="card-label">配图</span><span class="card-value">{{ pc.requires_image ? pc.image_count_min + '-' + pc.image_count_max + '张' : '不需要' }}</span></div>
|
|
<div class="card-row"><span class="card-label">激活</span><span class="card-value">{{ pc.is_active ? '是' : '否' }}</span></div>
|
|
<div class="card-actions">
|
|
<el-button size="small" @click="showPcDialogFn(pc)">编辑</el-button>
|
|
<el-button size="small" type="danger" @click="deletePlatformConfig(pc.platform)">删除</el-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<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 === 'roles'">
|
|
<div class="toolbar">
|
|
<el-button type="primary" size="small" @click="showRoleDialog()">新增角色</el-button>
|
|
<span style="font-size:13px;color:#909399;margin-left:8px;">共 {{ roles.length }} 个</span>
|
|
</div>
|
|
<div v-if="rolesLoading" class="card-loading">加载中...</div>
|
|
<template v-else-if="roles.length === 0">
|
|
<div class="empty-state">
|
|
<el-icon style="font-size:48px;color:#c0c4cc;"><IconSetting /></el-icon>
|
|
<div class="empty-text">暂无角色,请先创建</div>
|
|
</div>
|
|
</template>
|
|
<template v-else>
|
|
<el-table :data="roles" 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="description" label="描述" min-width="200"></el-table-column>
|
|
<el-table-column prop="is_system" label="系统" width="60">
|
|
<template #default="scope">
|
|
<el-icon v-if="scope.row.is_system" style="color:#67C23A;"><IconCheck /></el-icon>
|
|
<span v-else>-</span>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="操作" width="150" fixed="right">
|
|
<template #default="scope">
|
|
<el-button size="small" @click="showRoleDialog(scope.row)" :disabled="scope.row.is_system">编辑</el-button>
|
|
<el-button size="small" type="danger" @click="deleteRole(scope.row.id)" :disabled="scope.row.is_system">删除</el-button>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
</template>
|
|
<el-dialog v-model="roleDialogVisible" :title="roleDialogTitle" width="450px" :close-on-click-modal="false">
|
|
<el-form :model="roleForm" label-width="80px">
|
|
<el-form-item label="名称"><el-input v-model="roleForm.name" placeholder="如:editor"/></el-form-item>
|
|
<el-form-item label="描述"><el-input v-model="roleForm.description" type="textarea" :rows="3" placeholder="角色说明"/></el-form-item>
|
|
</el-form>
|
|
<template #footer>
|
|
<el-button @click="roleDialogVisible = false">取消</el-button>
|
|
<el-button type="primary" @click="saveRole" :loading="roleSaving">{{ roleDialogTitle === '新增角色' ? '创建' : '保存' }}</el-button>
|
|
</template>
|
|
</el-dialog>
|
|
</div>
|
|
|
|
<div v-if="activeTab === 'menus'">
|
|
<div class="toolbar">
|
|
<el-button type="primary" size="small" @click="showMenuDialog()">新增菜单</el-button>
|
|
<span style="font-size:13px;color:#909399;margin-left:8px;">共 {{ menus.length }} 个</span>
|
|
</div>
|
|
<div v-if="menusLoading" class="card-loading">加载中...</div>
|
|
<template v-else-if="menus.length === 0">
|
|
<div class="empty-state">
|
|
<el-icon style="font-size:48px;color:#c0c4cc;"><IconMenu /></el-icon>
|
|
<div class="empty-text">暂无菜单,请先创建</div>
|
|
</div>
|
|
</template>
|
|
<template v-else>
|
|
<el-table :data="menus" 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="path" label="路径" min-width="150"></el-table-column>
|
|
<el-table-column prop="icon" label="图标" width="80"></el-table-column>
|
|
<el-table-column prop="sort_order" label="排序" width="60"></el-table-column>
|
|
<el-table-column prop="roles" label="可见角色" min-width="160">
|
|
<template #default="scope">
|
|
<el-tag v-for="r in (scope.row.roles || [])" :key="r" size="small" style="margin-right:4px;">{{ r }}</el-tag>
|
|
<span v-if="!scope.row.roles || scope.row.roles.length === 0" style="color:#909399;">全部</span>
|
|
</template>
|
|
</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="150" fixed="right">
|
|
<template #default="scope">
|
|
<el-button size="small" @click="showMenuDialog(scope.row)">编辑</el-button>
|
|
<el-button size="small" type="danger" @click="deleteMenu(scope.row.id)">删除</el-button>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
</template>
|
|
<el-dialog v-model="menuDialogVisible" :title="menuDialogTitle" width="500px" :close-on-click-modal="false">
|
|
<el-form :model="menuForm" label-width="80px">
|
|
<el-form-item label="名称"><el-input v-model="menuForm.name" placeholder="如:仪表盘"/></el-form-item>
|
|
<el-form-item label="路径"><el-input v-model="menuForm.path" placeholder="如:/ 或 topics.html"/></el-form-item>
|
|
<el-form-item label="图标"><el-input v-model="menuForm.icon" placeholder="如:IconHome"/></el-form-item>
|
|
<el-form-item label="排序"><el-input-number v-model="menuForm.sort_order" :min="0" style="width:100%"/></el-form-item>
|
|
<el-form-item label="可见角色">
|
|
<el-select v-model="menuForm.roles" multiple style="width:100%" placeholder="不选则全部可见">
|
|
<el-option v-for="r in roles" :key="r.name" :label="r.name" :value="r.name"></el-option>
|
|
</el-select>
|
|
</el-form-item>
|
|
<el-form-item label="激活"><el-switch v-model="menuForm.is_active"/></el-form-item>
|
|
</el-form>
|
|
<template #footer>
|
|
<el-button @click="menuDialogVisible = false">取消</el-button>
|
|
<el-button type="primary" @click="saveMenu" :loading="menuSaving">{{ menuDialogTitle === '新增菜单' ? '创建' : '保存' }}</el-button>
|
|
</template>
|
|
</el-dialog>
|
|
</div>
|
|
|
|
<div v-if="activeTab === 'logs'">
|
|
<div class="toolbar">
|
|
<el-select v-model="logType" placeholder="日志类型" style="width:200px;" @focus="loadLogTypes">
|
|
<el-option v-for="t in logTypes" :key="t.module_id" :label="t.name" :value="t.module_id"></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 v-if="activeTab === 'searchproviders'">
|
|
<div class="toolbar">
|
|
<el-button type="primary" size="small" @click="addSearchProvider">新增提供商</el-button>
|
|
<el-button size="small" @click="testAllSearchProviders">测试全部</el-button>
|
|
<el-button size="small" @click="resetSearchUsage">重置用量</el-button>
|
|
<span style="font-size:13px;color:#909399;margin-left:8px;">共 {{ searchProviders.length }} 个</span>
|
|
</div>
|
|
<div v-if="searchProvidersLoading" class="card-loading">加载中...</div>
|
|
<template v-else-if="searchProviders.length === 0">
|
|
<div class="empty-state">
|
|
<el-icon style="font-size:48px;color:#c0c4cc;"><IconSearch /></el-icon>
|
|
<div class="empty-text">暂无搜索API提供商</div>
|
|
</div>
|
|
</template>
|
|
<template v-else>
|
|
<el-table :data="searchProviders" 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="名称" width="120"></el-table-column>
|
|
<el-table-column prop="provider_type" label="类型" width="100">
|
|
<template #default="scope">
|
|
<el-tag :type="scope.row.provider_type === 'baidu' ? 'primary' : scope.row.provider_type === 'bing' ? 'success' : 'warning'" size="small">{{ scope.row.provider_type }}</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="enabled" label="启用" width="60">
|
|
<template #default="scope">
|
|
<el-switch v-model="scope.row.enabled" @change="updateSearchProvider(scope.row)"></el-switch>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="priority" label="优先级" width="70"></el-table-column>
|
|
<el-table-column prop="daily_limit" label="日限" width="70"></el-table-column>
|
|
<el-table-column prop="usage_today" label="已用/日限" width="120">
|
|
<template #default="scope">
|
|
<div style="display:flex;align-items:center;gap:4px;">
|
|
<el-progress :percentage="Math.round(scope.row.usage_today / scope.row.daily_limit * 100)" :stroke-width="12" :status="scope.row.usage_today >= scope.row.daily_limit ? 'exception' : scope.row.usage_today / scope.row.daily_limit > 0.8 ? 'warning' : 'success'" style="flex:1;min-width:80px;"></el-progress>
|
|
<span :style="{fontSize:'11px',color: scope.row.usage_today >= scope.row.daily_limit ? '#f56c6c' : scope.row.usage_today / scope.row.daily_limit > 0.8 ? '#e6a23c' : '#67c23a'}">{{ scope.row.usage_today }}/{{ scope.row.daily_limit }}</span>
|
|
</div>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="api_url" label="API地址" min-width="200">
|
|
<template #default="scope">
|
|
<span style="font-size:12px;word-break:break-all;">{{ scope.row.api_url || '-' }}</span>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="console_url" label="控制台" min-width="160">
|
|
<template #default="scope">
|
|
<a v-if="scope.row.console_url" :href="scope.row.console_url" target="_blank" style="font-size:12px;color:#409eff;">打开</a>
|
|
<span v-else style="color:#c0c4cc;">-</span>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="操作" width="200" fixed="right">
|
|
<template #default="scope">
|
|
<el-button size="small" @click="editSearchProvider(scope.row)">编辑</el-button>
|
|
<el-button size="small" @click="testSearchProvider(scope.row)">测试</el-button>
|
|
<el-button size="small" type="danger" @click="deleteSearchProvider(scope.row.id)">删除</el-button>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
<div class="card-list-mobile">
|
|
<div v-for="item in searchProviders" :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_type }}</span></div>
|
|
<div class="card-row"><span class="card-label">用量</span><span class="card-value">{{ item.usage_today }}/{{ item.daily_limit }}</span></div>
|
|
<div class="card-actions"><el-button size="small" @click="editSearchProvider(item)">编辑</el-button><el-button size="small" type="danger" @click="deleteSearchProvider(item.id)">删除</el-button></div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<el-dialog v-model="searchProviderDialogVisible" :title="searchProviderDialogTitle" width="720px" :close-on-click-modal="false">
|
|
<el-form :model="searchProviderForm" label-width="110px">
|
|
<el-row :gutter="16">
|
|
<el-col :span="12"><el-form-item label="名称"><el-input v-model="searchProviderForm.name" placeholder="如:百度千帆"/></el-form-item></el-col>
|
|
<el-col :span="12"> <el-form-item label="类型">
|
|
<el-select v-model="searchProviderForm.provider_type" style="width:100%">
|
|
<el-option label="百度千帆" value="baidu"></el-option>
|
|
<el-option label="360搜索" value="360"></el-option>
|
|
<el-option label="搜狗搜索" value="sogou"></el-option>
|
|
<el-option label="微信搜一搜" value="wechat"></el-option>
|
|
</el-select>
|
|
</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="searchProviderForm.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="searchProviderForm.priority" :min="1" :max="99" style="width:100%"/></el-form-item></el-col>
|
|
</el-row>
|
|
<el-row :gutter="16">
|
|
<el-col :span="14"><el-form-item label="API地址"><el-input v-model="searchProviderForm.api_url" placeholder="https://..."/></el-form-item></el-col>
|
|
<el-col :span="10"><el-form-item label="日限额"><el-input-number v-model="searchProviderForm.daily_limit" :min="1" :max="999999" style="width:100%"/></el-form-item></el-col>
|
|
</el-row>
|
|
<el-row :gutter="16">
|
|
<el-col :span="24"><el-form-item label="控制台地址"><el-input v-model="searchProviderForm.console_url" placeholder="https://console.xxx.com/..."/></el-form-item></el-col>
|
|
</el-row>
|
|
</el-form>
|
|
<template #footer>
|
|
<el-button @click="searchProviderDialogVisible = false">取消</el-button>
|
|
<el-button type="primary" @click="saveSearchProvider" :loading="searchProviderSaving">保存</el-button>
|
|
</template>
|
|
</el-dialog>
|
|
|
|
<el-dialog v-model="searchProviderTestVisible" title="测试结果" width="600px">
|
|
<div v-if="searchProviderTesting" style="text-align:center;padding:20px;">测试中...</div>
|
|
<div v-else>
|
|
<div :style="{padding:'8px',marginBottom:'8px',borderRadius:'4px',background:searchProviderTestResult.ok ? '#f0f9eb' : '#fef0f0',color:searchProviderTestResult.ok ? '#67c23a' : '#f56c6c'}">
|
|
{{ searchProviderTestResult.ok ? '✅ 连接成功' : '❌ 连接失败' }}
|
|
</div>
|
|
<div v-if="searchProviderTestResult.error" style="color:#f56c6c;font-size:13px;margin-bottom:8px;">{{ searchProviderTestResult.error }}</div>
|
|
<div v-if="searchProviderTestResult.results && searchProviderTestResult.results.length">
|
|
<div v-for="r in searchProviderTestResult.results" :key="r.url" style="padding:8px;border-bottom:1px solid #eee;">
|
|
<div style="font-weight:bold;">{{ r.title }}</div>
|
|
<div style="font-size:12px;color:#909399;">{{ r.url }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<template #footer><el-button @click="searchProviderTestVisible = false">关闭</el-button></template>
|
|
</el-dialog>
|
|
|
|
<div v-if="activeTab === 'searchrankings'">
|
|
<div class="toolbar">
|
|
<span style="font-size:13px;color:#909399;">GEO/SEO 搜索排名追踪 — 每日 07:00 自动检查</span>
|
|
</div>
|
|
<div v-if="rankingsLoading" class="card-loading">加载中...</div>
|
|
<template v-else>
|
|
<div style="display:flex;gap:16px;margin-bottom:16px;flex-wrap:wrap;">
|
|
<div class="stat-card" style="flex:1;min-width:140px;padding:16px;background:#f0f9ff;border-radius:8px;text-align:center;">
|
|
<div style="font-size:28px;font-weight:700;color:#409eff;">{{ rankingStats.total_checks }}</div>
|
|
<div style="font-size:12px;color:#909399;margin-top:4px;">累计检查关键词</div>
|
|
</div>
|
|
<div class="stat-card" style="flex:1;min-width:140px;padding:16px;background:#f0fdf4;border-radius:8px;text-align:center;">
|
|
<div style="font-size:28px;font-weight:700;color:#67c23a;">{{ rankingStats.on_page }}</div>
|
|
<div style="font-size:12px;color:#909399;margin-top:4px;">有排名关键词</div>
|
|
</div>
|
|
<div class="stat-card" style="flex:1;min-width:140px;padding:16px;background:#fef3f2;border-radius:8px;text-align:center;">
|
|
<div style="font-size:28px;font-weight:700;color:#f56c6c;">{{ rankingStats.ai_cited }}</div>
|
|
<div style="font-size:12px;color:#909399;margin-top:4px;">AI 搜索引用</div>
|
|
</div>
|
|
<div class="stat-card" style="flex:1;min-width:140px;padding:16px;background:#fff7e6;border-radius:8px;text-align:center;">
|
|
<div style="font-size:28px;font-weight:700;color:#e6a23c;">{{ rankingStats.best_position ? '#' + rankingStats.best_position : '-' }}</div>
|
|
<div style="font-size:12px;color:#909399;margin-top:4px;">最佳排名</div>
|
|
</div>
|
|
</div>
|
|
|
|
<el-table :data="rankings" style="width:100%" size="small" max-height="500">
|
|
<el-table-column prop="keyword" label="关键词" min-width="140" />
|
|
<el-table-column prop="platform" label="平台" width="80" />
|
|
<el-table-column prop="search_engine" label="引擎" width="70" />
|
|
<el-table-column prop="position" label="排名" width="70">
|
|
<template #default="{ row }">
|
|
<el-tag :type="row.position && row.position <= 3 ? 'success' : row.position ? 'warning' : 'info'" size="small">
|
|
{{ row.position ? '#' + row.position : '未上榜' }}
|
|
</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="ai_cited" label="AI引用" width="80">
|
|
<template #default="{ row }">
|
|
<el-tag :type="row.ai_cited ? 'success' : 'info'" size="small">{{ row.ai_cited ? '是' : '否' }}</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="url_found" label="匹配 URL" min-width="200" show-overflow-tooltip />
|
|
<el-table-column prop="checked_at" label="检查时间" width="160" />
|
|
</el-table>
|
|
</template>
|
|
</div>
|
|
|
|
<div v-if="activeTab === 'geo'">
|
|
<div class="toolbar">
|
|
<span style="font-size:13px;color:#909399;">GEO 就绪度 — AI 搜索引用追踪 & 结构优化评分,每日 07:30 自动检查</span>
|
|
<el-button size="small" type="primary" @click="triggerGeoTrack" :loading="geoTracking">手动追踪</el-button>
|
|
</div>
|
|
<div v-if="geoLoading" class="card-loading">加载中...</div>
|
|
<template v-else>
|
|
<div style="display:flex;gap:16px;margin-bottom:16px;flex-wrap:wrap;">
|
|
<div class="stat-card" style="flex:1;min-width:140px;padding:16px;background:#f0f9ff;border-radius:8px;text-align:center;">
|
|
<div style="font-size:28px;font-weight:700;color:#409eff;">{{ geoOverview.total_geo_checks }}</div>
|
|
<div style="font-size:12px;color:#909399;margin-top:4px;">GEO 检查次数</div>
|
|
</div>
|
|
<div class="stat-card" style="flex:1;min-width:140px;padding:16px;background:#f0fdf4;border-radius:8px;text-align:center;">
|
|
<div style="font-size:28px;font-weight:700;color:#67c23a;">{{ geoOverview.total_cited }}</div>
|
|
<div style="font-size:12px;color:#909399;margin-top:4px;">AI 引用次数</div>
|
|
</div>
|
|
<div class="stat-card" style="flex:1;min-width:140px;padding:16px;background:#fef3f2;border-radius:8px;text-align:center;">
|
|
<div style="font-size:28px;font-weight:700;color:#f56c6c;">{{ geoOverview.citation_rate }}%</div>
|
|
<div style="font-size:12px;color:#909399;margin-top:4px;">AI 引用率</div>
|
|
</div>
|
|
<div class="stat-card" style="flex:1;min-width:140px;padding:16px;background:#fff7e6;border-radius:8px;text-align:center;">
|
|
<div style="font-size:28px;font-weight:700;color:#e6a23c;">{{ geoOverview.avg_geo_readiness }}</div>
|
|
<div style="font-size:12px;color:#909399;margin-top:4px;">平均就绪度评分</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="Object.keys(geoOverview.engine_breakdown || {}).length" style="margin-bottom:16px;">
|
|
<h4 style="margin:0 0 8px;font-size:14px;color:#606266;">AI 引擎分布</h4>
|
|
<div style="display:flex;gap:12px;flex-wrap:wrap;">
|
|
<div v-for="(val, engine) in geoOverview.engine_breakdown" :key="engine" style="padding:10px 16px;background:#fafafa;border-radius:6px;border:1px solid #ebeef5;min-width:120px;">
|
|
<div style="font-weight:600;font-size:14px;">{{ engine }}</div>
|
|
<div style="font-size:12px;color:#909399;margin-top:4px;">检查 {{ val.total }} 次,引用 {{ val.cited }} 次</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<h4 style="margin:16px 0 8px;font-size:14px;color:#606266;">就绪度评分</h4>
|
|
<el-table :data="geoReadinessScores" style="width:100%" size="small" max-height="500">
|
|
<el-table-column prop="article_title" label="文章" min-width="160" show-overflow-tooltip />
|
|
<el-table-column prop="total_score" label="总分" width="80">
|
|
<template #default="{ row }">
|
|
<el-tag :type="row.total_score >= 70 ? 'success' : row.total_score >= 40 ? 'warning' : 'danger'" size="small">{{ row.total_score }}</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="has_schema" label="结构化数据" width="100">
|
|
<template #default="{ row }"><el-tag :type="row.has_schema ? 'success' : 'info'" size="small">{{ row.has_schema ? '✓' : '✗' }}</el-tag></template>
|
|
</el-table-column>
|
|
<el-table-column prop="has_faq_format" label="FAQ" width="60">
|
|
<template #default="{ row }"><el-tag :type="row.has_faq_format ? 'success' : 'info'" size="small">{{ row.has_faq_format ? '✓' : '✗' }}</el-tag></template>
|
|
</el-table-column>
|
|
<el-table-column prop="has_howto_format" label="HowTo" width="70">
|
|
<template #default="{ row }"><el-tag :type="row.has_howto_format ? 'success' : 'info'" size="small">{{ row.has_howto_format ? '✓' : '✗' }}</el-tag></template>
|
|
</el-table-column>
|
|
<el-table-column prop="has_citations" label="引用" width="60">
|
|
<template #default="{ row }"><el-tag :type="row.has_citations ? 'success' : 'info'" size="small">{{ row.has_citations ? '✓' : '✗' }}</el-tag></template>
|
|
</el-table-column>
|
|
<el-table-column prop="word_count" label="字数" width="70" />
|
|
<el-table-column prop="readability_score" label="可读性" width="80" />
|
|
<el-table-column prop="heading_structure_score" label="标题结构" width="80" />
|
|
<el-table-column prop="checked_at" label="检查时间" width="150" />
|
|
</el-table>
|
|
</template>
|
|
</div>
|
|
|
|
<div v-if="activeTab === 'configitems'">
|
|
<div class="filter-bar">
|
|
<el-button size="default" :type="configSubTab === 'sensitive' ? 'primary' : ''" @click="switchConfigSubTab('sensitive')">🔒 敏感词</el-button>
|
|
<el-button size="default" :type="configSubTab === 'cleanrules' ? 'primary' : ''" @click="switchConfigSubTab('cleanrules')">🧹 清理规则</el-button>
|
|
</div>
|
|
|
|
<template v-if="configSubTab === 'sensitive'">
|
|
<div class="toolbar">
|
|
<el-button type="primary" size="small" @click="showAddSensitiveWord">新增敏感词</el-button>
|
|
<span style="font-size:13px;color:#909399;margin-left:8px;">共 {{ sensitiveWords.length }} 个</span>
|
|
</div>
|
|
<div v-if="swLoading" class="card-loading">加载中...</div>
|
|
<template v-else-if="sensitiveWords.length === 0">
|
|
<div class="empty-state">
|
|
<el-icon style="font-size:48px;color:#c0c4cc;"><IconWarning /></el-icon>
|
|
<div class="empty-text">暂无敏感词</div>
|
|
</div>
|
|
</template>
|
|
<template v-else>
|
|
<el-table :data="sensitiveWords" border stripe class="data-table" style="width:100%">
|
|
<el-table-column type="index" label="#" width="50"></el-table-column>
|
|
<el-table-column prop="word" label="敏感词" min-width="160"></el-table-column>
|
|
<el-table-column prop="category" label="类别" width="110">
|
|
<template #default="{ row }">
|
|
<el-tag size="small">{{ row.category || '未分类' }}</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="操作" width="150">
|
|
<template #default="{ row }">
|
|
<el-button size="small" @click="editSensitiveWord(row)">编辑</el-button>
|
|
<el-button size="small" type="danger" @click="deleteSensitiveWord(row)">删除</el-button>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
<div class="card-list-mobile">
|
|
<div v-for="item in sensitiveWords" :key="item.id" class="card-item">
|
|
<div><el-tag size="small">{{ item.category || '未分类' }}</el-tag> <strong>{{ item.word }}</strong></div>
|
|
<div style="margin-top:4px;">
|
|
<el-button size="small" @click="editSensitiveWord(item)">编辑</el-button>
|
|
<el-button size="small" type="danger" @click="deleteSensitiveWord(item)">删除</el-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</template>
|
|
|
|
<template v-if="configSubTab === 'cleanrules'">
|
|
<div class="toolbar">
|
|
<el-button type="primary" size="small" @click="showAddCleanRule">新增清理规则</el-button>
|
|
<span style="font-size:13px;color:#909399;margin-left:8px;">共 {{ contentCleanRules.length }} 条</span>
|
|
</div>
|
|
<div v-if="ccrLoading" class="card-loading">加载中...</div>
|
|
<template v-else-if="contentCleanRules.length === 0">
|
|
<div class="empty-state">
|
|
<el-icon style="font-size:48px;color:#c0c4cc;"><IconWarning /></el-icon>
|
|
<div class="empty-text">暂无清理规则</div>
|
|
</div>
|
|
</template>
|
|
<template v-else>
|
|
<el-table :data="contentCleanRules" border stripe class="data-table" style="width:100%">
|
|
<el-table-column type="index" label="#" width="50"></el-table-column>
|
|
<el-table-column prop="rule_type" label="类型" width="110">
|
|
<template #default="{ row }">
|
|
<el-tag size="small">{{ row.rule_type }}</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="description" label="描述" min-width="140"></el-table-column>
|
|
<el-table-column prop="pattern" label="模式" min-width="220">
|
|
<template #default="{ row }"><code style="font-size:12px;word-break:break-all;">{{ row.pattern }}</code></template>
|
|
</el-table-column>
|
|
<el-table-column label="状态" width="80">
|
|
<template #default="{ row }">
|
|
<el-tag :type="row.is_active ? 'success' : 'info'" size="small">{{ row.is_active ? '启用' : '停用' }}</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="操作" width="150">
|
|
<template #default="{ row }">
|
|
<el-button size="small" @click="editCleanRule(row)">编辑</el-button>
|
|
<el-button size="small" type="danger" @click="deleteCleanRule(row)">删除</el-button>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
<div class="card-list-mobile">
|
|
<div v-for="item in contentCleanRules" :key="item.id" class="card-item">
|
|
<div><el-tag size="small">{{ item.rule_type }}</el-tag> <code style="font-size:12px;">{{ item.pattern }}</code></div>
|
|
<div style="margin-top:4px;">{{ item.description }}</div>
|
|
<div style="margin-top:4px;">
|
|
<el-tag :type="item.is_active ? 'success' : 'info'" size="small">{{ item.is_active ? '启用' : '停用' }}</el-tag>
|
|
<el-button size="small" style="margin-left:8px;" @click="editCleanRule(item)">编辑</el-button>
|
|
<el-button size="small" type="danger" @click="deleteCleanRule(item)">删除</el-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</template>
|
|
</div>
|
|
|
|
<div v-if="activeTab === 'taskmanagement'">
|
|
<div class="toolbar">
|
|
<span style="font-size:13px;color:#909399;">定时任务配置与运行记录 — 共 {{ taskConfigs.length }} 个模块</span>
|
|
</div>
|
|
<div v-if="tcLoading" class="card-loading">加载中...</div>
|
|
<template v-else-if="taskConfigs.length === 0">
|
|
<div class="empty-state">
|
|
<el-icon style="font-size:48px;color:#c0c4cc;"><IconTimer /></el-icon>
|
|
<div class="empty-text">暂无任务配置</div>
|
|
</div>
|
|
</template>
|
|
<template v-else>
|
|
<el-table :data="taskConfigs" border stripe style="width:100%" size="small"
|
|
@row-click="selectModule">
|
|
<el-table-column type="index" label="#" width="50"></el-table-column>
|
|
<el-table-column prop="module_id" label="模块ID" min-width="160"></el-table-column>
|
|
<el-table-column prop="enabled" label="启用" width="70">
|
|
<template #default="{ row }">
|
|
<el-switch :model-value="row.enabled" @change="(val) => toggleModule(row, val)" @click.stop
|
|
style="--el-switch-on-color:#67c23a;"></el-switch>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="定时" width="160">
|
|
<template #default="{ row }">
|
|
<code style="font-size:12px;background:#f4f4f5;padding:2px 6px;border-radius:4px;">{{ row.schedule || '-' }}</code>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="参数" min-width="180">
|
|
<template #default="{ row }">
|
|
<span style="font-size:12px;color:#606266;word-break:break-all;">
|
|
{{ row.params ? JSON.stringify(row.params).slice(0, 60) + (JSON.stringify(row.params).length > 60 ? '...' : '') : '-' }}
|
|
</span>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="上次运行" width="150">
|
|
<template #default="{ row }">
|
|
<span v-if="row.last_log" style="font-size:12px;">
|
|
<el-tag :type="row.last_log.status === 'success' ? 'success' : row.last_log.status === 'failed' ? 'danger' : row.last_log.status === 'running' ? 'warning' : 'info'" size="small" style="margin-right:4px;">
|
|
{{ row.last_log.status || '?' }}
|
|
</el-tag>
|
|
{{ formatDate(row.last_log.started_at) }}
|
|
</span>
|
|
<span v-else style="color:#c0c4cc;">-</span>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="操作" width="180" fixed="right">
|
|
<template #default="{ row }">
|
|
<el-button size="small" @click.stop="showEditConfig(row)">编辑</el-button>
|
|
<el-button size="small" @click.stop="selectModule(row); showHistory = true">历史</el-button>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
|
|
<div v-if="tcEditDialogVisible">
|
|
<el-dialog v-model="tcEditDialogVisible" title="编辑任务配置" width="600px" :close-on-click-modal="false">
|
|
<el-form :model="tcEditForm" label-width="100px">
|
|
<el-form-item label="模块ID"><el-input v-model="tcEditForm.module_id" disabled/></el-form-item>
|
|
<el-form-item label="启用"><el-switch v-model="tcEditForm.enabled"/></el-form-item>
|
|
<el-form-item label="定时">
|
|
<el-input v-model="tcEditForm.schedule" placeholder="cron 表达式,如 0 30 1 * *"/>
|
|
<div style="font-size:12px;color:#909399;margin-top:4px;">格式: 分 时 日 月 周 (5段cron)</div>
|
|
</el-form-item>
|
|
<el-form-item label="参数">
|
|
<el-input type="textarea" v-model="tcEditForm.paramsStr" :rows="5" placeholder="JSON 格式参数"/>
|
|
</el-form-item>
|
|
</el-form>
|
|
<template #footer>
|
|
<el-button @click="tcEditDialogVisible = false">取消</el-button>
|
|
<el-button type="primary" @click="saveTaskConfig" :loading="tcEditSaving">保存</el-button>
|
|
</template>
|
|
</el-dialog>
|
|
</div>
|
|
|
|
<div v-if="selectedModule && showHistory" style="margin-top:20px;">
|
|
<h4 style="margin:0 0 12px;font-size:14px;color:#303133;display:flex;align-items:center;gap:8px;">
|
|
运行记录:{{ selectedModule.module_id }}
|
|
<el-tag size="small" type="info" style="margin-left:4px;">最近 {{ taskLogs.length }} 条</el-tag>
|
|
<el-button size="small" style="margin-left:auto;" @click="showHistory = false">收起</el-button>
|
|
<el-button size="small" @click="loadTaskLogs(selectedModule)" :loading="tlLoading">刷新</el-button>
|
|
</h4>
|
|
<div v-if="tlLoading" class="card-loading">加载中...</div>
|
|
<template v-else-if="taskLogs.length === 0">
|
|
<div class="empty-state" style="padding:20px;">
|
|
<el-icon style="font-size:36px;color:#c0c4cc;"><IconDocument /></el-icon>
|
|
<div class="empty-text">暂无运行记录</div>
|
|
</div>
|
|
</template>
|
|
<el-timeline v-else>
|
|
<el-timeline-item v-for="log in taskLogs" :key="log.id"
|
|
:timestamp="formatDate(log.started_at)"
|
|
placement="top"
|
|
:color="log.status === 'success' ? '#67c23a' : log.status === 'failed' ? '#f56c6c' : log.status === 'running' ? '#e6a23c' : '#c0c4cc'">
|
|
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">
|
|
<el-tag :type="log.status === 'success' ? 'success' : log.status === 'failed' ? 'danger' : log.status === 'running' ? 'warning' : 'info'" size="small" effect="dark">
|
|
{{ log.status }}
|
|
</el-tag>
|
|
<span v-if="log.task_name" style="font-size:12px;color:#909399;">{{ log.task_name }}</span>
|
|
<span v-if="log.duration" style="font-size:12px;color:#909399;">耗时 {{ log.duration }}s</span>
|
|
<span v-if="log.triggered_by" style="font-size:12px;color:#909399;">触发: {{ log.triggered_by }}</span>
|
|
</div>
|
|
<div v-if="log.message" style="margin-top:6px;font-size:13px;color:#303133;background:#f9fafb;padding:8px 12px;border-radius:6px;border:1px solid #ebeef5;white-space:pre-wrap;word-break:break-word;">{{ log.message }}</div>
|
|
<div v-if="log.error_trace" style="margin-top:4px;font-size:12px;color:#f56c6c;background:#fef0f0;padding:6px 10px;border-radius:4px;white-space:pre-wrap;word-break:break-word;">{{ log.error_trace }}</div>
|
|
<div v-if="log.result_data" style="margin-top:4px;">
|
|
<el-button size="small" text @click="showResultData(log)">查看结果数据</el-button>
|
|
</div>
|
|
</el-timeline-item>
|
|
</el-timeline>
|
|
</div>
|
|
</template>
|
|
|
|
<el-dialog v-model="resultDialogVisible" title="结果数据" width="600px" :close-on-click-modal="false">
|
|
<pre style="max-height:400px;overflow-y:auto;background:#f9fafb;padding:12px;border-radius:6px;font-size:12px;white-space:pre-wrap;word-break:break-word;">{{ resultDialogContent }}</pre>
|
|
<template #footer><el-button @click="resultDialogVisible = false">关闭</el-button></template>
|
|
</el-dialog>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
|
|
<el-dialog v-model="swDialogVisible" :title="editingSwId ? '编辑敏感词' : '新增敏感词'" width="450px" :close-on-click-modal="false">
|
|
<el-form :model="swForm" label-width="80px">
|
|
<el-form-item label="敏感词"><el-input v-model="swForm.word" placeholder="输入敏感词"/></el-form-item>
|
|
<el-form-item label="类别">
|
|
<el-select v-model="swForm.category" style="width:100%">
|
|
<el-option label="general" value="general" />
|
|
<el-option label="政治敏感" value="政治敏感" />
|
|
<el-option label="违禁内容" value="违禁内容" />
|
|
<el-option label="不实信息" value="不实信息" />
|
|
<el-option label="porn" value="porn" />
|
|
<el-option label="ad" value="ad" />
|
|
<el-option label="violence" value="violence" />
|
|
<el-option label="other" value="other" />
|
|
</el-select>
|
|
</el-form-item>
|
|
</el-form>
|
|
<template #footer>
|
|
<el-button @click="swDialogVisible = false">取消</el-button>
|
|
<el-button type="primary" @click="saveSensitiveWord" :loading="swSaving">保存</el-button>
|
|
</template>
|
|
</el-dialog>
|
|
|
|
<el-dialog v-model="ccrDialogVisible" :title="ccrDialogTitle" width="650px" :close-on-click-modal="false">
|
|
<el-form :model="ccrForm" label-width="100px">
|
|
<el-form-item label="类型">
|
|
<el-select v-model="ccrForm.rule_type" style="width:100%">
|
|
<el-option label="thinking (AI思考)" value="thinking" />
|
|
<el-option label="preface (AI前缀)" value="preface" />
|
|
<el-option label="verbosity (AI废话)" value="verbosity" />
|
|
<el-option label="ai_telltale (AI套话)" value="ai_telltale" />
|
|
<el-option label="html_thinking" value="html_thinking" />
|
|
</el-select>
|
|
</el-form-item>
|
|
<el-form-item label="模式"><el-input v-model="ccrForm.pattern" placeholder="正则表达式"/></el-form-item>
|
|
<el-form-item label="描述"><el-input v-model="ccrForm.description" placeholder="简短说明"/></el-form-item>
|
|
<el-form-item label="排序"><el-input-number v-model="ccrForm.sort_order" :min="0" :max="999" style="width:100%"/></el-form-item>
|
|
<el-form-item label="启用"><el-switch v-model="ccrForm.is_active"/></el-form-item>
|
|
</el-form>
|
|
<template #footer>
|
|
<el-button @click="ccrDialogVisible = false">取消</el-button>
|
|
<el-button type="primary" @click="saveCleanRule" :loading="ccrSaving">保存</el-button>
|
|
</template>
|
|
</el-dialog>
|
|
|
|
<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="sensenova" value="sensenova"></el-option>
|
|
<el-option label="nvidia" value="nvidia"></el-option>
|
|
<el-option label="opencode-go" value="opencode-go"></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="6"><el-form-item label="激活"><el-switch v-model="llmConfigForm.is_active"/></el-form-item></el-col>
|
|
<el-col :span="6"><el-form-item label="默认"><el-switch v-model="llmConfigForm.is_default"/></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.rate_limit" :min="0" :max="999999" style="width:100%"/></el-form-item></el-col>
|
|
<el-col :span="12"><el-form-item label="时间窗口(分)"><el-input-number v-model="llmConfigForm.rate_limit_window_minutes" :min="1" :max="43200" 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="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 configSubTab = ref('sensitive');
|
|
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 llmConfigs = ref([]);
|
|
const llmConfigsLoading = ref(false);
|
|
const llmConfigDialogVisible = ref(false);
|
|
const llmConfigDialogTitle = ref('新增 LLM 配置');
|
|
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, is_default: false, rate_limit: 0, rate_limit_window_minutes: 300 });
|
|
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;
|
|
Object.assign(llmConfigForm, row);
|
|
} else {
|
|
llmConfigDialogTitle.value = '新增配置'; editingLLMConfigId.value = null;
|
|
llmConfigForm.id = null; llmConfigForm.name = ''; llmConfigForm.provider = 'sensenova'; llmConfigForm.model = 'deepseek-v4-flash';
|
|
llmConfigForm.base_url = 'https://token.sensenova.cn/v1'; llmConfigForm.api_key = ''; llmConfigForm.temperature = 0.3;
|
|
llmConfigForm.max_tokens = 4000; llmConfigForm.system_prompt = ''; llmConfigForm.user_prompt_template = ''; llmConfigForm.is_active = true; llmConfigForm.is_default = false;
|
|
llmConfigForm.rate_limit = 0; llmConfigForm.rate_limit_window_minutes = 300;
|
|
}
|
|
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(false);
|
|
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 pcDialogTitle = ref('新增平台');
|
|
const editingPcPlatform = ref(null);
|
|
const showPcDialog = ref(false);
|
|
const showPcDialogFn = (row = null) => {
|
|
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;
|
|
};
|
|
const savePcForm = async () => {
|
|
try {
|
|
if (editingPcPlatform.value) { await api.put(`/api/platform-config/${editingPcPlatform.value}`, 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 loadPlatformConfigs = async () => {
|
|
platformConfigsLoading.value = true;
|
|
try { platformConfigs.value = await api.get('/api/platform-config'); } catch (e) { ElMessage.error('加载平台配置失败: ' + e.message); }
|
|
finally { platformConfigsLoading.value = false; platformConfigsRequested.value = true; }
|
|
};
|
|
const deletePlatformConfig = async (platform) => {
|
|
try {
|
|
await ElMessageBox.confirm(`确定要停用平台配置「${platform}」吗?`, '确认删除', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' });
|
|
await api.delete(`/api/platform-config/${platform}`);
|
|
ElMessage.success('已停用');
|
|
await loadPlatformConfigs();
|
|
} catch (e) {
|
|
if (e !== 'cancel') ElMessage.error('删除失败: ' + (e.message || e));
|
|
}
|
|
};
|
|
|
|
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 orgPage = ref(1);
|
|
const orgPageSize = 10;
|
|
const paginatedOrgs = computed(() => {
|
|
const start = (orgPage.value - 1) * orgPageSize;
|
|
return orgs.value.slice(start, start + orgPageSize);
|
|
});
|
|
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 () => {
|
|
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 (id) => {
|
|
try { await ElMessageBox.confirm('确定删除该组织吗?', '提示', { type: 'warning' }); await api.delete(`/api/admin/orgs/${id}`); ElMessage.success('删除成功'); await loadOrgs(); }
|
|
catch (e) { if (e !== 'cancel') ElMessage.error('删除失败: ' + e.message); }
|
|
};
|
|
|
|
const roles = ref([]);
|
|
const rolesLoading = ref(false);
|
|
const roleDialogVisible = ref(false);
|
|
const roleDialogTitle = ref('新增角色');
|
|
const roleSaving = ref(false);
|
|
const roleForm = reactive({ id: null, name: '', description: '' });
|
|
const loadRoles = async () => {
|
|
rolesLoading.value = true;
|
|
try { roles.value = await api.get('/api/admin/roles'); } catch (e) { ElMessage.error('加载角色失败: ' + e.message); }
|
|
finally { rolesLoading.value = false; }
|
|
};
|
|
const showRoleDialog = (row = null) => {
|
|
if (row) { roleDialogTitle.value = '编辑角色'; roleForm.id = row.id; roleForm.name = row.name; roleForm.description = row.description || ''; }
|
|
else { roleDialogTitle.value = '新增角色'; roleForm.id = null; roleForm.name = ''; roleForm.description = ''; }
|
|
roleDialogVisible.value = true;
|
|
};
|
|
const saveRole = async () => {
|
|
if (!roleForm.name) { ElMessage.warning('请输入角色名称'); return; }
|
|
roleSaving.value = true;
|
|
try {
|
|
if (roleForm.id) { await api.put(`/api/admin/roles/${roleForm.id}`, { name: roleForm.name, description: roleForm.description }); ElMessage.success('更新成功'); }
|
|
else { await api.post('/api/admin/roles', { name: roleForm.name, description: roleForm.description }); ElMessage.success('创建成功'); }
|
|
roleDialogVisible.value = false; await loadRoles();
|
|
} catch (e) { ElMessage.error('保存失败: ' + e.message); }
|
|
finally { roleSaving.value = false; }
|
|
};
|
|
const deleteRole = async (id) => {
|
|
try { await ElMessageBox.confirm('确定删除该角色吗?', '提示', { type: 'warning' }); await api.delete(`/api/admin/roles/${id}`); ElMessage.success('删除成功'); await loadRoles(); }
|
|
catch (e) { if (e !== 'cancel') ElMessage.error('删除失败: ' + e.message); }
|
|
};
|
|
|
|
const menus = ref([]);
|
|
const menusLoading = ref(false);
|
|
const menuDialogVisible = ref(false);
|
|
const menuDialogTitle = ref('新增菜单');
|
|
const menuSaving = ref(false);
|
|
const menuForm = reactive({ id: null, name: '', path: '', icon: '', sort_order: 0, roles: [], is_active: true });
|
|
const loadMenus = async () => {
|
|
menusLoading.value = true;
|
|
try { menus.value = await api.get('/api/admin/menus'); } catch (e) { ElMessage.error('加载菜单失败: ' + e.message); }
|
|
finally { menusLoading.value = false; }
|
|
};
|
|
const showMenuDialog = (row = null) => {
|
|
if (row) { menuDialogTitle.value = '编辑菜单'; menuForm.id = row.id; menuForm.name = row.name; menuForm.path = row.path; menuForm.icon = row.icon || ''; menuForm.sort_order = row.sort_order || 0; menuForm.roles = row.roles || []; menuForm.is_active = row.is_active !== false; }
|
|
else { menuDialogTitle.value = '新增菜单'; menuForm.id = null; menuForm.name = ''; menuForm.path = ''; menuForm.icon = ''; menuForm.sort_order = 0; menuForm.roles = []; menuForm.is_active = true; }
|
|
menuDialogVisible.value = true;
|
|
};
|
|
const saveMenu = async () => {
|
|
if (!menuForm.name || !menuForm.path) { ElMessage.warning('请填写名称和路径'); return; }
|
|
menuSaving.value = true;
|
|
try {
|
|
const body = { name: menuForm.name, path: menuForm.path, icon: menuForm.icon, sort_order: menuForm.sort_order, roles: menuForm.roles, is_active: menuForm.is_active };
|
|
if (menuForm.id) { await api.put(`/api/admin/menus/${menuForm.id}`, body); ElMessage.success('更新成功'); }
|
|
else { await api.post('/api/admin/menus', body); ElMessage.success('创建成功'); }
|
|
menuDialogVisible.value = false; await loadMenus();
|
|
} catch (e) { ElMessage.error('保存失败: ' + e.message); }
|
|
finally { menuSaving.value = false; }
|
|
};
|
|
const deleteMenu = async (id) => {
|
|
try { await ElMessageBox.confirm('确定删除该菜单吗?', '提示', { type: 'warning' }); await api.delete(`/api/admin/menus/${id}`); ElMessage.success('删除成功'); await loadMenus(); }
|
|
catch (e) { if (e !== 'cancel') ElMessage.error('删除失败: ' + e.message); }
|
|
};
|
|
|
|
const logType = ref('');
|
|
const logTypes = ref([]);
|
|
const users = ref([]);
|
|
const usersLoading = ref(false);
|
|
const userDialogVisible = ref(false);
|
|
const userDialogTitle = ref('新增用户');
|
|
const userSubmitting = ref(false);
|
|
const userForm = reactive({ id: null, 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 = () => { userDialogTitle.value = '新增用户'; userForm.id = null; userForm.username = ''; userForm.password = ''; userForm.role = 'editor'; userDialogVisible.value = true; };
|
|
const showEditUserDialog = (row) => { userDialogTitle.value = '编辑用户'; userForm.id = row.id; userForm.username = row.username; userForm.password = ''; userForm.role = row.role; userDialogVisible.value = true; };
|
|
const submitUser = async () => {
|
|
if (!userForm.username || userForm.username.length < 2) { ElMessage.warning('用户名至少2个字符'); return; }
|
|
if (userDialogTitle.value === '新增用户' && (!userForm.password || userForm.password.length < 6)) { ElMessage.warning('密码至少6位'); return; }
|
|
userSubmitting.value = true;
|
|
try {
|
|
if (userForm.id) {
|
|
const body = { username: userForm.username, role: userForm.role };
|
|
if (userForm.password) body.password = userForm.password;
|
|
await api.put(`/api/admin/users/${userForm.id}`, body);
|
|
ElMessage.success('更新成功');
|
|
} else {
|
|
await api.post('/api/admin/users', { ...userForm });
|
|
ElMessage.success('创建成功');
|
|
}
|
|
userDialogVisible.value = false; await fetchUsers();
|
|
} catch (e) { ElMessage.error((userForm.id ? '更新' : '创建') + '失败: ' + 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 logDate = ref(new Date().toISOString().slice(0, 10));
|
|
const logContent = ref('');
|
|
const logsLoading = ref(false);
|
|
const loadLogTypes = async () => {
|
|
try {
|
|
logTypes.value = await api.get('/api/admin/task-logs/log-types');
|
|
} catch (e) { console.error('加载日志类型失败:', e); }
|
|
};
|
|
const fetchLogs = async () => {
|
|
logsLoading.value = true;
|
|
try {
|
|
const module = logTypes.value.find(t => t.module_id === logType.value);
|
|
const logFile = module ? module.log_file : logType.value;
|
|
const resp = await fetch(`/api/system/logs/${logDate.value}?log_type=${logFile}`, {
|
|
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 () => {
|
|
try {
|
|
const configs = await api.get('/api/admin/systemconfigs');
|
|
const cfg = configs.find(c => c.key === 'assistant_system_prompt');
|
|
if (cfg) { await api.put(`/api/admin/systemconfigs/${cfg.id}`, { value: assistantPrompt.value, description: 'AI 助手系统提示词' }); }
|
|
else { await api.post('/api/admin/systemconfigs', { key: 'assistant_system_prompt', value: assistantPrompt.value, description: 'AI 助手系统提示词' }); }
|
|
ElMessage.success('保存成功');
|
|
} catch (e) { ElMessage.error('保存失败: ' + e.message); }
|
|
};
|
|
|
|
const searchProviders = ref([]);
|
|
const searchProvidersLoading = ref(false);
|
|
const searchProviderDialogVisible = ref(false);
|
|
const searchProviderDialogTitle = ref('新增搜索API');
|
|
const searchProviderSaving = ref(false);
|
|
const searchProviderForm = reactive({ id: null, name: '', provider_type: 'baidu', api_key: '', api_url: '', console_url: '', priority: 1, daily_limit: 1500, enabled: true });
|
|
const searchProviderTestVisible = ref(false);
|
|
const searchProviderTesting = ref(false);
|
|
const searchProviderTestResult = ref({ ok: false, error: '', results: [] });
|
|
|
|
const loadSearchProviders = async () => {
|
|
searchProvidersLoading.value = true;
|
|
try { searchProviders.value = await api.get('/api/search-providers'); }
|
|
catch (e) { ElMessage.error('加载搜索API失败: ' + e.message); }
|
|
finally { searchProvidersLoading.value = false; }
|
|
};
|
|
const addSearchProvider = () => {
|
|
searchProviderDialogTitle.value = '新增搜索API';
|
|
searchProviderForm.id = null;
|
|
searchProviderForm.name = '';
|
|
searchProviderForm.provider_type = 'baidu';
|
|
searchProviderForm.api_key = '';
|
|
searchProviderForm.api_url = '';
|
|
searchProviderForm.console_url = '';
|
|
searchProviderForm.priority = 1;
|
|
searchProviderForm.daily_limit = 1500;
|
|
searchProviderForm.enabled = true;
|
|
searchProviderDialogVisible.value = true;
|
|
};
|
|
const editSearchProvider = (row) => {
|
|
searchProviderDialogTitle.value = '编辑搜索API';
|
|
Object.assign(searchProviderForm, { id: row.id, name: row.name, provider_type: row.provider_type, api_key: row.api_key, api_url: row.api_url, console_url: row.console_url || '', priority: row.priority, daily_limit: row.daily_limit, enabled: row.enabled });
|
|
searchProviderDialogVisible.value = true;
|
|
};
|
|
const saveSearchProvider = async () => {
|
|
searchProviderSaving.value = true;
|
|
try {
|
|
const body = { name: searchProviderForm.name, provider_type: searchProviderForm.provider_type, api_key: searchProviderForm.api_key, api_url: searchProviderForm.api_url, console_url: searchProviderForm.console_url, priority: searchProviderForm.priority, daily_limit: searchProviderForm.daily_limit, enabled: searchProviderForm.enabled };
|
|
if (searchProviderForm.id) { await api.put(`/api/search-providers/${searchProviderForm.id}`, body); ElMessage.success('更新成功'); }
|
|
else { await api.post('/api/search-providers', body); ElMessage.success('创建成功'); }
|
|
searchProviderDialogVisible.value = false;
|
|
await loadSearchProviders();
|
|
} catch (e) { ElMessage.error('保存失败: ' + e.message); }
|
|
finally { searchProviderSaving.value = false; }
|
|
};
|
|
const deleteSearchProvider = async (id) => {
|
|
try { await ElMessageBox.confirm('确定删除该提供商吗?', '提示', { type: 'warning' }); await api.delete(`/api/search-providers/${id}`); ElMessage.success('删除成功'); await loadSearchProviders(); }
|
|
catch (e) { if (e !== 'cancel') ElMessage.error('删除失败: ' + e.message); }
|
|
};
|
|
const updateSearchProvider = async (row) => {
|
|
try { await api.put(`/api/search-providers/${row.id}`, { enabled: row.enabled }); }
|
|
catch (e) { ElMessage.error('更新失败: ' + e.message); }
|
|
};
|
|
const testSearchProvider = async (row) => {
|
|
searchProviderTesting.value = true;
|
|
searchProviderTestVisible.value = true;
|
|
searchProviderTestResult.value = { ok: false, error: '', results: [] };
|
|
try {
|
|
const r = await api.post(`/api/search-providers/${row.id}/test`, { query: '测试搜索' });
|
|
searchProviderTestResult.value = r;
|
|
} catch (e) { searchProviderTestResult.value = { ok: false, error: e.message, results: [] }; }
|
|
finally { searchProviderTesting.value = false; }
|
|
};
|
|
const testAllSearchProviders = async () => {
|
|
for (const p of searchProviders.value) {
|
|
try {
|
|
const r = await api.post(`/api/search-providers/${p.id}/test`, { query: '测试搜索' });
|
|
ElMessage({ type: r.ok ? 'success' : 'error', message: `${p.name}: ${r.ok ? 'OK' : '失败 ' + (r.error || '')}` });
|
|
} catch (e) { ElMessage.error(`${p.name} 测试失败: ${e.message}`); }
|
|
}
|
|
};
|
|
const resetSearchUsage = async () => {
|
|
try { await api.post('/api/search-providers/reset-usage'); ElMessage.success('用量已重置'); await loadSearchProviders(); }
|
|
catch (e) { ElMessage.error('重置失败: ' + e.message); }
|
|
};
|
|
|
|
const sensitiveWords = ref([]);
|
|
const swLoading = ref(false);
|
|
const swDialogVisible = ref(false);
|
|
const swSaving = ref(false);
|
|
const swForm = reactive({ word: '', category: 'general' });
|
|
const editingSwId = ref(null);
|
|
const loadSensitiveWords = async () => {
|
|
swLoading.value = true;
|
|
try { sensitiveWords.value = await api.get('/api/admin/config/sensitive-words'); }
|
|
catch (e) { console.error(e); }
|
|
finally { swLoading.value = false; }
|
|
};
|
|
const showAddSensitiveWord = () => { swForm.word = ''; swForm.category = 'general'; editingSwId.value = null; swDialogVisible.value = true; };
|
|
const editSensitiveWord = (row) => { swForm.word = row.word; swForm.category = row.category || 'general'; editingSwId.value = row.id; swDialogVisible.value = true; };
|
|
const saveSensitiveWord = async () => {
|
|
swSaving.value = true;
|
|
try {
|
|
const body = { word: swForm.word, category: swForm.category };
|
|
if (editingSwId.value) { await api.put('/api/admin/config/sensitive-words/' + editingSwId.value, body); ElMessage.success('更新成功'); }
|
|
else { await api.post('/api/admin/config/sensitive-words', body); ElMessage.success('添加成功'); }
|
|
swDialogVisible.value = false;
|
|
await loadSensitiveWords();
|
|
}
|
|
catch (e) { ElMessage.error('保存失败: ' + e.message); }
|
|
finally { swSaving.value = false; }
|
|
};
|
|
const deleteSensitiveWord = async (row) => {
|
|
try { await ElMessageBox.confirm('确定删除「' + row.word + '」吗?', '提示', { type: 'warning' }); await api.delete('/api/admin/config/sensitive-words/' + row.id); ElMessage.success('删除成功'); await loadSensitiveWords(); }
|
|
catch (e) { if (e !== 'cancel') ElMessage.error('删除失败: ' + e.message); }
|
|
};
|
|
const sensitiveWordsByCategory = computed(() => {
|
|
const map = {};
|
|
for (const w of sensitiveWords.value) {
|
|
const cat = w.category || '未分类';
|
|
if (!map[cat]) map[cat] = [];
|
|
map[cat].push(w);
|
|
}
|
|
return Object.entries(map).sort((a, b) => a[0].localeCompare(b[0]));
|
|
});
|
|
|
|
const rankings = ref([]);
|
|
const rankingStats = ref({ total_checks: 0, on_page: 0, ai_cited: 0, best_position: null });
|
|
const rankingsLoading = ref(false);
|
|
const loadRankings = async () => {
|
|
rankingsLoading.value = true;
|
|
try {
|
|
const resp = await api.get('/api/seo/rankings?limit=100');
|
|
rankings.value = resp.data || [];
|
|
const overview = await api.get('/api/seo/rankings/overview');
|
|
rankingStats.value = overview.data || { total_checks: 0, on_page: 0, ai_cited: 0, best_position: null };
|
|
} catch (e) { console.error('加载排名数据失败:', e); }
|
|
finally { rankingsLoading.value = false; }
|
|
};
|
|
|
|
const geoOverview = ref({ total_geo_checks: 0, total_cited: 0, citation_rate: 0, avg_geo_readiness: 0, engine_breakdown: {} });
|
|
const geoReadinessScores = ref([]);
|
|
const geoLoading = ref(false);
|
|
const geoTracking = ref(false);
|
|
const loadGeoData = async () => {
|
|
geoLoading.value = true;
|
|
try {
|
|
const overview = await api.get('/api/seo/geo/overview');
|
|
geoOverview.value = overview.data || { total_geo_checks: 0, total_cited: 0, citation_rate: 0, avg_geo_readiness: 0, engine_breakdown: {} };
|
|
const scores = await api.get('/api/seo/geo/readiness?limit=50');
|
|
geoReadinessScores.value = scores.data || [];
|
|
} catch (e) { console.error('加载 GEO 数据失败:', e); }
|
|
finally { geoLoading.value = false; }
|
|
};
|
|
const triggerGeoTrack = async () => {
|
|
geoTracking.value = true;
|
|
try {
|
|
const r = await api.post('/api/seo/geo/track');
|
|
ElMessage.success('GEO 追踪完成');
|
|
await loadGeoData();
|
|
} catch (e) { ElMessage.error('GEO 追踪失败: ' + e.message); }
|
|
finally { geoTracking.value = false; }
|
|
};
|
|
|
|
const contentCleanRules = ref([]);
|
|
const ccrLoading = ref(false);
|
|
const ccrDialogVisible = ref(false);
|
|
const ccrDialogTitle = ref('新增清理规则');
|
|
const ccrSaving = ref(false);
|
|
const ccrForm = reactive({ id: null, rule_type: 'ai_telltale', pattern: '', description: '', sort_order: 0, is_active: true });
|
|
const editingCcrId = ref(null);
|
|
const loadContentCleanRules = async () => {
|
|
ccrLoading.value = true;
|
|
try { contentCleanRules.value = await api.get('/api/admin/config/content-clean-rules'); }
|
|
catch (e) { console.error(e); }
|
|
finally { ccrLoading.value = false; }
|
|
};
|
|
const showAddCleanRule = () => {
|
|
ccrDialogTitle.value = '新增清理规则';
|
|
editingCcrId.value = null;
|
|
ccrForm.id = null; ccrForm.rule_type = 'ai_telltale'; ccrForm.pattern = ''; ccrForm.description = ''; ccrForm.sort_order = 0; ccrForm.is_active = true;
|
|
ccrDialogVisible.value = true;
|
|
};
|
|
const editCleanRule = (row) => {
|
|
ccrDialogTitle.value = '编辑清理规则';
|
|
editingCcrId.value = row.id;
|
|
Object.assign(ccrForm, { id: row.id, rule_type: row.rule_type, pattern: row.pattern, description: row.description || '', sort_order: row.sort_order || 0, is_active: row.is_active });
|
|
ccrDialogVisible.value = true;
|
|
};
|
|
const saveCleanRule = async () => {
|
|
ccrSaving.value = true;
|
|
try {
|
|
const body = { rule_type: ccrForm.rule_type, pattern: ccrForm.pattern, description: ccrForm.description, sort_order: ccrForm.sort_order, is_active: ccrForm.is_active };
|
|
if (editingCcrId.value) { await api.put('/api/admin/config/content-clean-rules/' + editingCcrId.value, body); ElMessage.success('更新成功'); }
|
|
else { await api.post('/api/admin/config/content-clean-rules', body); ElMessage.success('创建成功'); }
|
|
ccrDialogVisible.value = false;
|
|
await loadContentCleanRules();
|
|
} catch (e) { ElMessage.error('保存失败: ' + e.message); }
|
|
finally { ccrSaving.value = false; }
|
|
};
|
|
const deleteCleanRule = async (row) => {
|
|
try { await ElMessageBox.confirm('确定删除该规则吗?', '提示', { type: 'warning' }); await api.delete('/api/admin/config/content-clean-rules/' + row.id); ElMessage.success('删除成功'); await loadContentCleanRules(); }
|
|
catch (e) { if (e !== 'cancel') ElMessage.error('删除失败: ' + e.message); }
|
|
};
|
|
|
|
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 taskConfigs = ref([]);
|
|
const tcLoading = ref(false);
|
|
const selectedModule = ref(null);
|
|
const showHistory = ref(false);
|
|
const taskLogs = ref([]);
|
|
const tlLoading = ref(false);
|
|
|
|
const loadTaskConfigs = async () => {
|
|
tcLoading.value = true;
|
|
try {
|
|
const data = await api.get('/api/admin/task-configs');
|
|
taskConfigs.value = Array.isArray(data) ? data : (data.data || []);
|
|
} catch (e) { ElMessage.error('加载任务配置失败: ' + e.message); }
|
|
finally { tcLoading.value = false; }
|
|
};
|
|
|
|
const toggleModule = async (row, val) => {
|
|
try {
|
|
await api.put(`/api/admin/task-configs/${row.module_id}`, { enabled: val });
|
|
row.enabled = val;
|
|
ElMessage.success(val ? '已启用' : '已停用');
|
|
} catch (e) { ElMessage.error('操作失败: ' + e.message); }
|
|
};
|
|
|
|
const tcEditDialogVisible = ref(false);
|
|
const tcEditSaving = ref(false);
|
|
const tcEditForm = reactive({ module_id: '', enabled: true, schedule: '', paramsStr: '{}' });
|
|
|
|
const showEditConfig = (row) => {
|
|
tcEditForm.module_id = row.module_id;
|
|
tcEditForm.enabled = row.enabled;
|
|
tcEditForm.schedule = row.schedule || '';
|
|
tcEditForm.paramsStr = row.params ? JSON.stringify(row.params, null, 2) : '{}';
|
|
tcEditDialogVisible.value = true;
|
|
};
|
|
|
|
const saveTaskConfig = async () => {
|
|
tcEditSaving.value = true;
|
|
try {
|
|
let params;
|
|
try { params = JSON.parse(tcEditForm.paramsStr); }
|
|
catch (e) { ElMessage.warning('参数格式不是有效的 JSON'); tcEditSaving.value = false; return; }
|
|
await api.put(`/api/admin/task-configs/${tcEditForm.module_id}`, {
|
|
enabled: tcEditForm.enabled,
|
|
schedule: tcEditForm.schedule,
|
|
params: params,
|
|
});
|
|
ElMessage.success('保存成功');
|
|
tcEditDialogVisible.value = false;
|
|
await loadTaskConfigs();
|
|
} catch (e) { ElMessage.error('保存失败: ' + e.message); }
|
|
finally { tcEditSaving.value = false; }
|
|
};
|
|
|
|
const selectModule = (row) => {
|
|
selectedModule.value = row;
|
|
showHistory.value = true;
|
|
loadTaskLogs(row);
|
|
};
|
|
|
|
const loadTaskLogs = async (row) => {
|
|
tlLoading.value = true;
|
|
try {
|
|
const data = await api.get(`/api/admin/task-configs/history/${row.module_id}?limit=30`);
|
|
taskLogs.value = Array.isArray(data) ? data : (data.data || []);
|
|
} catch (e) { ElMessage.error('加载运行记录失败: ' + e.message); taskLogs.value = []; }
|
|
finally { tlLoading.value = false; }
|
|
};
|
|
|
|
const resultDialogVisible = ref(false);
|
|
const resultDialogContent = ref('');
|
|
const showResultData = (log) => {
|
|
try {
|
|
const d = typeof log.result_data === 'string' ? log.result_data : JSON.stringify(log.result_data, null, 2);
|
|
resultDialogContent.value = d;
|
|
} catch (e) { resultDialogContent.value = String(log.result_data || ''); }
|
|
resultDialogVisible.value = true;
|
|
};
|
|
|
|
const tabLoaders = {
|
|
llmconfigs: loadLLMConfigs, platformconfigs: loadPlatformConfigs, systemconfigs: loadSystemConfigs,
|
|
users: fetchUsers,
|
|
logs: loadLogTypes, orgs: loadOrgs, roles: loadRoles, menus: loadMenus, assistant: loadAssistantConfig,
|
|
searchproviders: loadSearchProviders,
|
|
searchrankings: loadRankings,
|
|
geo: loadGeoData,
|
|
configitems: () => { loadSensitiveWords(); loadContentCleanRules(); },
|
|
taskmanagement: loadTaskConfigs,
|
|
};
|
|
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);
|
|
}
|
|
};
|
|
const switchConfigSubTab = (name) => { configSubTab.value = name; };
|
|
|
|
onMounted(() => {
|
|
const hash = window.location.hash.replace('#tab=', '');
|
|
if (hash && tabLoaders[hash]) { switchTab(hash); }
|
|
else { switchTab('users'); }
|
|
});
|
|
|
|
const defaultProvider = computed(() => {
|
|
const d = [...llmConfigs.value].filter(x => x.is_default);
|
|
return d.length ? d[0].provider : '-';
|
|
});
|
|
const defaultModel = computed(() => {
|
|
const d = [...llmConfigs.value].filter(x => x.is_default);
|
|
return d.length ? d[0].model : '-';
|
|
});
|
|
const complianceProvider = computed(() => {
|
|
const c = [...llmConfigs.value].filter(x => x.is_active && x.model === 'deepseek-v4-flash');
|
|
return c.length ? c[0].provider : '-';
|
|
});
|
|
const complianceModel = computed(() => {
|
|
const c = [...llmConfigs.value].filter(x => x.is_active && x.model === 'deepseek-v4-flash');
|
|
return c.length ? c[0].model : '-';
|
|
});
|
|
return {
|
|
defaultProvider, defaultModel, complianceProvider, complianceModel,
|
|
activeTab, switchTab,
|
|
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,
|
|
roles, rolesLoading, roleDialogVisible, roleDialogTitle, roleSaving, roleForm, loadRoles, showRoleDialog, saveRole, deleteRole,
|
|
menus, menusLoading, menuDialogVisible, menuDialogTitle, menuSaving, menuForm, loadMenus, showMenuDialog, saveMenu, deleteMenu,
|
|
logout, currentUser, isAdmin, redirectToPage,
|
|
users, usersLoading, userDialogVisible, userDialogTitle, userSubmitting, userForm, userPage, userPageSize, paginatedUsers,
|
|
fetchUsers, addUser, showEditUserDialog, submitUser, deleteUser,
|
|
logType, logTypes, logDate, logContent, logsLoading, fetchLogs,
|
|
assistantPrompt, assistantEnabled, assistantSaving, saveAssistantPrompt,
|
|
searchProviders, searchProvidersLoading, searchProviderDialogVisible, searchProviderDialogTitle, searchProviderSaving, searchProviderForm,
|
|
searchProviderTestVisible, searchProviderTesting, searchProviderTestResult,
|
|
loadSearchProviders, addSearchProvider, editSearchProvider, saveSearchProvider, deleteSearchProvider, updateSearchProvider,
|
|
testSearchProvider, testAllSearchProviders, resetSearchUsage,
|
|
sensitiveWords, swLoading, swDialogVisible, swSaving, swForm, editingSwId, loadSensitiveWords, showAddSensitiveWord, editSensitiveWord, saveSensitiveWord, deleteSensitiveWord, configSubTab, switchConfigSubTab,
|
|
contentCleanRules, ccrLoading, ccrDialogVisible, ccrDialogTitle, ccrSaving, ccrForm, editingCcrId,
|
|
loadContentCleanRules, showAddCleanRule, editCleanRule, saveCleanRule, deleteCleanRule,
|
|
rankings, rankingStats, rankingsLoading, loadRankings,
|
|
geoOverview, geoReadinessScores, geoLoading, geoTracking, loadGeoData, triggerGeoTrack,
|
|
taskConfigs, tcLoading, selectedModule, showHistory, taskLogs, tlLoading,
|
|
loadTaskConfigs, selectModule, toggleModule, loadTaskLogs,
|
|
tcEditDialogVisible, tcEditSaving, tcEditForm, showEditConfig, saveTaskConfig,
|
|
resultDialogVisible, resultDialogContent, showResultData,
|
|
};
|
|
}
|
|
});
|
|
|
|
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>
|