8a04a23e86
Add campaigns.html with 6 tabs (Overview/Products/Campaigns/Rankings/Audits/Tasks) using Vue 3 + Element Plus CDN. Add nav entry to uni-nav.js. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
332 lines
15 KiB
JavaScript
332 lines
15 KiB
JavaScript
(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 === '/index.html') return 'workspace';
|
|
var m = path.match(/\/(\w+)\.html/);
|
|
return m ? m[1] : 'workspace';
|
|
}
|
|
|
|
function navItems(isAdmin) {
|
|
var items = [
|
|
{ key: 'workspace', label: '工作台', page: 'index.html' },
|
|
{ key: 'factory', label: '内容工厂', page: 'factory.html' },
|
|
{ key: 'insights', label: '数据洞察', page: 'insights.html' },
|
|
{ key: 'assets', label: '资产库', page: 'assets.html' },
|
|
{ key: 'campaigns', label: '外推营销', page: 'campaigns.html' },
|
|
];
|
|
if (isAdmin) {
|
|
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,
|
|
profileOpen: false,
|
|
showProfileDialog: false,
|
|
showPwdDialog: false,
|
|
profile: null,
|
|
pwdForm: { old_password: '', new_password: '', confirm: '' },
|
|
pwdSubmitting: false,
|
|
serverMenus: null,
|
|
};
|
|
},
|
|
computed: {
|
|
items: function () {
|
|
if (this.serverMenus) {
|
|
return this.serverMenus
|
|
.filter(function (m) { return m.is_active !== false; })
|
|
.map(function (m) { return { key: m.name, label: m.name, page: m.path }; });
|
|
}
|
|
return navItems(this.isAdmin);
|
|
},
|
|
page: function () {
|
|
return this.currentPage || getCurrentPage();
|
|
},
|
|
showTitle: function () {
|
|
return this.title || '宇之然';
|
|
},
|
|
},
|
|
methods: {
|
|
fetchMenus: function () {
|
|
var token = localStorage.getItem('authToken');
|
|
if (!token) return;
|
|
fetch('/api/menus/active', { headers: { 'Authorization': 'Bearer ' + token } })
|
|
.then(function (r) { return r.ok ? r.json() : Promise.reject(); })
|
|
.then(function (data) {
|
|
if (data && data.length > 0) this.serverMenus = data;
|
|
}.bind(this))
|
|
.catch(function () {});
|
|
},
|
|
navigate: function (page) {
|
|
this.dropdownOpen = false; this.profileOpen = false;
|
|
if (this.onNavigate) { this.onNavigate(page); return; }
|
|
window.location.href = page.startsWith('/') ? page : '/' + page;
|
|
},
|
|
logout: function () {
|
|
this.profileOpen = false;
|
|
this.$emit('logout');
|
|
},
|
|
toggleDropdown: function () {
|
|
this.dropdownOpen = !this.dropdownOpen; this.profileOpen = false;
|
|
},
|
|
toggleProfile: function () {
|
|
this.profileOpen = !this.profileOpen; this.dropdownOpen = false;
|
|
},
|
|
closeAll: function (e) {
|
|
var el = this.$el;
|
|
if (!el) return;
|
|
if (this.dropdownOpen && !el.contains(e.target)) this.dropdownOpen = false;
|
|
if (this.profileOpen && !el.querySelector('.uni-nav-profile-menu')?.contains(e.target) && e.target !== el.querySelector('.uni-nav-avatar') && !el.querySelector('.uni-nav-avatar')?.contains(e.target)) this.profileOpen = false;
|
|
},
|
|
openProfile: function () {
|
|
this.profileOpen = false;
|
|
var token = localStorage.getItem('authToken');
|
|
if (!token) return;
|
|
fetch('/api/auth/me', { headers: { 'Authorization': 'Bearer ' + token } })
|
|
.then(function (r) { return r.ok ? r.json() : Promise.reject(); })
|
|
.then(function (d) { this.profile = d.user || d; this.showProfileDialog = true; }.bind(this))
|
|
.catch(function () { (ElementPlus.ElMessage || {}).error && ElementPlus.ElMessage.error('获取用户信息失败'); });
|
|
},
|
|
openPwdDialog: function () {
|
|
this.profileOpen = false;
|
|
this.pwdForm = { old_password: '', new_password: '', confirm: '' };
|
|
this.showPwdDialog = true;
|
|
},
|
|
submitPwd: function () {
|
|
if (this.pwdForm.new_password.length < 6) { (ElementPlus.ElMessage || {}).warning && ElementPlus.ElMessage.warning('新密码至少6位'); return; }
|
|
if (this.pwdForm.new_password !== this.pwdForm.confirm) { (ElementPlus.ElMessage || {}).warning && ElementPlus.ElMessage.warning('两次密码不一致'); return; }
|
|
this.pwdSubmitting = true;
|
|
var token = localStorage.getItem('authToken');
|
|
fetch('/api/auth/password', {
|
|
method: 'PUT',
|
|
headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ old_password: this.pwdForm.old_password, new_password: this.pwdForm.new_password })
|
|
}).then(function (r) {
|
|
if (r.ok) { this.showPwdDialog = false; (ElementPlus.ElMessage || {}).success && ElementPlus.ElMessage.success('密码修改成功'); }
|
|
else { return r.json().then(function (d) { throw new Error(d.detail || '修改失败'); }); }
|
|
}.bind(this)).catch(function (e) {
|
|
(ElementPlus.ElMessage || {}).error && ElementPlus.ElMessage.error(e.message);
|
|
}).finally(function () { this.pwdSubmitting = false; }.bind(this));
|
|
},
|
|
closePwdDialog: function () { this.showPwdDialog = false; },
|
|
closeProfileDialog: function () { this.showProfileDialog = false; },
|
|
},
|
|
mounted: function () {
|
|
document.addEventListener('click', this.closeAll);
|
|
this.fetchMenus();
|
|
},
|
|
beforeUnmount: function () {
|
|
document.removeEventListener('click', this.closeAll);
|
|
},
|
|
template: '\
|
|
<nav class="uni-nav" style="position:relative;">\
|
|
<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" style="cursor:pointer;" @click.stop="toggleProfile">{{ username ? username.charAt(0).toUpperCase() : "?" }}</div>\
|
|
<span v-if="username" class="uni-nav-username" style="cursor:pointer;" @click.stop="toggleProfile">{{ username }}</span>\
|
|
<span v-if="isAdmin" class="uni-nav-badge">管理员</span>\
|
|
<button class="uni-nav-logout" @click="logout">退出</button>\
|
|
</div>\
|
|
</div>\
|
|
<div v-if="profileOpen" class="uni-nav-profile-menu" @click.stop\
|
|
style="position:absolute;top:60px;right:16px;background:#fff;box-shadow:0 4px 16px rgba(0,0,0,0.12);border-radius:8px;z-index:10001;min-width:160px;padding:4px;">\
|
|
<button style="display:flex;align-items:center;gap:8px;width:100%;padding:10px 14px;border:none;background:transparent;color:#303133;font-size:13px;border-radius:6px;cursor:pointer;"\
|
|
@click="openProfile">📋 个人信息</button>\
|
|
<button style="display:flex;align-items:center;gap:8px;width:100%;padding:10px 14px;border:none;background:transparent;color:#303133;font-size:13px;border-radius:6px;cursor:pointer;"\
|
|
@click="openPwdDialog">🔑 修改密码</button>\
|
|
<div style="height:1px;background:#f0f0f0;margin:4px 0;"></div>\
|
|
<button style="display:flex;align-items:center;gap:8px;width:100%;padding:10px 14px;border:none;background:transparent;color:#e74c3c;font-size:13px;border-radius:6px;cursor:pointer;"\
|
|
@click="logout">🚪 退出登录</button>\
|
|
</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>\
|
|
<div v-if="showProfileDialog" style="position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.4);z-index:99999;display:flex;align-items:center;justify-content:center;" @click.self="closeProfileDialog">\
|
|
<div style="background:#fff;border-radius:12px;width:380px;max-width:90vw;padding:24px;box-shadow:0 8px 32px rgba(0,0,0,0.2);">\
|
|
<h3 style="margin:0 0 16px;font-size:16px;font-weight:600;color:#303133;">个人信息</h3>\
|
|
<div style="display:flex;flex-direction:column;gap:10px;">\
|
|
<div style="display:flex;gap:8px;font-size:13px;"><span style="color:#909399;min-width:70px;">用户名</span><span style="color:#303133;">{{ profile ? profile.username : \'-\' }}</span></div>\
|
|
<div style="display:flex;gap:8px;font-size:13px;"><span style="color:#909399;min-width:70px;">角色</span><span style="color:#303133;">{{ profile ? profile.role : \'-\' }}</span></div>\
|
|
<div style="display:flex;gap:8px;font-size:13px;"><span style="color:#909399;min-width:70px;">组织</span><span style="color:#303133;">{{ profile ? (profile.org_id || \'-\') : \'-\' }}</span></div>\
|
|
<div style="display:flex;gap:8px;font-size:13px;"><span style="color:#909399;min-width:70px;">创建时间</span><span style="color:#303133;">{{ profile && profile.created_at ? new Date(profile.created_at).toLocaleString(\'zh-CN\') : \'-\' }}</span></div>\
|
|
<div style="display:flex;gap:8px;font-size:13px;"><span style="color:#909399;min-width:70px;">最后登录</span><span style="color:#303133;">{{ profile && profile.last_login ? new Date(profile.last_login).toLocaleString(\'zh-CN\') : \'-\' }}</span></div>\
|
|
</div>\
|
|
<div style="margin-top:16px;text-align:right;"><button style="padding:6px 16px;border:1px solid #dcdfe6;background:#fff;color:#606266;border-radius:6px;cursor:pointer;font-size:13px;" @click="closeProfileDialog">关闭</button></div>\
|
|
</div>\
|
|
</div>\
|
|
<div v-if="showPwdDialog" style="position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.4);z-index:99999;display:flex;align-items:center;justify-content:center;" @click.self="closePwdDialog">\
|
|
<div style="background:#fff;border-radius:12px;width:380px;max-width:90vw;padding:24px;box-shadow:0 8px 32px rgba(0,0,0,0.2);">\
|
|
<h3 style="margin:0 0 16px;font-size:16px;font-weight:600;color:#303133;">修改密码</h3>\
|
|
<div style="display:flex;flex-direction:column;gap:12px;">\
|
|
<div><label style="display:block;font-size:13px;color:#606266;margin-bottom:4px;">原密码</label>\
|
|
<input v-model="pwdForm.old_password" type="password" placeholder="输入原密码"\
|
|
style="width:100%;padding:8px 12px;border:1px solid #dcdfe6;border-radius:6px;font-size:13px;box-sizing:border-box;"></div>\
|
|
<div><label style="display:block;font-size:13px;color:#606266;margin-bottom:4px;">新密码</label>\
|
|
<input v-model="pwdForm.new_password" type="password" placeholder="至少6位"\
|
|
style="width:100%;padding:8px 12px;border:1px solid #dcdfe6;border-radius:6px;font-size:13px;box-sizing:border-box;"></div>\
|
|
<div><label style="display:block;font-size:13px;color:#606266;margin-bottom:4px;">确认新密码</label>\
|
|
<input v-model="pwdForm.confirm" type="password" placeholder="再次输入新密码"\
|
|
style="width:100%;padding:8px 12px;border:1px solid #dcdfe6;border-radius:6px;font-size:13px;box-sizing:border-box;"></div>\
|
|
</div>\
|
|
<div style="margin-top:16px;display:flex;gap:8px;justify-content:flex-end;">\
|
|
<button style="padding:6px 16px;border:1px solid #dcdfe6;background:#fff;color:#606266;border-radius:6px;cursor:pointer;font-size:13px;" @click="closePwdDialog">取消</button>\
|
|
<button style="padding:6px 16px;border:none;background:#409eff;color:#fff;border-radius:6px;cursor:pointer;font-size:13px;" @click="submitPwd" :disabled="pwdSubmitting">{{ pwdSubmitting ? "提交中..." : "确定" }}</button>\
|
|
</div>\
|
|
</div>\
|
|
</div>',
|
|
};
|
|
|
|
window.UniNav = UniNav;
|
|
|
|
window.installUniNav = function (app) {
|
|
app.component('uni-nav', UniNav);
|
|
return app;
|
|
};
|
|
})();
|