Files
yu-zhi-ran/platform/frontend/ai-slop.html
T
yuzhiran 7e953afbd2 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 市场调研简报
2026-07-11 12:36:01 +08:00

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> &nbsp;&nbsp; 净化后分:<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>