Phase1: 效果数据真实化
- 替换scheduler.py中假随机数metrics_sync为知乎API自动获取 - 新增 POST /api/metrics/zhihu-fetch 知乎公开数据API端点 - metrics.html新增「数据录入」tab:手动录入表单+已有数据列表+知乎自动获取 - gitignore清理已跟踪的生成文件(outlines/research/images/cache)
This commit is contained in:
@@ -46,12 +46,17 @@
|
||||
</uni-nav>
|
||||
<div class="main-content">
|
||||
<main class="content-area">
|
||||
<h2 style="font-size: 24px; font-weight: 700; margin-bottom: 24px; color: #303133;"><el-icon style="vertical-align:-2px;"><IconDashboard /></el-icon> 数据分析</h2>
|
||||
<div v-if="loadingDashboard" style="text-align:center;padding:40px 0;color:#909399;">
|
||||
<el-icon style="font-size:32px;margin-bottom:12px;" class="is-loading"><IconLoading /></el-icon>
|
||||
<div>加载中...</div>
|
||||
</div>
|
||||
<div class="card page-fade">
|
||||
<div class="filter-bar" style="margin-bottom:20px;">
|
||||
<el-button size="default" :type="activeTab === 'dashboard' ? 'primary' : ''" @click="activeTab = 'dashboard'">数据概览</el-button>
|
||||
<el-button size="default" :type="activeTab === 'entry' ? 'primary' : ''" @click="activeTab = 'entry'; switchToEntry()">数据录入</el-button>
|
||||
</div>
|
||||
<div v-if="activeTab === 'dashboard'">
|
||||
<h2 style="font-size: 24px; font-weight: 700; margin-bottom: 24px; color: #303133;"><el-icon style="vertical-align:-2px;"><IconDashboard /></el-icon> 数据分析</h2>
|
||||
<div v-if="loadingDashboard" style="text-align:center;padding:40px 0;color:#909399;">
|
||||
<el-icon style="font-size:32px;margin-bottom:12px;" class="is-loading"><IconLoading /></el-icon>
|
||||
<div>加载中...</div>
|
||||
</div>
|
||||
<div class="card page-fade">
|
||||
<h3 style="font-size: 18px; margin-bottom: 16px;"><el-icon style="vertical-align:-2px;"><IconDashboard /></el-icon> 选题状态分布</h3>
|
||||
<div style="display: flex; gap: 20px; flex-wrap: wrap; align-items:center;">
|
||||
<div style="flex:1;min-width:200px;">
|
||||
@@ -169,6 +174,104 @@
|
||||
</div>
|
||||
<div v-else class="empty-state"><el-icon style="font-size:48px;color:#c0c4cc;"><IconInfo /></el-icon><div class="empty-text">暂无选题推荐</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="activeTab === 'entry'" class="page-fade">
|
||||
<h2 style="font-size: 24px; font-weight: 700; margin-bottom: 24px; color: #303133;"><el-icon style="vertical-align:-2px;"><IconEdit /></el-icon> 数据录入</h2>
|
||||
|
||||
<div class="card" style="margin-bottom:20px;">
|
||||
<h3 style="font-size:16px;margin-bottom:16px;">手动录入</h3>
|
||||
<el-form :model="entryForm" label-width="100px" size="small">
|
||||
<el-form-item label="选题">
|
||||
<el-select v-model="entryForm.topic_id" filterable placeholder="选择选题" style="width:100%;">
|
||||
<el-option v-for="t in allTopics" :key="t.id" :label="t.id + ' - ' + t.title" :value="t.id"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="平台">
|
||||
<el-select v-model="entryForm.platform" placeholder="选择平台" style="width:200px;">
|
||||
<el-option label="知乎" value="zhihu"></el-option>
|
||||
<el-option label="微信公众号" value="wechat"></el-option>
|
||||
<el-option label="小红书" value="xiaohongshu"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="发布链接">
|
||||
<el-input v-model="entryForm.publish_url" placeholder="https://..."></el-input>
|
||||
</el-form-item>
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="6"><el-form-item label="阅读"><el-input-number v-model="entryForm.views" :min="0" style="width:100%;"></el-input-number></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="点赞"><el-input-number v-model="entryForm.likes" :min="0" style="width:100%;"></el-input-number></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="收藏"><el-input-number v-model="entryForm.favorites" :min="0" style="width:100%;"></el-input-number></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="评论"><el-input-number v-model="entryForm.comments" :min="0" style="width:100%;"></el-input-number></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="6"><el-form-item label="分享"><el-input-number v-model="entryForm.shares" :min="0" style="width:100%;"></el-input-number></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="submitEntry" :loading="submitting">保存</el-button>
|
||||
<el-button @click="resetEntryForm">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom:20px;">
|
||||
<h3 style="font-size:16px;margin-bottom:16px;">从知乎自动获取</h3>
|
||||
<el-form :model="zhihuForm" label-width="100px" size="small" @submit.prevent="fetchZhihu">
|
||||
<el-form-item label="选题">
|
||||
<el-select v-model="zhihuForm.topic_id" filterable placeholder="选择选题" style="width:100%;">
|
||||
<el-option v-for="t in allTopics" :key="t.id" :label="t.id + ' - ' + t.title" :value="t.id"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="知乎链接">
|
||||
<el-input v-model="zhihuForm.zhihu_url" placeholder="https://zhuanlan.zhihu.com/p/xxxxxx"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="fetchZhihu" :loading="zhihuLoading">获取数据</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="toolbar" style="margin-bottom:12px;">
|
||||
<span style="font-size:15px;font-weight:600;">已有数据</span>
|
||||
<span style="font-size:13px;color:#909399;margin-left:8px;">共 {{ allEntries.length }} 条</span>
|
||||
<el-button size="small" style="margin-left:12px;" @click="fetchEntries">刷新</el-button>
|
||||
</div>
|
||||
<div v-if="entriesLoading" style="text-align:center;padding:20px;color:#909399;">加载中...</div>
|
||||
<template v-else-if="allEntries.length === 0">
|
||||
<div class="empty-state" style="padding:30px 0;">
|
||||
<el-icon style="font-size:48px;color:#c0c4cc;"><IconDashboard /></el-icon>
|
||||
<div class="empty-text">暂无数据,请先录入</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="metrics-table">
|
||||
<el-table :data="allEntries" stripe size="small" style="width:100%;">
|
||||
<el-table-column prop="topic_id" label="选题ID" width="80"></el-table-column>
|
||||
<el-table-column label="平台" width="100">
|
||||
<template #default="scope">{{ getPlatformName(scope.row.platform) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="views" label="阅读" width="70"></el-table-column>
|
||||
<el-table-column prop="likes" label="点赞" width="70"></el-table-column>
|
||||
<el-table-column prop="favorites" label="收藏" width="70"></el-table-column>
|
||||
<el-table-column prop="comments" label="评论" width="70"></el-table-column>
|
||||
<el-table-column prop="shares" label="分享" width="70"></el-table-column>
|
||||
<el-table-column label="互动率" width="80">
|
||||
<template #default="scope">{{ scope.row.engagement_rate || 0 }}%</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="获取时间" width="150">
|
||||
<template #default="scope">{{ formatDate(scope.row.last_fetched) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button size="small" @click="editEntry(scope.row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="deleteEntry(scope.row.id)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
@@ -182,6 +285,7 @@ const MetricsApp = {
|
||||
currentUser: { username: '' },
|
||||
isAdmin: false,
|
||||
isLoggedIn: false,
|
||||
activeTab: 'dashboard',
|
||||
loadingDashboard: false,
|
||||
dashboard: { total_topics: 0, topics_by_status: {}, total_published: 0, total_views: 0, total_likes: 0, avg_engagement_rate: 0, top_topics: [], recent_metrics: [] },
|
||||
trendDays: 30,
|
||||
@@ -189,7 +293,15 @@ const MetricsApp = {
|
||||
maxViews: 1,
|
||||
platformData: [],
|
||||
recommendations: [],
|
||||
chartInstances: {}
|
||||
chartInstances: {},
|
||||
allTopics: [],
|
||||
allEntries: [],
|
||||
entriesLoading: false,
|
||||
submitting: false,
|
||||
zhihuLoading: false,
|
||||
entryForm: { topic_id: '', platform: 'zhihu', publish_url: '', views: 0, likes: 0, favorites: 0, comments: 0, shares: 0 },
|
||||
zhihuForm: { topic_id: '', zhihu_url: '' },
|
||||
editingEntryId: null,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -354,7 +466,92 @@ const MetricsApp = {
|
||||
if (res.ok) this.recommendations = await res.json();
|
||||
else { const d = await res.json().catch(() => ({})); throw new Error(d.detail || '加载失败'); }
|
||||
} catch (e) { console.error(e); this.$message.error('加载推荐失败: ' + e.message); }
|
||||
}
|
||||
},
|
||||
async fetchAllTopics() {
|
||||
try {
|
||||
const token = localStorage.getItem('authToken');
|
||||
const res = await fetch('/api/topics?limit=200', { headers: { 'Authorization': 'Bearer ' + token } });
|
||||
if (res.ok) { const data = await res.json(); this.allTopics = data.topics || data || []; }
|
||||
} catch (e) { console.error(e); }
|
||||
},
|
||||
async fetchEntries() {
|
||||
this.entriesLoading = true;
|
||||
try {
|
||||
const token = localStorage.getItem('authToken');
|
||||
const res = await fetch('/api/metrics/entries?limit=200', { headers: { 'Authorization': 'Bearer ' + token } });
|
||||
if (res.ok) this.allEntries = await res.json();
|
||||
} catch (e) { console.error(e); this.$message.error('加载数据失败'); }
|
||||
finally { this.entriesLoading = false; }
|
||||
},
|
||||
resetEntryForm() {
|
||||
this.entryForm = { topic_id: '', platform: 'zhihu', publish_url: '', views: 0, likes: 0, favorites: 0, comments: 0, shares: 0 };
|
||||
this.editingEntryId = null;
|
||||
},
|
||||
editEntry(row) {
|
||||
this.entryForm = { topic_id: row.topic_id, platform: row.platform, publish_url: row.publish_url || '', views: row.views || 0, likes: row.likes || 0, favorites: row.favorites || 0, comments: row.comments || 0, shares: row.shares || 0 };
|
||||
this.editingEntryId = row.id;
|
||||
this.$message.info('请修改后保存');
|
||||
},
|
||||
async submitEntry() {
|
||||
if (!this.entryForm.topic_id || !this.entryForm.platform) { this.$message.warning('请选择选题和平台'); return; }
|
||||
this.submitting = true;
|
||||
try {
|
||||
const token = localStorage.getItem('authToken');
|
||||
const body = { topic_id: this.entryForm.topic_id, platform: this.entryForm.platform, publish_url: this.entryForm.publish_url, views: this.entryForm.views, likes: this.entryForm.likes, favorites: this.entryForm.favorites, comments: this.entryForm.comments, shares: this.entryForm.shares };
|
||||
if (this.editingEntryId) {
|
||||
const res = await fetch('/api/metrics/entries/' + this.editingEntryId, { method: 'PUT', headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
||||
if (!res.ok) throw new Error('更新失败');
|
||||
this.$message.success('已更新');
|
||||
} else {
|
||||
const res = await fetch('/api/metrics/entries', { method: 'POST', headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
||||
if (!res.ok) throw new Error('保存失败');
|
||||
this.$message.success('已保存');
|
||||
}
|
||||
this.resetEntryForm();
|
||||
this.fetchEntries();
|
||||
} catch (e) { console.error(e); this.$message.error(e.message); }
|
||||
finally { this.submitting = false; }
|
||||
},
|
||||
async deleteEntry(id) {
|
||||
try {
|
||||
const token = localStorage.getItem('authToken');
|
||||
const res = await fetch('/api/metrics/entries/' + id, { method: 'DELETE', headers: { 'Authorization': 'Bearer ' + token } });
|
||||
if (!res.ok) throw new Error('删除失败');
|
||||
this.$message.success('已删除');
|
||||
this.fetchEntries();
|
||||
} catch (e) { console.error(e); this.$message.error(e.message); }
|
||||
},
|
||||
async fetchZhihu() {
|
||||
if (!this.zhihuForm.topic_id || !this.zhihuForm.zhihu_url) { this.$message.warning('请选择选题并填写知乎链接'); return; }
|
||||
this.zhihuLoading = true;
|
||||
try {
|
||||
const token = localStorage.getItem('authToken');
|
||||
const res = await fetch('/api/metrics/zhihu-fetch', { method: 'POST', headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' }, body: JSON.stringify(this.zhihuForm) });
|
||||
if (!res.ok) { const d = await res.json().catch(() => ({ detail: '请求失败' })); throw new Error(d.detail); }
|
||||
const result = await res.json();
|
||||
this.$message.success('已从知乎获取数据');
|
||||
this.fetchEntries();
|
||||
if (result.data) {
|
||||
this.entryForm.topic_id = result.data.topic_id;
|
||||
this.entryForm.platform = result.data.platform;
|
||||
this.entryForm.publish_url = result.data.publish_url || '';
|
||||
this.entryForm.views = result.data.views || 0;
|
||||
this.entryForm.likes = result.data.likes || 0;
|
||||
this.entryForm.favorites = result.data.favorites || 0;
|
||||
this.entryForm.comments = result.data.comments || 0;
|
||||
this.entryForm.shares = result.data.shares || 0;
|
||||
}
|
||||
} catch (e) { console.error(e); this.$message.error(e.message); }
|
||||
finally { this.zhihuLoading = false; }
|
||||
},
|
||||
formatDate(dt) {
|
||||
if (!dt) return '-';
|
||||
try { return new Date(dt).toLocaleString('zh-CN'); } catch { return dt; }
|
||||
},
|
||||
switchToEntry() {
|
||||
this.fetchAllTopics();
|
||||
this.fetchEntries();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.checkAuth();
|
||||
|
||||
Reference in New Issue
Block a user