7e953afbd2
- 新增创作工作台 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 市场调研简报
151 lines
9.8 KiB
HTML
151 lines
9.8 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>宇之然内容创作平台 - AI味检测</title>
|
|
<link rel="stylesheet" href="element-plus.css">
|
|
<link rel="stylesheet" href="theme-modern.css">
|
|
<script src="uni-nav.js"></script>
|
|
</head>
|
|
<body>
|
|
<div id="app" v-cloak>
|
|
<uni-nav title="AI味检测" :username="currentUser.username" :is-admin="isAdmin" current-page="ai-slop" @navigate="redirectToPage" @logout="handleLogout">
|
|
</uni-nav>
|
|
<div class="main-content">
|
|
<main class="content-area">
|
|
<div class="card page-fade">
|
|
<div class="page-header">
|
|
<h2 class="page-title"><el-icon style="vertical-align:-2px;"><IconSearch /></el-icon> AI味检测 + 净化</h2>
|
|
</div>
|
|
<div class="filter-bar">
|
|
<el-input v-model="topicId" placeholder="输入选题 ID,如 B02" clearable size="default" style="width:280px;" @keyup.enter="runReport">
|
|
<template #prepend>选题</template>
|
|
</el-input>
|
|
<el-button type="primary" :loading="loadingReport" @click="runReport">检测</el-button>
|
|
</div>
|
|
<el-alert v-if="errorMsg" :title="errorMsg" type="error" show-icon :closable="false" style="margin-bottom:12px;"></el-alert>
|
|
<div v-if="!loadingReport && report && report.platforms.length === 0" style="text-align:center;padding:40px 0;color:#909399;font-size:14px;">
|
|
该选题暂无文章,请先创作内容。
|
|
</div>
|
|
<div v-for="p in (report ? report.platforms : [])" :key="p.platform" style="margin-bottom:20px;">
|
|
<el-card shadow="hover">
|
|
<template #header>
|
|
<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;">
|
|
<strong style="font-size:16px;">{{ platformLabel(p.platform) }}</strong>
|
|
<el-tag :type="p.passed ? 'success' : 'danger'" size="small">{{ p.passed ? '通过' : '未通过' }}</el-tag>
|
|
<span style="font-size:13px;color:#909399;">合规分:<b :style="{color: p.score >= 80 ? '#67c23a' : (p.score >= 60 ? '#e6a23c' : '#f56c6c')}">{{ p.score }}</b></span>
|
|
<el-button size="small" type="warning" :loading="p.purifying" @click="purify(p)" style="margin-left:auto;">一键净化</el-button>
|
|
</div>
|
|
</template>
|
|
<div v-if="p.message" style="margin-bottom:10px;color:#67c23a;font-size:13px;">{{ p.message }}</div>
|
|
<div v-if="p.score_before !== null" style="margin-bottom:10px;font-size:13px;color:#606266;">
|
|
净化前分:<b>{{ p.score_before }}</b> → 净化后分:<b :style="{color:'#67c23a'}">{{ p.score_after }}</b>
|
|
<span style="margin-left:12px;">问题数:{{ p.issues_before.length }} → {{ p.issues_after.length }}</span>
|
|
</div>
|
|
<div v-if="p.issues.length === 0" style="color:#67c23a;font-size:14px;">✅ 未检测到明显问题</div>
|
|
<template v-else>
|
|
<el-table class="card-table" :data="p.issues" size="small" stripe>
|
|
<el-table-column prop="type" label="类型" width="110">
|
|
<template #default="s"><el-tag :type="s.row.severity === 'high' ? 'danger' : 'warning'" size="small">{{ s.row.type }}</el-tag></template>
|
|
</el-table-column>
|
|
<el-table-column prop="category" label="类别" width="130" show-overflow-tooltip></el-table-column>
|
|
<el-table-column prop="detail" label="详情" min-width="200" show-overflow-tooltip></el-table-column>
|
|
<el-table-column prop="suggestion" label="建议" min-width="180" show-overflow-tooltip></el-table-column>
|
|
</el-table>
|
|
<div class="card-list-mobile">
|
|
<div v-for="iss in p.issues" :key="iss.type + iss.detail" class="card-item">
|
|
<div class="card-row"><span class="card-label">类型</span><span class="card-value"><el-tag :type="iss.severity === 'high' ? 'danger' : 'warning'" size="small">{{ iss.type }}</el-tag></span></div>
|
|
<div class="card-row"><span class="card-label">类别</span><span class="card-value">{{ iss.category }}</span></div>
|
|
<div class="card-row"><span class="card-label">详情</span><span class="card-value">{{ iss.detail }}</span></div>
|
|
<div class="card-row"><span class="card-label">建议</span><span class="card-value">{{ iss.suggestion }}</span></div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</el-card>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
</div>
|
|
<script src="vue.global.prod.js"></script>
|
|
<script src="icon-components.js"></script>
|
|
<script src="element-plus.full.js"></script>
|
|
<script>
|
|
const AiSlopApp = {
|
|
data() {
|
|
return {
|
|
isLoggedIn: false, isAdmin: false, currentUser: { username: '' },
|
|
topicId: '', loadingReport: false, report: null, errorMsg: ''
|
|
}
|
|
},
|
|
methods: {
|
|
getToken() { return localStorage.getItem('authToken'); },
|
|
async api(url, opts = {}) {
|
|
const token = this.getToken();
|
|
if (!token) { this.$message.error('请先登录'); setTimeout(() => window.location.href = '/login.html', 1500); return null; }
|
|
const res = await fetch(url, { headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json', ...(opts.headers || {}) }, ...opts });
|
|
if (!res.ok) { const data = await res.json().catch(() => ({})); throw new Error(data.detail || `请求失败: ${res.status}`); }
|
|
return res.json();
|
|
},
|
|
async runReport() {
|
|
this.errorMsg = '';
|
|
if (!this.topicId.trim()) { this.$message.warning('请输入选题 ID'); return; }
|
|
this.loadingReport = true;
|
|
this.report = null;
|
|
try {
|
|
const data = await this.api('/api/ai-slop/report?topic_id=' + encodeURIComponent(this.topicId.trim()));
|
|
this.report = { topic_id: data.topic_id, platforms: (data.platforms || []).map(p => ({ ...p, purifying: false, message: '', score_before: null, score_after: null, issues_before: [], issues_after: [] })) };
|
|
} catch (error) {
|
|
this.errorMsg = error.message;
|
|
} finally { this.loadingReport = false; }
|
|
},
|
|
async purify(p) {
|
|
p.purifying = true; p.message = '';
|
|
try {
|
|
const data = await this.api('/api/ai-slop/purify', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ topic_id: this.topicId.trim(), platform: p.platform })
|
|
});
|
|
p.score_before = data.score_before;
|
|
p.score_after = data.score_after;
|
|
p.issues_before = data.issues_before || [];
|
|
p.issues_after = data.issues_after || [];
|
|
p.issues = data.issues_after || [];
|
|
p.score = data.score_after;
|
|
p.passed = (data.score_after !== null && data.issues_after.length === 0) || (data.issues_after && data.issues_after.every(i => i.severity !== 'high')) || (p.score >= 80);
|
|
p.passed = data.score_after !== null ? (data.issues_after || []).every(i => i.severity !== 'high') : p.passed;
|
|
p.message = data.message || '净化完成';
|
|
this.$message.success('净化完成,合规分 ' + data.score_before + ' → ' + data.score_after);
|
|
} catch (error) {
|
|
this.$message.error('净化失败: ' + error.message);
|
|
} finally { p.purifying = false; }
|
|
},
|
|
handleLogout() { localStorage.removeItem('authToken'); localStorage.removeItem('userRole'); localStorage.removeItem('currentUser'); window.location.href = '/login.html'; },
|
|
redirectToPage(page) { window.location.href = page.startsWith('/') ? page : '/' + page; },
|
|
platformLabel(p) { return { zhihu: '知乎', wechat: '微信公众号', xiaohongshu: '小红书' }[p] || p; },
|
|
},
|
|
mounted() {
|
|
const token = localStorage.getItem('authToken');
|
|
if (!token) { window.location.href = '/login.html'; return; }
|
|
const params = new URLSearchParams(window.location.search);
|
|
const tid = params.get('topic_id');
|
|
if (tid) this.topicId = tid;
|
|
fetch('/api/auth/me', { headers: { 'Authorization': 'Bearer ' + token } })
|
|
.then(r => r.ok ? r.json() : Promise.reject())
|
|
.then(data => {
|
|
this.currentUser = data.user; this.isAdmin = data.user.role === 'admin'; this.isLoggedIn = true;
|
|
if (tid) this.runReport();
|
|
})
|
|
.catch(() => { localStorage.removeItem('authToken'); localStorage.removeItem('userRole'); localStorage.removeItem('currentUser'); window.location.href = '/login.html'; });
|
|
}
|
|
};
|
|
const app = Vue.createApp(AiSlopApp);
|
|
app.use(ElementPlus);
|
|
if (window.installIcons) { window.installIcons(app); }
|
|
if (window.installUniNav) { window.installUniNav(app); }
|
|
app.mount('#app');
|
|
</script>
|
|
</body>
|
|
</html>
|