fix: content quality, image format, task monitor, calendar data source, search UI & sort
This commit is contained in:
+161
-20
@@ -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(); })
|
||||
|
||||
Reference in New Issue
Block a user