Compare commits
3 Commits
bdc62a2975
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2475619228 | ||
|
|
eaed16a12e | ||
|
|
4bace565e4 |
@@ -26,7 +26,20 @@
|
|||||||
</el-icon>
|
</el-icon>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="title" label="Title" min-width="150" />
|
<el-table-column label="Title" min-width="150">
|
||||||
|
<template #default="scope">
|
||||||
|
<a
|
||||||
|
v-if="scope.row.url"
|
||||||
|
:href="scope.row.url"
|
||||||
|
target="_blank"
|
||||||
|
class="project-link"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
{{ scope.row.title }}
|
||||||
|
</a>
|
||||||
|
<span v-else>{{ scope.row.title }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column prop="source" label="Source" min-width="100" />
|
<el-table-column prop="source" label="Source" min-width="100" />
|
||||||
<el-table-column prop="publishDate" label="Date" width="95">
|
<el-table-column prop="publishDate" label="Date" width="95">
|
||||||
<template #default="scope">{{ formatDate(scope.row.publishDate) }}</template>
|
<template #default="scope">{{ formatDate(scope.row.publishDate) }}</template>
|
||||||
@@ -126,6 +139,17 @@ const togglePin = async (item: any) => {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.project-link {
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-link:hover {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
.pagination {
|
.pagination {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -24,9 +24,20 @@
|
|||||||
{{ formatDateTime(row.latestPublishDate) }}
|
{{ formatDateTime(row.latestPublishDate) }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="状态" width="70">
|
<el-table-column label="状态" width="80">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag :type="row.error && row.error.trim() ? 'danger' : (row.count > 0 ? 'success' : 'info')" size="small">
|
<el-tag
|
||||||
|
v-if="row.count === -1"
|
||||||
|
type="warning"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
正在更新...
|
||||||
|
</el-tag>
|
||||||
|
<el-tag
|
||||||
|
v-else
|
||||||
|
:type="row.error && row.error.trim() ? 'danger' : (row.count > 0 ? 'success' : 'info')"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
{{ row.error && row.error.trim() ? '出错' : (row.count > 0 ? '正常' : '无数据') }}
|
{{ row.error && row.error.trim() ? '出错' : (row.count > 0 ? '正常' : '无数据') }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="dashboard-ai-container">
|
<div class="dashboard-ai-container">
|
||||||
<div class="dashboard-header">
|
<div class="dashboard-header">
|
||||||
|
<div class="dashboard-title-wrapper">
|
||||||
<h2 class="dashboard-title">Dashboard AI</h2>
|
<h2 class="dashboard-title">Dashboard AI</h2>
|
||||||
|
|
||||||
|
</div>
|
||||||
<el-button type="primary" :loading="loading" @click="fetchAIRecommendations">
|
<el-button type="primary" :loading="loading" @click="fetchAIRecommendations">
|
||||||
<el-icon style="margin-right: 5px"><MagicStick /></el-icon>
|
<el-icon style="margin-right: 5px"><MagicStick /></el-icon>
|
||||||
获取 AI 推荐
|
获取 AI 推荐
|
||||||
@@ -13,6 +16,12 @@
|
|||||||
<el-card class="box-card" shadow="hover">
|
<el-card class="box-card" shadow="hover">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<span>AI 推荐项目</span>
|
<span>AI 推荐项目</span>
|
||||||
|
<span v-if="lastRecommendationTime" class="last-recommendation-time">
|
||||||
|
生成时间: {{ lastRecommendationTime }}
|
||||||
|
</span>
|
||||||
|
<span v-else class="last-recommendation-time text-muted">
|
||||||
|
暂无推荐时间
|
||||||
|
</span>
|
||||||
<el-tag type="success">{{ aiRecommendations.length }} 个推荐</el-tag>
|
<el-tag type="success">{{ aiRecommendations.length }} 个推荐</el-tag>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="loading" style="text-align: center; padding: 40px;">
|
<div v-if="loading" style="text-align: center; padding: 40px;">
|
||||||
@@ -40,7 +49,20 @@
|
|||||||
</el-icon>
|
</el-icon>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="title" label="项目名称" min-width="150" />
|
<el-table-column label="项目名称" min-width="150">
|
||||||
|
<template #default="scope">
|
||||||
|
<a
|
||||||
|
v-if="scope.row.url"
|
||||||
|
:href="scope.row.url"
|
||||||
|
target="_blank"
|
||||||
|
class="project-link"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
{{ scope.row.title }}
|
||||||
|
</a>
|
||||||
|
<span v-else>{{ scope.row.title }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column prop="source" label="来源" min-width="80" />
|
<el-table-column prop="source" label="来源" min-width="80" />
|
||||||
<el-table-column prop="publishDate" label="发布日期" width="95">
|
<el-table-column prop="publishDate" label="发布日期" width="95">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
@@ -102,8 +124,21 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<el-table :data="bidsByDateRange" style="width: 100%" size="small" class="bids-table">
|
<el-table :data="bidsByDateRange" style="width: 100%" size="small" class="bids-table">
|
||||||
<el-table-column prop="title" label="项目名称" min-width="150" />
|
<el-table-column label="项目名称" min-width="150">
|
||||||
<el-table-column prop="source" label="来源" min-width="80" />
|
<template #default="scope">
|
||||||
|
<a
|
||||||
|
v-if="scope.row.url"
|
||||||
|
:href="scope.row.url"
|
||||||
|
target="_blank"
|
||||||
|
class="project-link"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
{{ scope.row.title }}
|
||||||
|
</a>
|
||||||
|
<span v-else>{{ scope.row.title }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="source" label="来源" width="220" />
|
||||||
<el-table-column prop="publishDate" label="发布日期" width="95">
|
<el-table-column prop="publishDate" label="发布日期" width="95">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
{{ formatDate(scope.row.publishDate) }}
|
{{ formatDate(scope.row.publishDate) }}
|
||||||
@@ -146,6 +181,7 @@ const dateRange = ref<[string, string] | null>(null)
|
|||||||
const showAllBids = ref(false)
|
const showAllBids = ref(false)
|
||||||
const bidsLoading = ref(false)
|
const bidsLoading = ref(false)
|
||||||
const bidsByDateRange = ref<any[]>([])
|
const bidsByDateRange = ref<any[]>([])
|
||||||
|
const lastRecommendationTime = ref<string | null>(null)
|
||||||
|
|
||||||
// 从 localStorage 加载保存的日期范围
|
// 从 localStorage 加载保存的日期范围
|
||||||
const loadSavedDateRange = () => {
|
const loadSavedDateRange = () => {
|
||||||
@@ -168,7 +204,10 @@ watch(dateRange, (newDateRange) => {
|
|||||||
const loadLatestRecommendations = async () => {
|
const loadLatestRecommendations = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await api.get('/api/ai/latest-recommendations')
|
const response = await api.get('/api/ai/latest-recommendations')
|
||||||
const recommendations = response.data
|
const { recommendations, generatedAt } = response.data
|
||||||
|
|
||||||
|
// 更新生成时间
|
||||||
|
lastRecommendationTime.value = generatedAt
|
||||||
|
|
||||||
// 获取所有置顶的项目
|
// 获取所有置顶的项目
|
||||||
const pinnedResponse = await api.get('/api/bids/pinned')
|
const pinnedResponse = await api.get('/api/bids/pinned')
|
||||||
@@ -263,6 +302,9 @@ const fetchAIRecommendations = async () => {
|
|||||||
recommendations
|
recommendations
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 更新时间戳
|
||||||
|
lastRecommendationTime.value = new Date().toLocaleString('zh-CN', { hour12: false })
|
||||||
|
|
||||||
ElMessage.success('AI 推荐获取成功')
|
ElMessage.success('AI 推荐获取成功')
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
ElMessage.error('获取 AI 推荐失败')
|
ElMessage.error('获取 AI 推荐失败')
|
||||||
@@ -363,11 +405,26 @@ const togglePin = async (item: AIRecommendation) => {
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dashboard-title-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.dashboard-title {
|
.dashboard-title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.last-recommendation-time {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.last-recommendation-time.text-muted {
|
||||||
|
color: var(--el-text-color-placeholder);
|
||||||
|
}
|
||||||
|
|
||||||
.ai-section {
|
.ai-section {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
}
|
}
|
||||||
@@ -416,6 +473,17 @@ const togglePin = async (item: AIRecommendation) => {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.project-link {
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-link:hover {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
@@ -432,6 +500,11 @@ a:hover {
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dashboard-title-wrapper {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.dashboard-title {
|
.dashboard-title {
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,20 @@
|
|||||||
</el-icon>
|
</el-icon>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="title" label="Title" min-width="150" />
|
<el-table-column label="Title" min-width="150">
|
||||||
|
<template #default="scope">
|
||||||
|
<a
|
||||||
|
v-if="scope.row.url"
|
||||||
|
:href="scope.row.url"
|
||||||
|
target="_blank"
|
||||||
|
class="project-link"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
{{ scope.row.title }}
|
||||||
|
</a>
|
||||||
|
<span v-else>{{ scope.row.title }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column prop="source" label="Source" min-width="90" />
|
<el-table-column prop="source" label="Source" min-width="90" />
|
||||||
<el-table-column prop="publishDate" label="Date" width="100">
|
<el-table-column prop="publishDate" label="Date" width="100">
|
||||||
<template #default="scope">{{ formatDate(scope.row.publishDate) }}</template>
|
<template #default="scope">{{ formatDate(scope.row.publishDate) }}</template>
|
||||||
@@ -372,6 +385,17 @@ if (!dateRange.value) {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.project-link {
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-link:hover {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
.card-header {
|
.card-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|||||||
@@ -30,7 +30,20 @@
|
|||||||
</el-icon>
|
</el-icon>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="title" label="项目名称" min-width="150" />
|
<el-table-column label="项目名称" min-width="150">
|
||||||
|
<template #default="scope">
|
||||||
|
<a
|
||||||
|
v-if="scope.row.url"
|
||||||
|
:href="scope.row.url"
|
||||||
|
target="_blank"
|
||||||
|
class="project-link"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
{{ scope.row.title }}
|
||||||
|
</a>
|
||||||
|
<span v-else>{{ scope.row.title }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column prop="source" label="来源" min-width="100" />
|
<el-table-column prop="source" label="来源" min-width="100" />
|
||||||
<el-table-column prop="publishDate" label="发布日期" width="95">
|
<el-table-column prop="publishDate" label="发布日期" width="95">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
@@ -117,6 +130,17 @@ defineExpose({
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.project-link {
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-link:hover {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export interface AIRecommendation {
|
|||||||
source: string;
|
source: string;
|
||||||
confidence: number;
|
confidence: number;
|
||||||
publishDate?: Date;
|
publishDate?: Date;
|
||||||
|
generatedAt?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -127,10 +128,22 @@ ${JSON.stringify(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getLatestRecommendations(): Promise<AIRecommendation[]> {
|
async getLatestRecommendations(): Promise<{ recommendations: AIRecommendation[]; generatedAt: string | null }> {
|
||||||
this.logger.log('获取最新的 AI 推荐结果');
|
this.logger.log('获取最新的 AI 推荐结果');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 查询最大的 createdAt 作为生成时间
|
||||||
|
const maxCreatedAtResult = await this.aiRecommendationRepository
|
||||||
|
.createQueryBuilder('rec')
|
||||||
|
.select('MAX(rec.createdAt)', 'maxCreatedAt')
|
||||||
|
.getRawOne();
|
||||||
|
|
||||||
|
const generatedAt = maxCreatedAtResult?.maxCreatedAt
|
||||||
|
? new Date(maxCreatedAtResult.maxCreatedAt).toLocaleString('zh-CN', { hour12: false })
|
||||||
|
: null;
|
||||||
|
|
||||||
|
this.logger.log(`AI 推荐生成时间: ${generatedAt}`);
|
||||||
|
|
||||||
const entities = await this.aiRecommendationRepository.find({
|
const entities = await this.aiRecommendationRepository.find({
|
||||||
order: { confidence: 'DESC' },
|
order: { confidence: 'DESC' },
|
||||||
});
|
});
|
||||||
@@ -160,7 +173,7 @@ ${JSON.stringify(
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return result;
|
return { recommendations: result, generatedAt };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error('获取最新 AI 推荐失败:', error);
|
this.logger.error('获取最新 AI 推荐失败:', error);
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -49,7 +49,17 @@ export class CrawlerController {
|
|||||||
this.crawlingSources.add(sourceName);
|
this.crawlingSources.add(sourceName);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 设置状态为正在更新(count = -1)
|
||||||
|
await this.crawlerService.updateCrawlStatus(sourceName, -1);
|
||||||
|
|
||||||
const result = await this.crawlerService.crawlSingleSource(sourceName);
|
const result = await this.crawlerService.crawlSingleSource(sourceName);
|
||||||
|
|
||||||
|
// 更新状态为实际数量
|
||||||
|
await this.crawlerService.updateCrawlStatus(
|
||||||
|
sourceName,
|
||||||
|
result.success ? result.count : 0,
|
||||||
|
);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
} finally {
|
} finally {
|
||||||
this.crawlingSources.delete(sourceName);
|
this.crawlingSources.delete(sourceName);
|
||||||
|
|||||||
@@ -430,4 +430,38 @@ export class BidCrawlerService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新爬虫状态,count = -1 表示正在更新
|
||||||
|
async updateCrawlStatus(source: string, count: number) {
|
||||||
|
try {
|
||||||
|
// 使用原生查询实现 upsert 逻辑
|
||||||
|
await this.crawlInfoRepository.manager.transaction(
|
||||||
|
async (manager) => {
|
||||||
|
// 检查记录是否存在
|
||||||
|
const existing = await manager.findOne(CrawlInfoAdd, {
|
||||||
|
where: { source },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
// 更新现有记录
|
||||||
|
await manager.update(CrawlInfoAdd, { source }, { count });
|
||||||
|
} else {
|
||||||
|
// 插入新记录
|
||||||
|
await manager.save(CrawlInfoAdd, {
|
||||||
|
source,
|
||||||
|
count,
|
||||||
|
latestPublishDate: null,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
this.logger.log(`Updated crawl status for ${source}: ${count}`);
|
||||||
|
} catch (err) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||||
|
this.logger.error(
|
||||||
|
`Failed to update crawl status for ${source}: ${errorMessage}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user