fix: content quality, image format, task monitor, calendar data source, search UI & sort

This commit is contained in:
Yuzhiran Dev
2026-05-26 11:26:10 +08:00
parent b2d043b231
commit ac8644d752
36 changed files with 2294 additions and 873 deletions
+161 -20
View File
@@ -22,8 +22,11 @@
.topic-card-meta { display: flex; flex-direction: column; gap: 3px; font-size: var(--font-size-caption); color: var(--color-text-regular); margin-bottom: 10px; }
.topic-card-actions { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 4px; margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--color-border); }
.topic-card-actions .el-button { margin: 0; width: 100%; justify-content: center; padding: 8px 4px !important; }
.topic-card.is-checked { border-color: var(--color-primary); box-shadow: 0 0 0 1px rgba(64,158,255,0.2); }
.topic-card-actions .el-button--danger { grid-column: 1 / -1; }
.topic-card.is-checked { border-color: var(--color-primary); box-shadow: 0 0 0 1px rgba(64,158,255,0.2); }
.topic-card-actions .el-button--danger { grid-column: 1 / -1; }
.search-area { background:#f8faff; border:1px solid #e8edf5; border-radius:8px; padding:12px 16px; margin-bottom:12px; }
.search-area .el-form-item { margin-bottom:6px; }
.search-toggle { cursor:pointer; user-select:none; font-size:14px; color:#409eff; }
}
</style>
<script src="uni-nav.js"></script>
@@ -53,9 +56,52 @@
<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>
</div>
<el-table ref="topicTable" :data="paginatedTopics" stripe v-loading="loadingTable" @selection-change="selectedTopicIds = $event.map(item => item.id)">
<div style="margin-bottom:10px;">
<span class="search-toggle" @click="showSearch = !showSearch">🔍 {{ showSearch ? '收起搜索' : '展开搜索' }}</span>
<span v-if="searchActive" style="margin-left:8px;font-size:12px;color:#909399;">筛选条件已生效,共 {{ filteredTopics.length }} 条</span>
</div>
<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="10"><el-form-item label="标题"><el-input v-model="searchForm.title" placeholder="关键词" clearable @input="doSearch"></el-input></el-form-item></el-col>
<el-col :span="8"><el-form-item label="领域"><el-input v-model="searchForm.field" placeholder="如 科技前沿" clearable @input="doSearch"></el-input></el-form-item></el-col>
</el-row>
<el-row :gutter="12">
<el-col :span="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-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-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-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>
</el-form-item></el-col>
</el-row>
<el-row>
<el-col :span="24" style="text-align:right;">
<el-button type="primary" @click="doSearch" size="small">查询</el-button>
<el-button @click="resetSearch" size="small">重置</el-button>
</el-col>
</el-row>
</el-form>
</div>
<el-table ref="topicTable" :data="paginatedTopics" stripe v-loading="loadingTable" @selection-change="selectedTopicIds = $event.map(item => item.id)" @sort-change="handleSortChange" :default-sort="{ prop: 'created_at', order: 'ascending' }">
<el-table-column type="selection" width="55"></el-table-column>
<el-table-column prop="id" label="ID" width="70" fixed></el-table-column>
<el-table-column prop="id" label="ID" width="70" fixed sortable="custom"></el-table-column>
<el-table-column prop="title" label="标题" min-width="200"></el-table-column>
<el-table-column prop="field" label="领域" width="100"></el-table-column>
<el-table-column prop="status" label="状态" width="90">
@@ -64,9 +110,10 @@
<el-table-column prop="compliance_score" label="合规分" width="90">
<template #default="scope"><el-progress :percentage="scope.row.compliance_score || 0" :format="() => scope.row.compliance_score || '-'" :stroke-width="15"></el-progress></template>
</el-table-column>
<el-table-column prop="created_at" label="创建时间" width="140"><template #default="scope">{{ formatDate(scope.row.created_at) }}</template></el-table-column>
<el-table-column prop="generated_at" label="创作时间" width="140"><template #default="scope">{{ scope.row.generated_at ? formatDate(scope.row.generated_at) : '-' }}</template></el-table-column>
<el-table-column prop="published_at" label="发布时间" width="140"><template #default="scope">{{ scope.row.published_at ? formatDate(scope.row.published_at) : '-' }}</template></el-table-column>
<el-table-column prop="created_at" label="创建时间" width="140" sortable="custom"><template #default="scope">{{ formatDate(scope.row.created_at) }}</template></el-table-column>
<el-table-column prop="generated_at" label="创作时间" width="140" sortable="custom"><template #default="scope">{{ scope.row.generated_at ? formatDate(scope.row.generated_at) : '-' }}</template></el-table-column>
<el-table-column prop="reviewed_at" label="审查时间" width="140" sortable="custom"><template #default="scope">{{ scope.row.reviewed_at ? formatDate(scope.row.reviewed_at) : '-' }}</template></el-table-column>
<el-table-column prop="published_at" label="发布时间" width="140" sortable="custom"><template #default="scope">{{ scope.row.published_at ? formatDate(scope.row.published_at) : '-' }}</template></el-table-column>
<el-table-column label="操作" width="230" fixed="right">
<template #default="scope">
<div style="display: flex; gap: 4px; white-space: nowrap;">
@@ -100,6 +147,7 @@
<div class="topic-card-meta">
<div>创建: {{ formatDate(topic.created_at) }}</div>
<div>创作: {{ topic.generated_at ? formatDate(topic.generated_at) : '-' }}</div>
<div>审查: {{ topic.reviewed_at ? formatDate(topic.reviewed_at) : '-' }}</div>
<div>发布: {{ topic.published_at ? formatDate(topic.published_at) : '-' }}</div>
</div>
<div class="topic-card-actions">
@@ -177,6 +225,7 @@
<span style="margin: 0 8px;">|</span>
<span>状态:{{ getStatusLabel(previewTopic.status) }}</span>
<span v-if="previewTopic.generated_at" style="margin-left:8px;">创作:{{ formatDate(previewTopic.generated_at) }}</span>
<span v-if="previewTopic.reviewed_at" style="margin-left:8px;">审查:{{ formatDate(previewTopic.reviewed_at) }}</span>
<span v-if="previewTopic.published_at" style="margin-left:8px;">发布:{{ formatDate(previewTopic.published_at) }}</span>
</div>
<div>
@@ -204,16 +253,66 @@ const TopicsApp = {
publishPlatforms: { zhihu: true, wechat: true, xiaohongshu: true },
publishing: false,
savingContent: false,
currentPage: 1, pageSize: 10
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 }
}
},
computed: {
searchActive() {
const f = this.searchForm;
return !!(f.id || f.title || f.field || f.createdStart || f.createdEnd || f.generatedStart || f.generatedEnd || f.reviewedStart || f.reviewedEnd || f.publishedStart || f.publishedEnd);
},
filteredTopics() {
if (this.filterStatus === 'today') return this.todayTopics;
if (!this.filterStatus) return this.topics;
const map = { 'pending': ['pending','待处理'], 'review': ['review','待审查'], 'ready': ['ready','待发布'], 'published': ['published','已发布'] };
const allowed = map[this.filterStatus] || [this.filterStatus];
return this.topics.filter(t => allowed.includes(t.status));
let list;
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 allowed = map[this.filterStatus] || [this.filterStatus];
list = this.topics.filter(t => allowed.includes(t.status));
}
// 搜索过滤
const sf = this.searchForm;
if (sf.id || sf.title || sf.field || sf.createdStart || sf.createdEnd || sf.generatedStart || sf.generatedEnd || sf.reviewedStart || sf.reviewedEnd || sf.publishedStart || sf.publishedEnd) {
list = list.filter(t => {
if (sf.id && !t.id.toLowerCase().includes(sf.id.toLowerCase())) return false;
if (sf.title && !t.title.toLowerCase().includes(sf.title.toLowerCase())) return false;
if (sf.field && !(t.field || '').toLowerCase().includes(sf.field.toLowerCase())) return false;
const inRange = (val, start, end) => {
if (!val) return !start && !end;
const d = val.slice(0, 10);
if (start && d < start) return false;
if (end && d > end) return false;
return true;
};
return inRange(t.created_at, sf.createdStart, sf.createdEnd)
&& inRange(t.generated_at, sf.generatedStart, sf.generatedEnd)
&& inRange(t.reviewed_at, sf.reviewedStart, sf.reviewedEnd)
&& inRange(t.published_at, sf.publishedStart, sf.publishedEnd);
});
}
if (this.sortField) {
const field = this.sortField;
const isDateField = ['created_at','generated_at','reviewed_at','published_at','updated_at'].includes(field);
list = [...list].sort((a, b) => {
const va = a[field], vb = b[field];
if (!va && !vb) return 0;
if (!va) return 1;
if (!vb) return -1;
let cmp;
if (isDateField) {
const ta = new Date(va.replace(' ', 'T')).getTime();
const tb = new Date(vb.replace(' ', 'T')).getTime();
cmp = ta - tb;
} else {
cmp = String(va).localeCompare(String(vb), 'zh', { numeric: true });
}
return this.sortOrder === 'descending' ? -cmp : cmp;
});
}
return list;
},
paginatedTopics() {
const start = (this.currentPage - 1) * this.pageSize;
@@ -356,13 +455,42 @@ const TopicsApp = {
const title = titleEl ? titleEl.textContent.trim() : (this.previewTopic?.title || '');
const body = doc.body;
if (body) {
body.querySelectorAll('script, style, nav, .header, .tags, footer, .interaction, svg, img, button').forEach(el => el.remove());
body.querySelectorAll('script, style, nav, .header, .tags, footer, .interaction, button, .ad, aside, .comment').forEach(el => el.remove());
}
if (platform === 'xiaohongshu') {
const lines = [];
body.querySelectorAll('h1,h2,h3,h4,p,li').forEach(el => {
const text = el.textContent.trim();
if (!text || text.length < 2) return;
const tag = el.tagName.toLowerCase();
const prefix = tag.startsWith('h') ? '\n### ' : '- ';
const clean = text.replace(/\n/g, ' ').replace(/^[\s#]+|[\s#]+$/g, '');
if (clean) lines.push(prefix + clean);
});
const md = `**${title}**\n\n${lines.join('\n')}`;
navigator.clipboard.writeText(md).then(() => this.$message.success('✅ 已复制 Markdown(小红书格式)')).catch(() => this.$message.error('❌ 复制失败'));
} else {
const container = doc.createElement('div');
if (titleEl) {
const h1 = doc.createElement('h1');
h1.textContent = title;
container.appendChild(h1);
}
body.querySelectorAll('h2,h3,h4,p,li,blockquote,img,pre,code,table,hr').forEach(el => {
const clone = el.cloneNode(true);
container.appendChild(clone);
});
const cleanHtml = container.innerHTML;
const blob = new Blob([cleanHtml], { type: 'text/html' });
const richText = new Blob([cleanHtml], { type: 'text/plain' });
const item = new ClipboardItem({ 'text/html': blob, 'text/plain': richText });
navigator.clipboard.write([item]).then(() => {
this.$message.success(`✅ 已复制(含格式和配图),Ctrl+V 粘贴到${platform === 'zhihu' ? '知乎' : '微信公众号'}`);
}).catch(() => {
navigator.clipboard.writeText(cleanHtml).then(() => this.$message.success('✅ 已复制 HTML')).catch(() => this.$message.error('❌ 复制失败'));
});
}
const contentEls = body ? Array.from(body.querySelectorAll('p, h1, h2, h3, h4, li')) : [];
const text = contentEls.map(el => el.textContent.trim()).filter(t => t && t.length > 1).join('\n\n');
navigator.clipboard.writeText(`标题:${title}\n\n内容:\n${text}`)
.then(() => this.$message.success(`已复制内容,请前往${platform}粘贴发布`))
.catch(() => this.$message.error('复制失败,请手动复制'));
},
async createTopic(topic) {
if (this.isStatus(topic, 'published')) { this.$message.info('已发布选题不可创作'); return; }
@@ -451,6 +579,11 @@ const TopicsApp = {
} catch (e) { this.$message.error('保存失败: ' + e.message); }
finally { this.savingContent = false; }
},
doSearch() { this.currentPage = 1; },
resetSearch() {
this.searchForm = { id: '', title: '', field: '', createdStart: null, createdEnd: null, generatedStart: null, generatedEnd: null, reviewedStart: null, reviewedEnd: null, publishedStart: null, publishedEnd: null };
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; },
@@ -471,6 +604,11 @@ const TopicsApp = {
table.toggleRowSelection(row, !allChecked);
}
},
handleSortChange({ prop, order }) {
this.sortField = prop || 'created_at';
this.sortOrder = order || 'ascending';
this.currentPage = 1;
},
formatDate(dateStr) {
if (!dateStr) return '-';
try { return new Date(dateStr.replace(' ', 'T')).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); }
@@ -482,8 +620,11 @@ const TopicsApp = {
mounted() {
const token = localStorage.getItem('authToken');
if (!token) { window.location.href = '/login.html'; return; }
const urlFilter = new URLSearchParams(window.location.search).get('filter');
const params = new URLSearchParams(window.location.search);
const urlFilter = params.get('filter');
if (urlFilter) this.filterStatus = urlFilter;
const topicId = params.get('topic_id');
if (topicId) { this.searchForm.id = topicId; this.showSearch = true; }
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; this.fetchTopics(); this.fetchTodayCount(); })