新增用户管理/角色管理/菜单管理功能,修复创作流水线研究脚本
- 用户管理:新增编辑弹窗(修改用户名/角色/密码),增加组织/创建时间/最后登录列 - 角色管理:新增 Role 模型 + CRUD API,admin.html 新增角色管理 tab - 菜单管理:新增 Menu 模型 + CRUD API,导航栏从 API 动态加载菜单项 - 个人中心:右上角下拉菜单(个人信息/修改密码/退出),新增修改密码 API - 种子数据:initial_data.py 自动创建默认角色(admin/editor)和默认菜单(7项) - 修复 research.py 缺少 enrich_topic_research 函数导致导入失败 - 修复 db_helper.py 中 generated_at 条件导致重创作不更新时间戳 - admin.html 操作列加宽防止按钮换行,平台配置增加删除按钮 - articles.html 预览弹窗加 lock-scroll=false 防止页面尺寸跳动
This commit is contained in:
@@ -19,7 +19,8 @@
|
||||
.module-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--spacing-md); }
|
||||
.module-title { font-size: var(--font-size-h3); font-weight: 600; color: var(--color-text-primary); display: flex; align-items: center; gap: var(--spacing-sm); }
|
||||
.module-status { padding: 6px 12px; border-radius: var(--radius-xl); font-size: var(--font-size-caption); font-weight: 600; background: rgba(103,194,58,0.12); color: #67c23a; border: 1px solid rgba(103,194,58,0.25); }
|
||||
.module-status.running { animation: pulse-glow 2s infinite; }
|
||||
.module-status.inactive { background: rgba(144,147,153,0.12); color: #909399; border: 1px solid rgba(144,147,153,0.25); }
|
||||
.module-drawer .el-drawer__body { padding: 20px; overflow-y: auto; }
|
||||
.module-content { font-size: var(--font-size-body); color: var(--color-text-regular); line-height: 1.8; }
|
||||
.module-content div { display: flex; justify-content: space-between; padding: 4px 0; border-bottom: 1px dashed var(--color-border); }
|
||||
.module-content div:last-child { border-bottom: none; }
|
||||
@@ -95,18 +96,31 @@
|
||||
</span>
|
||||
</h3>
|
||||
<div class="module-grid">
|
||||
<div v-for="mod in modules" :key="mod.id" class="module-card">
|
||||
<div v-for="mod in modules" :key="mod.module_id" class="module-card">
|
||||
<div class="module-header">
|
||||
<span class="module-title">{{ mod.title }}</span>
|
||||
<span :class="['module-status', mod.status === 'running' ? 'running' : '']">{{ mod.status === 'running' ? '运行中' : '已停止' }}</span>
|
||||
<span :class="['module-status', mod.enabled ? '' : 'inactive']">
|
||||
<el-icon v-if="mod.enabled" style="vertical-align:-2px;"><IconCheck /></el-icon>
|
||||
<el-icon v-else style="vertical-align:-2px;"><IconClose /></el-icon>
|
||||
{{ mod.enabled ? '已启用' : '已禁用' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="module-content">
|
||||
<div><span>最后运行</span><span>{{ mod.last_run }}</span></div>
|
||||
<div><span>下次运行</span><span>{{ mod.next_run }}</span></div>
|
||||
<div><span>今日任务</span><span>{{ mod.task_count }} 个</span></div>
|
||||
<div><span>成功率</span><span>{{ mod.success_rate > 0 ? mod.success_rate + '%' : '暂无' }}</span></div>
|
||||
<div><span>执行时间</span><span>{{ mod.schedule || mod.cron_default }}</span></div>
|
||||
<div><span>成功/失败</span><span style="color:#67c23a;">{{ mod.success_runs }}</span> / <span style="color:#f56c6c;">{{ mod.failed_runs }}</span></div>
|
||||
<div><span>今日任务</span><span>{{ mod.total_runs }} 次</span></div>
|
||||
<div><span>下次运行</span><span style="color:#409eff;">{{ mod.next_run || '—' }}</span></div>
|
||||
<div v-if="mod.last_result && mod.last_result.topics_count" style="border-bottom:none;">
|
||||
<span>最新产出</span>
|
||||
<span style="color:#409eff;font-weight:600;">{{ mod.last_result.topics_count }} 个选题</span>
|
||||
</div>
|
||||
<div v-else-if="mod.last_result && mod.last_result.articles_synced" style="border-bottom:none;">
|
||||
<span>最新产出</span>
|
||||
<span style="color:#409eff;font-weight:600;">{{ mod.last_result.articles_synced }} 篇</span>
|
||||
</div>
|
||||
<div style="margin-top:10px; border-bottom:none;">
|
||||
<el-button size="small" type="primary" @click="triggerModule(mod.id)" :loading="runningModule === mod.id">立即运行</el-button>
|
||||
<el-button v-if="isAdmin" size="small" type="primary" @click="openModuleDetail(mod)">详情</el-button>
|
||||
<el-button size="small" @click="triggerModule(mod.module_id || mod.id)" :loading="runningModule === (mod.module_id || mod.id)" :disabled="!mod.enabled">立即运行</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -132,6 +146,56 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 模块详情抽屉 -->
|
||||
<el-drawer v-model="showModuleDrawer" :title="(moduleDetail ? moduleDetail.title : '') + ' 详情'" size="50%" direction="rtl" class="module-drawer">
|
||||
<div v-if="moduleDetailLoading" style="text-align:center;padding:40px;color:#909399;">加载中...</div>
|
||||
<div v-else-if="moduleDetail" style="padding:0 20px;">
|
||||
<div style="margin-bottom:16px;display:flex;gap:16px;align-items:center;">
|
||||
<el-tag :type="moduleDetail.enabled ? 'success' : 'info'" size="small">{{ moduleDetail.enabled ? '已启用' : '已禁用' }}</el-tag>
|
||||
<span style="font-size:13px;color:#909399;">执行时间: {{ moduleDetail.schedule || moduleDetail.cron_default }}</span>
|
||||
</div>
|
||||
<el-tabs style="height:calc(100vh - 200px);">
|
||||
<el-tab-pane label="📋 运行记录" name="history" style="overflow:auto;">
|
||||
<div v-if="moduleDetailHistory && moduleDetailHistory.length > 0">
|
||||
<el-timeline>
|
||||
<el-timeline-item v-for="(h, i) in moduleDetailHistory" :key="h.id || i"
|
||||
:timestamp="h.started_at ? new Date(h.started_at).toLocaleString('zh-CN', {timeZone:'Asia/Shanghai'}) : '—'"
|
||||
:color="h.status === 'success' ? '#67c23a' : h.status === 'running' ? '#e6a23c' : '#f56c6c'">
|
||||
<div style="font-size:13px;">
|
||||
<el-tag :type="h.status === 'success' ? 'success' : h.status === 'running' ? 'warning' : 'danger'" size="small" style="margin-right:6px;">{{ h.status }}</el-tag>
|
||||
<span v-if="h.message">{{ h.message }}</span>
|
||||
<span v-else style="color:#909399;">—</span>
|
||||
</div>
|
||||
<div v-if="h.error_trace" style="margin-top:4px;padding:6px 8px;background:#fef0f0;border-radius:4px;font-size:12px;color:#f56c6c;max-height:60px;overflow:auto;">{{ h.error_trace }}</div>
|
||||
<div style="font-size:12px;color:#c0c4cc;margin-top:2px;">耗时 {{ h.duration ? h.duration + 's' : '—' }} · {{ h.triggered_by }}</div>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
</div>
|
||||
<div v-else style="color:#909399;padding:20px;text-align:center;">暂无运行记录</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="🤖 提示词" name="prompts" style="overflow:auto;">
|
||||
<div v-if="moduleDetailPrompts && moduleDetailPrompts.length > 0">
|
||||
<div v-for="p in moduleDetailPrompts" :key="p.id" style="margin-bottom:12px;border:1px solid #ebeef5;border-radius:10px;padding:12px;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:6px;">
|
||||
<div>
|
||||
<el-tag size="small" type="info" style="margin-right:6px;">{{ p.category }}</el-tag>
|
||||
<span style="font-weight:600;font-size:13px;">{{ p.key }}</span>
|
||||
<div style="font-size:11px;color:#909399;margin-top:2px;">{{ p.description }}</div>
|
||||
</div>
|
||||
<el-switch v-model="p.enabled" @change="savePrompt(p)" size="small" :disabled="promptSaving === p.id"></el-switch>
|
||||
</div>
|
||||
<el-input type="textarea" v-model="p.content" :rows="5" style="font-family:monospace;font-size:12px;" @blur="savePrompt(p)"></el-input>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else style="color:#909399;padding:20px;text-align:center;">此模块暂无提示词配置</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<div style="padding-top:16px;border-top:1px solid #ebeef5;text-align:center;">
|
||||
<el-button type="primary" @click="triggerModule(moduleDetail.module_id)" :loading="runningModule === moduleDetail.module_id" :disabled="!moduleDetail.enabled">立即运行此任务</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -163,7 +227,13 @@
|
||||
schedulerRunning: false,
|
||||
upcomingEntries: [],
|
||||
loadingPlan: false,
|
||||
runningModule: null
|
||||
runningModule: null,
|
||||
showModuleDrawer: false,
|
||||
moduleDetail: null,
|
||||
moduleDetailLoading: false,
|
||||
moduleDetailHistory: [],
|
||||
moduleDetailPrompts: [],
|
||||
promptSaving: null
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
@@ -311,6 +381,40 @@
|
||||
},
|
||||
redirectToPage(page) {
|
||||
window.location.href = page.startsWith('/') ? page : '/' + page;
|
||||
},
|
||||
async openModuleDetail(mod) {
|
||||
this.showModuleDrawer = true;
|
||||
this.moduleDetail = mod;
|
||||
this.moduleDetailLoading = true;
|
||||
this.moduleDetailHistory = [];
|
||||
this.moduleDetailPrompts = [];
|
||||
const token = localStorage.getItem('authToken');
|
||||
try {
|
||||
const [history, prompts] = await Promise.all([
|
||||
fetch('/api/admin/task-configs/history/' + mod.module_id + '?limit=20', {
|
||||
headers: { 'Authorization': 'Bearer ' + token }
|
||||
}).then(r => r.ok ? r.json() : []),
|
||||
fetch('/api/admin/prompt-configs?module_id=' + mod.module_id, {
|
||||
headers: { 'Authorization': 'Bearer ' + token }
|
||||
}).then(r => r.ok ? r.json() : [])
|
||||
]);
|
||||
this.moduleDetailHistory = history || [];
|
||||
this.moduleDetailPrompts = prompts || [];
|
||||
} catch (e) { this.$message.error('加载详情失败: ' + e.message); }
|
||||
finally { this.moduleDetailLoading = false; }
|
||||
},
|
||||
async savePrompt(p) {
|
||||
this.promptSaving = p.id;
|
||||
const token = localStorage.getItem('authToken');
|
||||
try {
|
||||
await fetch('/api/admin/prompt-configs/' + p.id, {
|
||||
method: 'PUT',
|
||||
headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ content: p.content, enabled: p.enabled, description: p.description, temperature: p.temperature, max_tokens: p.max_tokens })
|
||||
});
|
||||
this.$message.success('已保存');
|
||||
} catch (e) { this.$message.error('保存失败: ' + e.message); }
|
||||
finally { this.promptSaving = null; }
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
||||
Reference in New Issue
Block a user