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:
@@ -33,7 +33,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="app" v-cloak>
|
||||
<uni-nav title="选题管理" :username="currentUser.username" :is-admin="isAdmin" current-page="topics" @navigate="redirectToPage" @logout="handleLogout">
|
||||
<uni-nav v-if="!isEmbed" title="选题管理" :username="currentUser.username" :is-admin="isAdmin" current-page="topics" @navigate="redirectToPage" @logout="handleLogout">
|
||||
</uni-nav>
|
||||
<div class="main-content">
|
||||
<main class="content-area">
|
||||
@@ -56,6 +56,7 @@
|
||||
<el-button size="default" :type="filterStatus === 'review' ? 'primary' : ''" @click="filterStatus = 'review'; fetchTopics()">待审查 ({{ statusStats.review }})</el-button>
|
||||
<el-button size="default" :type="filterStatus === 'ready' ? 'primary' : ''" @click="filterStatus = 'ready'; fetchTopics()">待发布 ({{ statusStats.ready }})</el-button>
|
||||
<el-button size="default" :type="filterStatus === 'published' ? 'primary' : ''" @click="filterStatus = 'published'; fetchTopics()">已发布 ({{ statusStats.published }})</el-button>
|
||||
<el-button size="default" :type="filterStatus === 'pending_publish' ? 'primary' : ''" @click="filterStatus = 'pending_publish'; fetchTopics()">待人工发布 ({{ statusStats.pendingPublish }})</el-button>
|
||||
</div>
|
||||
<div style="margin-bottom:10px;">
|
||||
<span class="search-toggle" @click="showSearch = !showSearch">🔍 {{ showSearch ? '收起搜索' : '展开搜索' }}</span>
|
||||
@@ -64,30 +65,30 @@
|
||||
<div v-if="showSearch" class="search-area">
|
||||
<el-form :model="searchForm" size="small" label-width="70px" @submit.prevent>
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="6"><el-form-item label="选题ID"><el-input v-model="searchForm.id" placeholder="如 A07" clearable @input="doSearch"></el-input></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="标题"><el-input v-model="searchForm.title" placeholder="关键词" clearable @input="doSearch"></el-input></el-form-item></el-col>
|
||||
<el-col :span="5"><el-form-item label="领域"><el-input v-model="searchForm.field" placeholder="如 科技前沿" clearable @input="doSearch"></el-input></el-form-item></el-col>
|
||||
<el-col :span="5"><el-form-item label="栏目"><el-select v-model="searchForm.series" clearable placeholder="全部" style="width:100%;" @change="doSearch"><el-option v-for="s in seriesOptions" :key="s" :label="s" :value="s"></el-option></el-select></el-form-item></el-col>
|
||||
<el-col :xs="24" :sm="12" :md="6"><el-form-item label="选题ID"><el-input v-model="searchForm.id" placeholder="如 A07" clearable @input="doSearch"></el-input></el-form-item></el-col>
|
||||
<el-col :xs="24" :sm="12" :md="8"><el-form-item label="标题"><el-input v-model="searchForm.title" placeholder="关键词" clearable @input="doSearch"></el-input></el-form-item></el-col>
|
||||
<el-col :xs="24" :sm="12" :md="5"><el-form-item label="领域"><el-input v-model="searchForm.field" placeholder="如 科技前沿" clearable @input="doSearch"></el-input></el-form-item></el-col>
|
||||
<el-col :xs="24" :sm="12" :md="5"><el-form-item label="栏目"><el-select v-model="searchForm.series" clearable placeholder="全部" style="width:100%;" @change="doSearch"><el-option v-for="s in seriesOptions" :key="s" :label="s" :value="s"></el-option></el-select></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="12"><el-form-item label="创建时间">
|
||||
<el-col :xs="24" :sm="24" :md="12"><el-form-item label="创建时间">
|
||||
<el-date-picker v-model="searchForm.createdStart" type="date" placeholder="开始" style="width:130px;" value-format="YYYY-MM-DD" @change="doSearch"></el-date-picker>
|
||||
<span style="margin:0 4px;color:#909399;">~</span>
|
||||
<el-date-picker v-model="searchForm.createdEnd" type="date" placeholder="结束" style="width:130px;" value-format="YYYY-MM-DD" @change="doSearch"></el-date-picker>
|
||||
</el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="创作时间">
|
||||
<el-col :xs="24" :sm="24" :md="12"><el-form-item label="创作时间">
|
||||
<el-date-picker v-model="searchForm.generatedStart" type="date" placeholder="开始" style="width:130px;" value-format="YYYY-MM-DD" @change="doSearch"></el-date-picker>
|
||||
<span style="margin:0 4px;color:#909399;">~</span>
|
||||
<el-date-picker v-model="searchForm.generatedEnd" type="date" placeholder="结束" style="width:130px;" value-format="YYYY-MM-DD" @change="doSearch"></el-date-picker>
|
||||
</el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="12"><el-form-item label="审查时间">
|
||||
<el-col :xs="24" :sm="24" :md="12"><el-form-item label="审查时间">
|
||||
<el-date-picker v-model="searchForm.reviewedStart" type="date" placeholder="开始" style="width:130px;" value-format="YYYY-MM-DD" @change="doSearch"></el-date-picker>
|
||||
<span style="margin:0 4px;color:#909399;">~</span>
|
||||
<el-date-picker v-model="searchForm.reviewedEnd" type="date" placeholder="结束" style="width:130px;" value-format="YYYY-MM-DD" @change="doSearch"></el-date-picker>
|
||||
</el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="发布时间">
|
||||
<el-col :xs="24" :sm="24" :md="12"><el-form-item label="发布时间">
|
||||
<el-date-picker v-model="searchForm.publishedStart" type="date" placeholder="开始" style="width:130px;" value-format="YYYY-MM-DD" @change="doSearch"></el-date-picker>
|
||||
<span style="margin:0 4px;color:#909399;">~</span>
|
||||
<el-date-picker v-model="searchForm.publishedEnd" type="date" placeholder="结束" style="width:130px;" value-format="YYYY-MM-DD" @change="doSearch"></el-date-picker>
|
||||
@@ -134,6 +135,8 @@
|
||||
<el-button size="small" type="success" :disabled="isStatus(scope.row, 'published')" @click="createTopic(scope.row)" style="padding:5px 8px;">创作</el-button>
|
||||
<el-button size="small" type="warning" :disabled="!isStatus(scope.row, 'review')" @click="reviewTopic(scope.row)" style="padding:5px 8px;">审查</el-button>
|
||||
<el-button v-if="isStatus(scope.row, 'ready')" size="small" type="primary" @click="openPublishDialog(scope.row)" style="padding:5px 8px;">发布</el-button>
|
||||
<el-button v-if="isStatus(scope.row, 'pending_publish')" size="small" type="success" @click="markPublished(scope.row)" style="padding:5px 8px;">标记发布</el-button>
|
||||
<el-button size="small" :disabled="(scope.row.article_count || 0) === 0" @click="openSlop(scope.row)" style="padding:5px 8px;">检测</el-button>
|
||||
<el-button size="small" type="danger" @click="deleteTopic(scope.row.id)" style="padding:5px 8px;">删除</el-button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -172,6 +175,8 @@
|
||||
<el-button size="small" type="success" :disabled="isStatus(topic, 'published')" @click="createTopic(topic)">创作</el-button>
|
||||
<el-button size="small" type="warning" :disabled="!isStatus(topic, 'review')" @click="reviewTopic(topic)">审查</el-button>
|
||||
<el-button v-show="isStatus(topic, 'ready')" size="small" type="primary" @click="openPublishDialog(topic)">发布</el-button>
|
||||
<el-button v-if="isStatus(topic, 'pending_publish')" size="small" type="success" @click="markPublished(topic)">标记发布</el-button>
|
||||
<el-button size="small" :disabled="(topic.article_count || 0) === 0" @click="openSlop(topic)">检测</el-button>
|
||||
<el-button size="small" type="danger" @click="deleteTopic(topic.id)">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -256,6 +261,10 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
<el-dialog v-model="publishDialogVisible" title="发布确认" width="420px" :close-on-click-modal="false">
|
||||
<el-alert type="warning" :closable="false" show-icon style="margin-bottom:16px;">
|
||||
<template #title>AI 内容需真人校验后发布</template>
|
||||
系统仅生成各平台发布稿并置为「待人工发布」,<b>不代发</b>。请在对应平台粘贴、核对 AI 声明与合规后手动发布,再回来点「标记已发布」。
|
||||
</el-alert>
|
||||
<div v-if="publishTopic">
|
||||
<div style="margin-bottom:16px;">
|
||||
<div style="font-size:14px;color:#606266;margin-bottom:8px;">选题:</div>
|
||||
@@ -336,6 +345,7 @@
|
||||
const TopicsApp = {
|
||||
data() {
|
||||
return {
|
||||
isEmbed: false,
|
||||
isLoggedIn: false, isAdmin: false, currentUser: { username: '' },
|
||||
loadingTable: false, selectedTopicIds: [], filterStatus: '',
|
||||
stats: { total: 0, pending: 0, review: 0, ready: 0, published: 0, today: 0 },
|
||||
@@ -371,7 +381,7 @@ const TopicsApp = {
|
||||
if (this.filterStatus === 'today') list = this.todayTopics;
|
||||
else if (!this.filterStatus) list = this.topics;
|
||||
else {
|
||||
const map = { 'pending': ['pending','待处理'], 'review': ['review','待审查'], 'ready': ['ready','待发布'], 'published': ['published','已发布'] };
|
||||
const map = { 'pending': ['pending','待处理'], 'review': ['review','待审查'], 'ready': ['ready','待发布'], 'published': ['published','已发布'], 'pending_publish': ['pending_publish','待人工发布'] };
|
||||
const allowed = map[this.filterStatus] || [this.filterStatus];
|
||||
list = this.topics.filter(t => allowed.includes(t.status));
|
||||
}
|
||||
@@ -422,8 +432,8 @@ const TopicsApp = {
|
||||
return this.filteredTopics.slice(start, start + this.pageSize);
|
||||
},
|
||||
statusStats() {
|
||||
const s = { total: this.topics.length, pending: 0, review: 0, ready: 0, published: 0 };
|
||||
const aliases = { 'pending': ['pending','待处理'], 'review': ['review','待审查'], 'ready': ['ready','待发布'], 'published': ['published','已发布'] };
|
||||
const s = { total: this.topics.length, pending: 0, review: 0, ready: 0, published: 0, pendingPublish: 0 };
|
||||
const aliases = { 'pending': ['pending','待处理'], 'review': ['review','待审查'], 'ready': ['ready','待发布'], 'published': ['published','已发布'], 'pending_publish': ['pending_publish','待人工发布'] };
|
||||
this.topics.forEach(t => { for (const [k, v] of Object.entries(aliases)) { if (v.includes(t.status)) { s[k]++; break; } } });
|
||||
return s;
|
||||
},
|
||||
@@ -658,13 +668,28 @@ const TopicsApp = {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ topic_id: this.publishTopic.id, platforms: selected })
|
||||
});
|
||||
this.$message.success(`发布完成: ${this.publishTopic.title}`);
|
||||
this.$message.success(data.message || `已生成发布稿,请在各平台校验后手动发布`);
|
||||
this.publishDialogVisible = false;
|
||||
await this.fetchTopics();
|
||||
await this.fetchTodayCount();
|
||||
} catch (error) { this.$message.error(`发布失败: ${error.message}`); }
|
||||
finally { this.publishing = false; }
|
||||
},
|
||||
openSlop(topic) {
|
||||
window.open('ai-slop.html?topic_id=' + encodeURIComponent(topic.id), '_blank');
|
||||
},
|
||||
async markPublished(topic) {
|
||||
try {
|
||||
await this.$confirm('确认已在各平台完成发布?此操作将把选题标记为「已发布」并关闭发布记录。', '标记发布', { type: 'warning' });
|
||||
await this.api('/api/publishing/' + encodeURIComponent(topic.id) + '/mark-published', { method: 'POST' });
|
||||
this.$message.success('已标记为已发布');
|
||||
await this.fetchTopics();
|
||||
} catch (e) {
|
||||
if (e === 'cancel') return;
|
||||
const msg = e?.response?.data?.detail || e?.message || '未知错误';
|
||||
this.$message.error('操作失败: ' + msg);
|
||||
}
|
||||
},
|
||||
async deleteTopic(id) {
|
||||
try {
|
||||
await this.$confirm('确定删除选题 ' + id + '?将同时删除相关文章、发布记录等关联数据。', '提示', { type: 'warning' });
|
||||
@@ -806,8 +831,8 @@ const TopicsApp = {
|
||||
this.currentPage = 1;
|
||||
},
|
||||
handleLogout() { localStorage.removeItem('authToken'); localStorage.removeItem('userRole'); localStorage.removeItem('currentUser'); window.location.href = '/login.html'; },
|
||||
redirectToPage(page) { window.location.href = page.startsWith('/') ? page : '/' + page; },
|
||||
getStatusLabel(status) { return { 'pending': '待处理', 'review': '待审查', 'ready': '待发布', 'published': '已发布' }[status] || status; },
|
||||
redirectToPage(page) { const url = page.startsWith('/') ? page : '/' + page; if (this.isEmbed) window.top.location.href = url; else window.location.href = url; },
|
||||
getStatusLabel(status) { return { 'pending': '待处理', 'review': '待审查', 'ready': '待发布', 'published': '已发布', 'pending_publish': '待人工发布' }[status] || status; },
|
||||
toggleCheck(id) {
|
||||
const idx = this.selectedTopicIds.indexOf(id);
|
||||
if (idx >= 0) {
|
||||
@@ -835,12 +860,13 @@ const TopicsApp = {
|
||||
try { return new Date(dateStr.replace(' ', 'T')).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); }
|
||||
catch (e) { return dateStr; }
|
||||
},
|
||||
getStatusType(status) { return { 'pending': 'warning', 'review': 'danger', 'ready': 'success', 'published': 'info' }[status] || 'primary'; },
|
||||
isStatus(row, status) { const map = { 'pending': ['pending','待处理'], 'review': ['review','待审查'], 'ready': ['ready','待发布'], 'published': ['published','已发布'] }; return map[status] ? map[status].includes(row.status) : row.status === status; }
|
||||
getStatusType(status) { return { 'pending': 'warning', 'review': 'danger', 'ready': 'success', 'published': 'info', 'pending_publish': 'warning' }[status] || 'primary'; },
|
||||
isStatus(row, status) { const map = { 'pending': ['pending','待处理'], 'review': ['review','待审查'], 'ready': ['ready','待发布'], 'published': ['published','已发布'], 'pending_publish': ['pending_publish','待人工发布'] }; return map[status] ? map[status].includes(row.status) : row.status === status; }
|
||||
},
|
||||
mounted() {
|
||||
const token = localStorage.getItem('authToken');
|
||||
if (!token) { window.location.href = '/login.html'; return; }
|
||||
if (new URLSearchParams(window.location.search).get('embed') === '1') { this.isEmbed = true; document.body.classList.add('embed'); }
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const urlFilter = params.get('filter');
|
||||
if (urlFilter) this.filterStatus = urlFilter;
|
||||
|
||||
Reference in New Issue
Block a user