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:
@@ -0,0 +1,230 @@
|
||||
(function () {
|
||||
if (document.getElementById('uni-nav-styles')) return;
|
||||
var style = document.createElement('style');
|
||||
style.id = 'uni-nav-styles';
|
||||
style.textContent = '\
|
||||
.uni-nav {\
|
||||
position: fixed; top: 0; left: 0; right: 0; height: 60px;\
|
||||
z-index: 10000;\
|
||||
display: flex; align-items: center;\
|
||||
padding: 0 16px;\
|
||||
background: linear-gradient(135deg, #2563eb, #1d4ed8);\
|
||||
color: #fff;\
|
||||
box-shadow: 0 2px 12px rgba(37,99,235,0.25);\
|
||||
}\
|
||||
.uni-nav-inner {\
|
||||
display: flex; align-items: center;\
|
||||
width: 100%; max-width: 1400px; margin: 0 auto;\
|
||||
gap: 8px;\
|
||||
}\
|
||||
.uni-nav-brand {\
|
||||
font-size: 18px; font-weight: 700;\
|
||||
white-space: nowrap;\
|
||||
margin-right: 8px;\
|
||||
letter-spacing: -0.3px;\
|
||||
}\
|
||||
.uni-nav-items {\
|
||||
display: flex; align-items: center; gap: 2px;\
|
||||
flex: 1; overflow: hidden;\
|
||||
}\
|
||||
.uni-nav-item {\
|
||||
display: inline-flex; align-items: center; gap: 4px;\
|
||||
padding: 6px 12px;\
|
||||
border: none; background: transparent;\
|
||||
color: rgba(255,255,255,0.8);\
|
||||
font-size: 13px;\
|
||||
border-radius: 8px;\
|
||||
cursor: pointer;\
|
||||
white-space: nowrap;\
|
||||
transition: all 0.2s;\
|
||||
}\
|
||||
.uni-nav-item:hover { background: rgba(255,255,255,0.12); color: #fff; }\
|
||||
.uni-nav-item.active {\
|
||||
background: rgba(255,255,255,0.18); color: #fff; font-weight: 600;\
|
||||
}\
|
||||
.uni-nav-right {\
|
||||
display: flex; align-items: center; gap: 8px;\
|
||||
flex-shrink: 0;\
|
||||
}\
|
||||
.uni-nav-avatar {\
|
||||
width: 30px; height: 30px;\
|
||||
border-radius: 50%;\
|
||||
background: rgba(255,255,255,0.2);\
|
||||
display: flex; align-items: center; justify-content: center;\
|
||||
font-size: 13px; font-weight: 600;\
|
||||
}\
|
||||
.uni-nav-username { font-size: 13px; color: rgba(255,255,255,0.9); }\
|
||||
.uni-nav-badge {\
|
||||
font-size: 10px;\
|
||||
background: rgba(255,255,255,0.15);\
|
||||
padding: 1px 8px; border-radius: 10px;\
|
||||
margin-left: 4px;\
|
||||
}\
|
||||
.uni-nav-logout {\
|
||||
background: rgba(255,255,255,0.1);\
|
||||
border: 1px solid rgba(255,255,255,0.15);\
|
||||
color: rgba(255,255,255,0.9);\
|
||||
padding: 4px 12px; border-radius: 6px;\
|
||||
cursor: pointer; font-size: 12px;\
|
||||
transition: all 0.2s;\
|
||||
}\
|
||||
.uni-nav-logout:hover { background: rgba(255,255,255,0.2); }\
|
||||
.uni-nav-hamburger {\
|
||||
display: none;\
|
||||
background: rgba(255,255,255,0.1);\
|
||||
border: none; color: #fff;\
|
||||
width: 36px; height: 36px;\
|
||||
border-radius: 8px;\
|
||||
cursor: pointer; font-size: 20px;\
|
||||
align-items: center; justify-content: center;\
|
||||
transition: all 0.2s;\
|
||||
}\
|
||||
.uni-nav-hamburger:hover { background: rgba(255,255,255,0.18); }\
|
||||
.uni-nav-dropdown {\
|
||||
display: none;\
|
||||
position: fixed; top: 60px; left: 0; right: 0;\
|
||||
background: #fff;\
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.12);\
|
||||
z-index: 9999;\
|
||||
padding: 8px;\
|
||||
max-height: calc(100vh - 60px);\
|
||||
overflow-y: auto;\
|
||||
animation: uniNavSlideDown 0.2s ease-out;\
|
||||
}\
|
||||
.uni-nav-dropdown.open { display: block; }\
|
||||
.uni-nav-dropdown-item {\
|
||||
display: flex; align-items: center; gap: 8px;\
|
||||
width: 100%;\
|
||||
padding: 12px 16px;\
|
||||
border: none; background: transparent;\
|
||||
color: #303133; font-size: 14px;\
|
||||
border-radius: 8px;\
|
||||
cursor: pointer;\
|
||||
transition: all 0.15s;\
|
||||
}\
|
||||
.uni-nav-dropdown-item:hover { background: #f0f4ff; color: #2563eb; }\
|
||||
.uni-nav-dropdown-item.active { background: #eef2ff; color: #2563eb; font-weight: 600; }\
|
||||
.uni-nav-dropdown-divider {\
|
||||
height: 1px; background: #f0f0f0; margin: 4px 0;\
|
||||
}\
|
||||
@media (max-width: 768px) {\
|
||||
.uni-nav-items { display: none; }\
|
||||
.uni-nav-username { display: none; }\
|
||||
.uni-nav-hamburger { display: inline-flex; }\
|
||||
}\
|
||||
@keyframes uniNavSlideDown {\
|
||||
from { opacity: 0; transform: translateY(-8px); }\
|
||||
to { opacity: 1; transform: translateY(0); }\
|
||||
}\
|
||||
';
|
||||
document.head.appendChild(style);
|
||||
|
||||
function getCurrentPage() {
|
||||
var path = window.location.pathname;
|
||||
if (path === '/' || path === '/index.html') return 'dashboard';
|
||||
var m = path.match(/\/(\w+)\.html/);
|
||||
return m ? m[1] : 'dashboard';
|
||||
}
|
||||
|
||||
function navItems(isAdmin) {
|
||||
var items = [
|
||||
{ key: 'dashboard', label: '仪表盘', page: '/' },
|
||||
{ key: 'topics', label: '选题', page: 'topics.html' },
|
||||
{ key: 'metrics', label: '数据', page: 'metrics.html' },
|
||||
{ key: 'calendar', label: '日历', page: 'calendar.html' },
|
||||
{ key: 'assets', label: '素材', page: 'assets.html' },
|
||||
{ key: 'tasks', label: '任务', page: 'tasks.html' },
|
||||
{ key: 'platforms', label: '平台', page: 'platforms.html' },
|
||||
{ key: 'logs', label: '日志', page: 'logs.html' },
|
||||
];
|
||||
if (isAdmin) {
|
||||
items.push({ key: 'users', label: '用户', page: 'users.html', admin: true });
|
||||
items.push({ key: 'admin', label: '系统', page: 'admin.html', admin: true });
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
var UniNav = {
|
||||
name: 'UniNav',
|
||||
props: {
|
||||
title: { type: String, default: '' },
|
||||
username: { type: String, default: '' },
|
||||
isAdmin: { type: Boolean, default: false },
|
||||
currentPage: { type: String, default: null },
|
||||
onNavigate: { type: Function, default: null },
|
||||
},
|
||||
emits: ['logout'],
|
||||
data: function () {
|
||||
return { dropdownOpen: false };
|
||||
},
|
||||
computed: {
|
||||
items: function () {
|
||||
return navItems(this.isAdmin);
|
||||
},
|
||||
page: function () {
|
||||
return this.currentPage || getCurrentPage();
|
||||
},
|
||||
showTitle: function () {
|
||||
return this.title || '宇之然';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
navigate: function (page) {
|
||||
this.dropdownOpen = false;
|
||||
if (this.onNavigate) { this.onNavigate(page); return; }
|
||||
window.location.href = page === '/' ? '/' : '/' + page;
|
||||
},
|
||||
logout: function () {
|
||||
this.$emit('logout');
|
||||
},
|
||||
toggleDropdown: function () {
|
||||
this.dropdownOpen = !this.dropdownOpen;
|
||||
},
|
||||
closeDropdown: function (e) {
|
||||
if (this.dropdownOpen && !this.$el.contains(e.target)) {
|
||||
this.dropdownOpen = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted: function () {
|
||||
document.addEventListener('click', this.closeDropdown);
|
||||
},
|
||||
beforeUnmount: function () {
|
||||
document.removeEventListener('click', this.closeDropdown);
|
||||
},
|
||||
template: '\
|
||||
<nav class="uni-nav">\
|
||||
<div class="uni-nav-inner">\
|
||||
<div class="uni-nav-brand">{{ showTitle }}</div>\
|
||||
<div class="uni-nav-items">\
|
||||
<button v-for="item in items" :key="item.key"\
|
||||
:class="[\'uni-nav-item\', { active: page === item.key }]"\
|
||||
@click="navigate(item.page)">{{ item.label }}</button>\
|
||||
</div>\
|
||||
<div class="uni-nav-right">\
|
||||
<button class="uni-nav-hamburger" @click.stop="toggleDropdown">\
|
||||
{{ dropdownOpen ? "✕" : "☰" }}\
|
||||
</button>\
|
||||
<div class="uni-nav-avatar">{{ username ? username.charAt(0).toUpperCase() : "?" }}</div>\
|
||||
<span v-if="username" class="uni-nav-username">{{ username }}</span>\
|
||||
<span v-if="isAdmin" class="uni-nav-badge">管理员</span>\
|
||||
<button class="uni-nav-logout" @click="logout">退出</button>\
|
||||
</div>\
|
||||
</div>\
|
||||
<div :class="[\'uni-nav-dropdown\', { open: dropdownOpen }]" @click.stop>\
|
||||
<button v-for="item in items" :key="item.key"\
|
||||
:class="[\'uni-nav-dropdown-item\', { active: page === item.key }]"\
|
||||
@click="navigate(item.page)">\
|
||||
{{ item.label }}\
|
||||
</button>\
|
||||
</div>\
|
||||
</nav>',
|
||||
};
|
||||
|
||||
window.UniNav = UniNav;
|
||||
|
||||
window.installUniNav = function (app) {
|
||||
app.component('uni-nav', UniNav);
|
||||
return app;
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user