feat: Phase 4 多租户隔离 + 四阶段升级测试 + CSS 统一化
Phase 4: org_id 注入 JWT/API 过滤/组织管理 CRUD/前端组织列 测试: tests/test_phase_upgrades.py 97项全覆盖 CSS: theme-modern.css 共享 mobile-card-list/status-dot/search-bar 等模式 修复: initial_data.py LLM配置 NOT NULL 约束, TopicResponse 含 org_id
This commit is contained in:
@@ -7,17 +7,7 @@
|
||||
<link rel="stylesheet" href="element-plus.css">
|
||||
<link rel="stylesheet" href="theme-modern.css">
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background: #f5f7fa; color: #303133; }
|
||||
|
||||
.main-content { display: flex; flex: 1; max-width: 1400px; margin: 0 auto; min-height: calc(100vh - 60px); }
|
||||
.content-area { flex: 1; padding: 24px; overflow-y: auto; }
|
||||
.card { background: white; border-radius: 16px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); overflow-x: auto; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); }
|
||||
|
||||
|
||||
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; flex-wrap: wrap; gap: 12px; }
|
||||
.page-title { font-size: 24px; font-weight: 700; color: #303133; display: flex; align-items: center; gap: 8px; }
|
||||
.toolbar { display: flex; gap: 12px; flex-wrap: wrap; align-items: center; }
|
||||
.toolbar { gap: 12px; }
|
||||
|
||||
.task-card { border: 1px solid #ebeef5; border-radius: 12px; padding: 16px; margin-bottom: 12px; transition: all 0.3s; background: #fff; }
|
||||
.task-card:hover { border-color: #409eff; box-shadow: 0 2px 12px rgba(64,158,255,0.12); transform: translateY(-1px); }
|
||||
@@ -56,38 +46,35 @@
|
||||
.detail-value { flex: 1; font-size: 13px; color: #303133; word-break: break-all; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.content-area { padding: 16px; padding-bottom: 80px; }
|
||||
.card { padding: 16px; }
|
||||
.task-table { display: none; }
|
||||
.task-card-list-mobile { display: block; }
|
||||
.schedule-row { padding: 10px 12px; gap: 10px; flex-wrap: wrap; }
|
||||
.schedule-next { width: 100%; margin-left: 46px; }
|
||||
}
|
||||
</style>
|
||||
<script src="navigation-component.js"></script>
|
||||
<script src="navbar-component.js"></script>
|
||||
<script src="uni-nav.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<navbar-component title="创作任务" :username="currentUser.username" :is-admin="isAdmin" @logout="handleLogout"></navbar-component>
|
||||
<navigation-component current-page="tasks" :is-admin="isAdmin" @navigate="redirectToPage"></navigation-component>
|
||||
<uni-nav title="创作任务" :username="currentUser.username" :is-admin="isAdmin" current-page="tasks" @navigate="redirectToPage" @logout="handleLogout">
|
||||
</uni-nav>
|
||||
<div class="main-content">
|
||||
<main class="content-area">
|
||||
<div class="card page-fade">
|
||||
<div class="page-header">
|
||||
<h2 class="page-title">🚀 定时任务</h2>
|
||||
<h2 class="page-title"><el-icon style="vertical-align:-2px;"><IconMenu /></el-icon> 定时任务</h2>
|
||||
<div class="toolbar">
|
||||
<el-button @click="loadSchedulerStatus">🔄 刷新</el-button>
|
||||
<el-button @click="loadSchedulerStatus"><el-icon style="vertical-align:-2px;"><IconRefresh /></el-icon> 刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="schedulerLoading" style="text-align: center; padding: 20px; color: #909399;">加载中...</div>
|
||||
<div v-else>
|
||||
<div v-if="schedulerJobs.length === 0" style="text-align: center; padding: 30px 20px; color: #909399; font-size: 14px;">暂无定时任务</div>
|
||||
<div v-for="job in schedulerJobs" :key="job.id" class="schedule-row">
|
||||
<div class="schedule-icon">{{ job.icon }}</div>
|
||||
<div class="schedule-icon"><el-icon><component :is="job.icon" /></el-icon></div>
|
||||
<div class="schedule-info">
|
||||
<div class="schedule-name">{{ job.name }}</div>
|
||||
<div class="schedule-time">⏰ 每日 {{ job.time }}</div>
|
||||
<div class="schedule-time"><el-icon style="vertical-align:-2px;"><IconClock /></el-icon> 每日 {{ job.time }}</div>
|
||||
</div>
|
||||
<div class="schedule-next">
|
||||
<span class="schedule-badge" :class="job.active ? 'active' : 'inactive'"></span>
|
||||
@@ -100,7 +87,7 @@
|
||||
|
||||
<div class="card page-fade">
|
||||
<div class="page-header">
|
||||
<h2 class="page-title">🚀 创作任务</h2>
|
||||
<h2 class="page-title"><el-icon style="vertical-align:-2px;"><IconMenu /></el-icon> 创作任务</h2>
|
||||
<div class="toolbar">
|
||||
<el-button-group>
|
||||
<el-button :type="filterStatus === '' ? 'primary' : ''" @click="filterStatus = ''; loadTasks()">全部</el-button>
|
||||
@@ -109,12 +96,12 @@
|
||||
<el-button :type="filterStatus === 'completed' ? 'primary' : ''" @click="filterStatus = 'completed'; loadTasks()">已完成</el-button>
|
||||
<el-button :type="filterStatus === 'failed' ? 'primary' : ''" @click="filterStatus = 'failed'; loadTasks()">失败</el-button>
|
||||
</el-button-group>
|
||||
<el-button @click="loadTasks">🔄 刷新</el-button>
|
||||
<el-button @click="loadTasks"><el-icon style="vertical-align:-2px;"><IconRefresh /></el-icon> 刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="loading" style="text-align: center; padding: 40px; color: #909399;">加载中...</div>
|
||||
<div v-else-if="tasks.length === 0" style="text-align: center; padding: 60px 20px; color: #909399;">
|
||||
<div style="font-size: 56px; margin-bottom: 16px;">📋</div>
|
||||
<el-icon style="font-size:56px;margin-bottom:16px;color:#c0c4cc;"><IconTopic /></el-icon>
|
||||
<div style="font-size: 16px;">暂无任务</div>
|
||||
<div style="margin-top: 12px; font-size: 14px;">前往 <a href="/topics.html" style="color: #409eff;">选题管理</a> 创建新任务</div>
|
||||
</div>
|
||||
@@ -173,14 +160,15 @@
|
||||
</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 TasksApp = {
|
||||
data() {
|
||||
const SCHEDULER_JOBS = {
|
||||
'scheduled_sync': { icon: '🔄', name: '数据同步', defaultTime: '02:30' },
|
||||
'scheduled_generate': { icon: '🤖', name: '内容创作', defaultTime: '03:30' },
|
||||
'scheduled_optimize': { icon: '🔍', name: '合规审查', defaultTime: '04:30' },
|
||||
'scheduled_sync': { icon: 'IconRefresh', name: '数据同步', defaultTime: '02:30' },
|
||||
'scheduled_generate': { icon: 'IconDocument', name: '内容创作', defaultTime: '03:30' },
|
||||
'scheduled_optimize': { icon: 'IconSearch', name: '合规审查', defaultTime: '04:30' },
|
||||
};
|
||||
return {
|
||||
currentUser: { username: '' }, isAdmin: false, isLoggedIn: false,
|
||||
@@ -215,7 +203,7 @@ const TasksApp = {
|
||||
const data = await this.api('/api/system/scheduler/status');
|
||||
if (!data) return;
|
||||
this.schedulerJobs = (data.jobs || []).map(job => {
|
||||
const info = this.SCHEDULER_JOBS[job.id] || { icon: '⏰', name: job.id, defaultTime: '' };
|
||||
const info = this.SCHEDULER_JOBS[job.id] || { icon: 'IconClock', name: job.id, defaultTime: '' };
|
||||
// Parse cron trigger for time display
|
||||
let time = info.defaultTime;
|
||||
const m = job.trigger && job.trigger.match(/hour='?(\d+)'?,\s*minute='?(\d+)'?/);
|
||||
@@ -227,7 +215,7 @@ const TasksApp = {
|
||||
}
|
||||
return { id: job.id, icon: info.icon, name: info.name, time, active: data.running, next_run };
|
||||
});
|
||||
} catch (e) { console.error(e); }
|
||||
} catch (e) { console.error(e); this.$message.error('加载调度状态失败: ' + e.message); }
|
||||
finally { this.schedulerLoading = false; }
|
||||
},
|
||||
getStatusLabel(status) { return { 'pending': '等待中', 'running': '进行中', 'completed': '已完成', 'failed': '失败', 'cancelled': '已取消' }[status] || status; },
|
||||
@@ -242,7 +230,7 @@ const TasksApp = {
|
||||
let url = '/api/tasks?limit=50';
|
||||
if (this.filterStatus) url += '&status=' + this.filterStatus;
|
||||
this.tasks = await this.api(url) || [];
|
||||
} catch (e) { console.error(e); }
|
||||
} catch (e) { console.error(e); this.$message.error('加载任务列表失败: ' + e.message); }
|
||||
finally { this.loading = false; }
|
||||
},
|
||||
viewTaskDetail(task) { this.detailTask = task; this.showDetailDialog = true; },
|
||||
@@ -267,8 +255,8 @@ const TasksApp = {
|
||||
};
|
||||
const app = Vue.createApp(TasksApp);
|
||||
app.use(ElementPlus);
|
||||
if (window.installNavbar) { window.installNavbar(app); }
|
||||
if (window.installNavigation) { window.installNavigation(app); } else if (window.NavigationComponent) { app.component("navigation-component", window.NavigationComponent); }
|
||||
if (window.installIcons) { window.installIcons(app); }
|
||||
if (window.installUniNav) { window.installUniNav(app); }
|
||||
app.mount('#app');
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user