feat: GEO/SEO structured data + search ranking tracker

This commit is contained in:
Yuzhiran Dev
2026-06-09 17:18:09 +08:00
parent 23ff63baa9
commit 8595bbc521
26 changed files with 961 additions and 51 deletions
+160 -9
View File
@@ -8,8 +8,8 @@
<link rel="stylesheet" href="theme-modern.css">
<style>
.selected-count { color: var(--color-text-secondary); font-size: var(--font-size-body); margin-left: auto; }
.preview-dialog-custom .el-dialog__body { overflow-y: auto; max-height: calc(90vh - 120px); min-height: 400px; padding: 16px 20px; }
.preview-dialog-custom .preview-body { max-height: calc(90vh - 180px); min-height: 300px; overflow: auto; }
.preview-dialog-custom.el-dialog .el-dialog__body { overflow-y: auto; min-height: 400px; padding: 16px 20px; }
.preview-dialog-custom .preview-body { min-height: 300px; overflow: auto; }
.preview-dialog-custom .preview-body img { max-width: 100%; }
.topic-card-list { display: none; }
@media (max-width: 768px) {
@@ -41,10 +41,11 @@
<div class="page-header">
<h2 class="page-title"><el-icon style="vertical-align:-2px;"><IconTopic /></el-icon> 选题管理</h2>
<div class="toolbar">
<el-button type="primary" size="small" @click="refreshAll"><el-icon style="vertical-align:-2px;"><IconRefresh /></el-icon> 批量刷</el-button>
<el-button type="primary" size="small" @click="openCreateDialog"><el-icon style="vertical-align:-2px;"><IconPlus /></el-icon>增选题</el-button>
<el-button size="small" @click="refreshAll"><el-icon style="vertical-align:-2px;"><IconRefresh /></el-icon> 批量刷新</el-button>
<el-button size="small" @click="toggleSelectAll">{{ selectAllLabel }}</el-button>
<el-button type="success" size="small" @click="triggerGenerateSelected" :disabled="selectedTopicIds.length === 0"><el-icon style="vertical-align:-2px;"><IconPlus /></el-icon> 批量创作</el-button>
<el-button type="warning" size="small" @click="triggerReviewSelected" :disabled="selectedTopicIds.length === 0"><el-icon style="vertical-align:-2px;"><IconSearch /></el-icon> 批量审查</el-button>
<el-button type="success" size="small" @click="triggerGenerateSelected" :disabled="selectedTopicIds.length === 0"> 批量创作</el-button>
<el-button type="warning" size="small" @click="triggerReviewSelected" :disabled="selectedTopicIds.length === 0"> 批量审查</el-button>
<span v-if="selectedTopicIds.length > 0" class="selected-count">已选 {{ selectedTopicIds.length }} 项</span>
</div>
</div>
@@ -162,6 +163,82 @@
</div>
</main>
</div>
<el-dialog v-model="createDialogVisible" :title="createStep === 1 ? '新增选题' : 'AI 分析结果'" width="680px" :close-on-click-modal="false" @close="resetCreateDialog">
<template v-if="createStep === 1">
<el-form label-position="top">
<el-form-item label="选题思路 / 内容描述">
<el-input type="textarea" :rows="5" v-model="createForm.raw_input" placeholder="描述你想写的选题方向,可以是大致想法、关键词、或者一句话需求&#10;例如:想写一篇关于旧物换补贴的实操指南,结合最近的碳普惠政策" />
</el-form-item>
<el-form-item label="参考链接(可选,每行一个)">
<el-input type="textarea" :rows="3" v-model="createForm.reference_links" placeholder="https://...&#10;https://..." />
</el-form-item>
<el-form-item label="所属领域(可选)">
<el-select v-model="createForm.field_id" placeholder="选择领域" clearable style="width:100%;">
<el-option v-for="f in fieldOptions" :key="f.id" :label="f.name" :value="f.id" />
</el-select>
</el-form-item>
</el-form>
</template>
<template v-else>
<el-form label-position="top">
<el-form-item label="选题标题">
<el-input v-model="createResult.title" />
</el-form-item>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="内容形式">
<el-select v-model="createResult.format" style="width:100%;">
<el-option label="趋势洞察" value="趋势洞察" />
<el-option label="实操指南" value="实操指南" />
<el-option label="对比分析" value="对比分析" />
<el-option label="案例解读" value="案例解读" />
<el-option label="观点讨论" value="观点讨论" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="优先级">
<el-select v-model="createResult.priority" style="width:100%;">
<el-option label="高" value="高" />
<el-option label="中" value="中" />
<el-option label="低" value="低" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="核心概念">
<el-input v-model="createResult.core_concept" />
</el-form-item>
<el-form-item label="受众痛点">
<el-input v-model="createResult.audience_pain" />
</el-form-item>
<el-form-item label="独特角度">
<el-input v-model="createResult.unique_angle" />
</el-form-item>
<el-form-item label="标签(回车添加)">
<el-select v-model="createResult.tags" multiple filterable allow-create default-first-option style="width:100%;" placeholder="输入标签后回车添加">
<el-option v-for="t in createResult.tags" :key="t" :label="t" :value="t" />
</el-select>
</el-form-item>
</el-form>
</template>
<template #footer>
<div style="display:flex; justify-content:space-between; width:100%;">
<div>
<el-button v-if="createStep === 2" size="small" @click="createStep = 1">← 返回编辑输入</el-button>
<el-button v-else @click="createDialogVisible = false">取消</el-button>
</div>
<div v-if="createStep === 1">
<el-button type="primary" @click="runAiAnalysis" :loading="analyzing">AI 智能分析</el-button>
</div>
<div v-else>
<el-button @click="saveCreatedTopic('list')">仅保存到选题列表</el-button>
<el-button type="success" @click="saveCreatedTopic('create')" :loading="saving">保存并创作</el-button>
<el-button type="warning" @click="saveCreatedTopic('review')" :loading="saving">保存并审查</el-button>
</div>
</div>
</template>
</el-dialog>
<el-dialog v-model="publishDialogVisible" title="发布确认" width="420px" :close-on-click-modal="false">
<div v-if="publishTopic">
<div style="margin-bottom:16px;">
@@ -253,10 +330,14 @@ const TopicsApp = {
publishPlatforms: { zhihu: true, wechat: true, xiaohongshu: true },
publishing: false,
savingContent: false,
currentPage: 1, pageSize: 10,
sortField: 'created_at', sortOrder: 'ascending',
showSearch: false,
searchForm: { id: '', title: '', field: '', createdStart: null, createdEnd: null, generatedStart: null, generatedEnd: null, reviewedStart: null, reviewedEnd: null, publishedStart: null, publishedEnd: null }
currentPage: 1, pageSize: 10,
sortField: 'created_at', sortOrder: 'ascending',
showSearch: false,
searchForm: { id: '', title: '', field: '', createdStart: null, createdEnd: null, generatedStart: null, generatedEnd: null, reviewedStart: null, reviewedEnd: null, publishedStart: null, publishedEnd: null },
createDialogVisible: false, createStep: 1, analyzing: false, saving: false,
fieldOptions: [],
createForm: { raw_input: '', reference_links: '', field_id: null },
createResult: { title: '', format: '趋势洞察', core_concept: '', audience_pain: '', unique_angle: '', tags: [], priority: '中' }
}
},
computed: {
@@ -558,6 +639,76 @@ const TopicsApp = {
this.$message.error('删除失败: ' + msg);
}
},
async openCreateDialog() {
this.createDialogVisible = true;
this.createStep = 1;
this.createForm = { raw_input: '', reference_links: '', field_id: null };
this.createResult = { title: '', format: '趋势洞察', core_concept: '', audience_pain: '', unique_angle: '', tags: [], priority: '中' };
try {
this.fieldOptions = await this.api('/api/topic-config/fields') || [];
} catch (e) { this.fieldOptions = []; }
},
resetCreateDialog() {
this.createStep = 1; this.analyzing = false; this.saving = false;
},
async runAiAnalysis() {
if (!this.createForm.raw_input.trim()) { this.$message.warning('请输入选题思路'); return; }
this.analyzing = true;
try {
const refs = this.createForm.reference_links.split('\n').map(s => s.trim()).filter(Boolean);
const data = await this.api('/api/topics/analyze', {
method: 'POST',
body: JSON.stringify({
raw_input: this.createForm.raw_input,
reference_links: refs,
field_id: this.createForm.field_id || null
})
});
this.createResult.title = data.title || '';
this.createResult.format = data.format || '趋势洞察';
this.createResult.core_concept = data.core_concept || '';
this.createResult.audience_pain = data.audience_pain || '';
this.createResult.unique_angle = data.unique_angle || '';
this.createResult.tags = data.tags || [];
this.createResult.priority = data.priority || '中';
this.createStep = 2;
this.$message.success('AI 分析完成,请确认选题信息');
} catch (e) { this.$message.error('AI 分析失败: ' + (e.message || '未知错误')); }
finally { this.analyzing = false; }
},
async saveCreatedTopic(action) {
this.saving = true;
try {
const body = {
title: this.createResult.title,
field_id: this.createForm.field_id || null,
format: this.createResult.format,
core_concept: this.createResult.core_concept,
audience_pain: this.createResult.audience_pain,
unique_angle: this.createResult.unique_angle,
tags: this.createResult.tags,
priority: this.createResult.priority,
};
const topic = await this.api('/api/topics', { method: 'POST', body: JSON.stringify(body) });
const topicId = topic.id;
this.createDialogVisible = false;
this.$message.success(`选题 ${topicId} 创建成功`);
await this.fetchTopics();
if (action === 'create') {
try {
await this.api('/api/tasks/run-creator?topic_id=' + topicId, { method: 'POST' });
this.$message.success(`创作任务已启动: ${topic.title}`);
} catch (e) { this.$message.error(`创作启动失败: ${e.message}`); }
} else if (action === 'review') {
try {
await this.api('/api/system/review/run', { method: 'POST', body: JSON.stringify({ topic_ids: [topicId] }) });
this.$message.success(`审查已启动: ${topic.title}`);
} catch (e) { this.$message.error(`审查启动失败: ${e.message}`); }
}
await this.fetchTodayCount();
} catch (e) { this.$message.error('保存选题失败: ' + (e.message || '未知错误')); }
finally { this.saving = false; }
},
switchPlatform(platform) {
if (this.editing) this.cancelEdit();
this.previewPlatform = platform;