feat: 创作工作台统一 + AI味检测/白标 + 移动端补全 + 合规发布闭环

- 新增创作工作台 studio.html:合并选题/内容工厂/文章管理为单一 tab 入口(iframe embed 模式)
- 新增 AI味检测模块(ai_slop API + 页面,合规软硬问题分级)
- 新增白标品牌配置(branding API + 页面 + deploy 私有化交付包)
- 发布闭环:publishing 放宽至 editor + records/mark-published 接口
- 移动端响应式补全(admin/calendar/ai-slop 表格卡片兜底)
- 修复菜单幂等播种缺陷(按 path 对齐,避免功能页孤立)
- 新增短视频脚本 shortvideo.py 与 2026 市场调研简报
This commit is contained in:
yuzhiran
2026-07-11 12:36:01 +08:00
parent 3d596275d5
commit 7e953afbd2
39 changed files with 2418 additions and 311 deletions
+50 -17
View File
@@ -107,7 +107,7 @@
.uni-nav-dropdown-divider {\
height: 1px; background: #f0f0f0; margin: 4px 0;\
}\
@media (max-width: 768px) {\
@media (max-width: 1000px) {\
.uni-nav-items { display: none; }\
.uni-nav-username { display: none; }\
.uni-nav-hamburger { display: inline-flex; }\
@@ -121,20 +121,24 @@
function getCurrentPage() {
var path = window.location.pathname;
if (path === '/index.html') return 'workspace';
var m = path.match(/\/(\w+)\.html/);
return m ? m[1] : 'workspace';
if (path === '/' || path === '/index.html') return 'index.html';
var m = path.match(/\/([\w-]+)\.html$/);
return m ? m[1] + '.html' : 'index.html';
}
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: 'dashboard', label: '仪表盘', page: 'index.html' },
{ key: 'studio', label: '创作工作台', page: 'studio.html' },
{ key: 'insights', label: '数据', page: 'metrics.html' },
{ key: 'calendar', label: '日历', page: 'calendar.html' },
{ key: 'assets', label: '素材', page: 'assets.html' },
{ key: 'tasks', label: '任务', page: 'tasks.html' },
{ key: 'campaigns', label: '外推营销', page: 'campaigns.html' },
{ key: 'ai-slop', label: 'AI味检测', page: 'ai-slop.html' },
];
if (isAdmin) {
items.push({ key: 'branding', label: '白标设置', page: 'branding.html', admin: true });
items.push({ key: 'admin', label: '系统管理', page: 'admin.html', admin: true });
}
return items;
@@ -160,6 +164,9 @@
pwdForm: { old_password: '', new_password: '', confirm: '' },
pwdSubmitting: false,
serverMenus: null,
brandName: '',
brandLogo: '',
primaryColor: '',
};
},
computed: {
@@ -167,15 +174,19 @@
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 }; });
.map(function (m) { return { key: m.name, label: m.name, page: (m.path || '').replace(/^\//, '') }; });
}
return navItems(this.isAdmin);
},
page: function () {
return this.currentPage || getCurrentPage();
return getCurrentPage();
},
showTitle: function () {
return this.title || '宇之然';
return this.brandName || this.title || '宇之然';
},
navStyle: function () {
if (!this.primaryColor) return {};
return { background: 'linear-gradient(135deg, ' + this.primaryColor + ', ' + this.primaryColor + 'cc)' };
},
},
methods: {
@@ -189,6 +200,24 @@
}.bind(this))
.catch(function () {});
},
fetchBranding: function () {
fetch('/api/branding')
.then(function (r) { return r.ok ? r.json() : Promise.reject(); })
.then(function (d) {
if (d.brand_name) this.brandName = d.brand_name;
if (d.brand_logo) this.brandLogo = d.brand_logo;
if (d.brand_primary_color) {
this.primaryColor = d.brand_primary_color;
document.documentElement.style.setProperty('--color-primary', d.brand_primary_color);
var s = document.getElementById('uni-nav-brand-style');
if (!s) { s = document.createElement('style'); s.id = 'uni-nav-brand-style'; document.head.appendChild(s); }
s.textContent = '.uni-nav-dropdown-item:hover,.uni-nav-dropdown-item.active{color:' + d.brand_primary_color + ';}' +
'.uni-nav-dropdown-item.active{background:' + d.brand_primary_color + '14;}' +
'.uni-nav-item.active{background:rgba(255,255,255,0.18);}';
}
}.bind(this))
.catch(function () {});
},
navigate: function (page) {
this.dropdownOpen = false; this.profileOpen = false;
if (this.onNavigate) { this.onNavigate(page); return; }
@@ -246,18 +275,22 @@
mounted: function () {
document.addEventListener('click', this.closeAll);
this.fetchMenus();
this.fetchBranding();
},
beforeUnmount: function () {
document.removeEventListener('click', this.closeAll);
},
template: '\
<nav class="uni-nav" style="position:relative;">\
<nav class="uni-nav" :style="navStyle">\
<div class="uni-nav-inner">\
<div class="uni-nav-brand">{{ showTitle }}</div>\
<div class="uni-nav-brand">\
<img v-if="brandLogo" :src="brandLogo" style="height:24px;width:auto;margin-right:6px;vertical-align:middle;border-radius:4px;" />\
<span>{{ showTitle }}</span>\
</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>\
<button v-for="item in items" :key="item.key"\
:class="['uni-nav-item', { active: page === item.page }]"\
@click="navigate(item.page)">{{ item.label }}</button>\
</div>\
<div class="uni-nav-right">\
<button class="uni-nav-hamburger" @click.stop="toggleDropdown">\
@@ -281,7 +314,7 @@
</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 }]"\
:class="[\'uni-nav-dropdown-item\', { active: page === item.page }]"\
@click="navigate(item.page)">\
{{ item.label }}\
</button>\