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,22 +7,23 @@
|
||||
<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; }
|
||||
.main-content { display: flex; flex: 1; max-width: 1400px; margin: 0 auto; }
|
||||
.content-area { flex: 1; padding: 32px; 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); }
|
||||
.content-area { padding: 32px; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.content-area { padding: 12px; padding-bottom: 80px; }
|
||||
.calendar-day { min-height: 56px !important; padding: 2px; }
|
||||
.calendar-weekday { padding: 6px; font-size: 12px; }
|
||||
.day-entry { font-size: 10px; padding: 2px 4px; white-space: normal; }
|
||||
.content-area { padding: 12px !important; padding-bottom: 80px !important; }
|
||||
.card { padding: 12px !important; }
|
||||
.calendar-header { flex-direction: column; align-items: flex-start; gap: 8px; }
|
||||
.calendar-title { font-size: 20px; }
|
||||
.calendar-nav { width: 100%; justify-content: space-between; }
|
||||
.calendar-grid { gap: 2px; }
|
||||
.calendar-weekday { padding: 4px; font-size: 11px; }
|
||||
.calendar-day { min-height: 56px !important; padding: 2px; }
|
||||
.day-number { font-size: 12px; margin-bottom: 0; }
|
||||
.day-lunar { font-size: 9px; margin-top: -1px; }
|
||||
.day-term { font-size: 8px; padding: 0 3px; }
|
||||
.day-holiday { font-size: 8px; padding: 0 3px; }
|
||||
.day-lunar { font-size: 9px; margin-top: -1px; display: none; }
|
||||
.day-term { font-size: 8px; padding: 0 2px; }
|
||||
.day-holiday { font-size: 8px; padding: 0 2px; }
|
||||
.day-entry { font-size: 10px; padding: 2px 3px; white-space: normal; line-height: 1.2; }
|
||||
.stats-bar { gap: 12px; font-size: 12px; }
|
||||
}
|
||||
|
||||
.calendar-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; flex-wrap: wrap; gap: 12px; }
|
||||
@@ -39,11 +40,16 @@
|
||||
.day-term { display: inline-block; font-size: 9px; color: #E6A23C; background: #fdf6ec; border-radius: 3px; padding: 0 4px; margin-top: 1px; font-weight: 500; white-space: nowrap; }
|
||||
.day-holiday { display: inline-block; font-size: 9px; color: #F56C6C; background: #fef0f0; border-radius: 3px; padding: 0 4px; margin-top: 1px; font-weight: 500; white-space: nowrap; }
|
||||
.day-entries { display: flex; flex-direction: column; gap: 4px; }
|
||||
.day-entry { font-size: 11px; padding: 4px 6px; border-radius: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: pointer; }
|
||||
.day-entry { font-size: 11px; padding: 4px 6px; border-radius: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: pointer; display:flex; align-items:center; gap:3px; }
|
||||
.day-entry.planned { background: #fdf6ec; color: #E6A23C; }
|
||||
.day-entry.published { background: #f0f9eb; color: #67C23A; }
|
||||
.day-entry.delayed { background: #fef0f0; color: #F56C6C; }
|
||||
.day-entry.cancelled { background: #f4f4f5; color: #909399; }
|
||||
.entry-topic-status { font-size:9px; padding:1px 3px; border-radius:2px; margin-left:auto; flex-shrink:0; }
|
||||
.entry-topic-status.review { background:#ecf5ff; color:#409eff; }
|
||||
.entry-topic-status.ready { background:#fdf6ec; color:#e6a23c; }
|
||||
.entry-topic-status.published { background:#f0f9eb; color:#67c23a; }
|
||||
.entry-topic-status.pending { background:#f4f4f5; color:#909399; }
|
||||
|
||||
.stats-bar { display: flex; gap: 24px; margin-bottom: 24px; flex-wrap: wrap; }
|
||||
.stat-item { display: flex; align-items: center; gap: 8px; }
|
||||
@@ -53,40 +59,37 @@
|
||||
.stat-dot.delayed { background: #F56C6C; }
|
||||
.stat-dot.cancelled { background: #909399; }
|
||||
</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="calendar"
|
||||
:is-admin="isAdmin"
|
||||
@navigate="redirectToPage"
|
||||
></navigation-component>
|
||||
<uni-nav title="内容日历" :username="currentUser.username" :is-admin="isAdmin" current-page="calendar" @navigate="redirectToPage" @logout="handleLogout">
|
||||
</uni-nav>
|
||||
<div class="main-content">
|
||||
<main class="content-area">
|
||||
<div class="card page-fade">
|
||||
<div class="calendar-header">
|
||||
<h2 class="calendar-title">📅 内容日历</h2>
|
||||
<h2 class="calendar-title"><el-icon style="vertical-align:-2px;"><IconCalendar /></el-icon> 内容日历</h2>
|
||||
<div class="calendar-nav">
|
||||
<el-button @click="prevMonth" size="large">◀</el-button>
|
||||
<el-button @click="prevMonth" size="large"><el-icon><IconArrowLeft /></el-icon></el-button>
|
||||
<span style="font-size: 20px; font-weight: 600;">{{ currentYear }}年 {{ currentMonth }}月</span>
|
||||
<el-button @click="nextMonth" size="large">▶</el-button>
|
||||
<el-button @click="nextMonth" size="large"><el-icon><IconArrowRight /></el-icon></el-button>
|
||||
<el-button type="primary" @click="goToday">今天</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-bar">
|
||||
<div class="stat-item"><span class="stat-dot planned"></span> 待发布: {{ stats.planned }}</div>
|
||||
<div class="stat-item"><span class="stat-dot published"></span> 已发布: {{ stats.published }}</div>
|
||||
<div class="stat-item"><span class="stat-dot delayed"></span> 延迟: {{ stats.delayed }}</div>
|
||||
<div class="stat-item"><span class="stat-dot cancelled"></span> 取消: {{ stats.cancelled }}</div>
|
||||
<div v-if="loadingCalendar" style="color:#909399;font-size:14px;">加载中...</div>
|
||||
<div v-else-if="!calendarDays.length" class="empty-state">
|
||||
<el-icon style="font-size:48px;color:#c0c4cc;"><IconCalendar /></el-icon>
|
||||
<div class="empty-text">暂无日历数据</div>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="stat-item"><span class="stat-dot planned"></span> 待发布: {{ stats.planned }}</div>
|
||||
<div class="stat-item"><span class="stat-dot published"></span> 已发布: {{ stats.published }}</div>
|
||||
<div class="stat-item"><span class="stat-dot delayed"></span> 延迟: {{ stats.delayed }}</div>
|
||||
<div class="stat-item"><span class="stat-dot cancelled"></span> 取消: {{ stats.cancelled }}</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="calendar-grid">
|
||||
@@ -104,7 +107,9 @@
|
||||
class="day-entry"
|
||||
:class="entry.status"
|
||||
@click.stop="openEntryDialog(entry)">
|
||||
{{ entry.platform_icon }} {{ entry.title || '未命名' }}
|
||||
<span v-if="entry.platform_icon" style="flex-shrink:0;">{{ entry.platform_icon }}</span><el-icon v-else style="flex-shrink:0;"><IconDocument /></el-icon>
|
||||
<span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">{{ entry.title || '未命名' }}</span>
|
||||
<span v-if="entry.topic_status" class="entry-topic-status" :class="entry.topic_status">{{ topicStatusLabel(entry.topic_status) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -122,7 +127,7 @@
|
||||
<template #default="scope">{{ platformName(scope.row.platform) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="80">
|
||||
<template #default="scope"><el-tag :type="statusType(scope.row.status)" size="small">{{ statusLabel(scope.row.status) }}</template>
|
||||
<template #default="scope"><el-tag :type="statusType(scope.row.status)" size="small">{{ statusLabel(scope.row.status) }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120">
|
||||
<template #default="scope">
|
||||
@@ -248,11 +253,12 @@ function getDayMeta(year, month, day) {
|
||||
}
|
||||
</script>
|
||||
<script src="vue.global.prod.js"></script>
|
||||
<script src="icon-components.js"></script>
|
||||
<script src="element-plus.full.js"></script>
|
||||
<script>
|
||||
const { ref, reactive, computed, onMounted } = Vue;
|
||||
const CalendarApp = {
|
||||
components: { 'navbar-component': window.NavbarComponent, 'navigation-component': window.NavigationComponent },
|
||||
|
||||
setup() {
|
||||
const currentUser = ref({ username: '' });
|
||||
const isAdmin = ref(false);
|
||||
@@ -313,14 +319,20 @@ function getDayMeta(year, month, day) {
|
||||
return days;
|
||||
});
|
||||
|
||||
const loadingCalendar = ref(false);
|
||||
const calendarError = ref('');
|
||||
|
||||
const fetchEntries = async () => {
|
||||
try { entries.value = await api(`/api/calendar?year=${currentYear.value}&month=${currentMonth.value}`); } catch (e) { console.error(e); }
|
||||
loadingCalendar.value = true;
|
||||
calendarError.value = '';
|
||||
try { entries.value = await api(`/api/calendar?year=${currentYear.value}&month=${currentMonth.value}`); } catch (e) { console.error(e); calendarError.value = e.message; ElMessage.error('加载日历失败: ' + e.message); }
|
||||
finally { loadingCalendar.value = false; }
|
||||
};
|
||||
const fetchStats = async () => {
|
||||
try { stats.value = await api(`/api/calendar/stats?year=${currentYear.value}&month=${currentMonth.value}`); } catch (e) { console.error(e); }
|
||||
try { stats.value = await api(`/api/calendar/stats?year=${currentYear.value}&month=${currentMonth.value}`); } catch (e) { console.error(e); ElMessage.error('加载统计失败: ' + e.message); }
|
||||
};
|
||||
const fetchTopics = async () => {
|
||||
try { const res = await api('/api/topics?limit=100'); topics.value = res; } catch (e) { console.error(e); }
|
||||
try { const res = await api('/api/topics?limit=100'); topics.value = res; } catch (e) { console.error(e); ElMessage.error('加载选题失败: ' + e.message); }
|
||||
};
|
||||
|
||||
const prevMonth = () => { if (currentMonth.value === 1) { currentMonth.value = 12; currentYear.value--; } else { currentMonth.value--; } fetchEntries(); fetchStats(); };
|
||||
@@ -338,21 +350,23 @@ function getDayMeta(year, month, day) {
|
||||
if (isEdit.value) { await api(`/api/calendar/entries/${data.id}`, { method: 'PUT', body: JSON.stringify(data) }); }
|
||||
else { await api('/api/calendar/entries', { method: 'POST', body: JSON.stringify(data) }); }
|
||||
entryDialogVisible.value = false;
|
||||
ElMessage.success('保存成功');
|
||||
await fetchEntries();
|
||||
await fetchStats();
|
||||
} catch (e) { alert('保存失败: ' + e.message); }
|
||||
} catch (e) { ElMessage.error('保存失败: ' + e.message); }
|
||||
saving.value = false;
|
||||
};
|
||||
const deleteEntry = async () => {
|
||||
if (!confirm('确定删除?')) return;
|
||||
try { await ElMessageBox.confirm('确定删除?', '提示', { type: 'warning' }); } catch { return; }
|
||||
saving.value = true;
|
||||
try { await api(`/api/calendar/entries/${entryForm.value.id}`, { method: 'DELETE' }); entryDialogVisible.value = false; await fetchEntries(); await fetchStats(); } catch (e) { alert('删除失败: ' + e.message); }
|
||||
try { await api(`/api/calendar/entries/${entryForm.value.id}`, { method: 'DELETE' }); entryDialogVisible.value = false; ElMessage.success('删除成功'); await fetchEntries(); await fetchStats(); } catch (e) { ElMessage.error('删除失败: ' + e.message); }
|
||||
saving.value = false;
|
||||
};
|
||||
|
||||
const platformName = (p) => ({ zhihu: '知乎', wechat: '微信公众号', xiaohongshu: '小红书' }[p] || p);
|
||||
const statusLabel = (s) => ({ planned: '待发布', published: '已发布', delayed: '延迟', cancelled: '取消' }[s] || s);
|
||||
const statusType = (s) => ({ planned: 'warning', published: 'success', delayed: 'danger', cancelled: 'info' }[s] || '');
|
||||
const topicStatusLabel = (s) => ({ pending: '待处理', review: '待审查', ready: '待发布', published: '已发布' }[s] || s);
|
||||
|
||||
const checkAuth = () => {
|
||||
const token = localStorage.getItem('authToken');
|
||||
@@ -384,15 +398,15 @@ function getDayMeta(year, month, day) {
|
||||
checkAuth();
|
||||
});
|
||||
|
||||
return { currentUser, isAdmin, currentYear, currentMonth, weekDays, calendarDays, entries, topics, stats, dayDialogVisible, entryDialogVisible, selectedDay, selectedDayEntries, isEdit, saving, entryForm, prevMonth, nextMonth, goToday, openDayDialog, openCreateDialog, openEntryDialog, saveEntry, deleteEntry, platformName, statusLabel, statusType, handleLogout, redirectToPage };
|
||||
return { currentUser, isAdmin, currentYear, currentMonth, weekDays, calendarDays, entries, topics, stats, dayDialogVisible, entryDialogVisible, selectedDay, selectedDayEntries, isEdit, saving, entryForm, prevMonth, nextMonth, goToday, openDayDialog, openCreateDialog, openEntryDialog, saveEntry, deleteEntry, platformName, statusLabel, statusType, topicStatusLabel, handleLogout, redirectToPage };
|
||||
}
|
||||
};
|
||||
|
||||
const app = Vue.createApp(CalendarApp);
|
||||
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>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user