feat: 添加代理隧道连接失败的重试机制
refactor(crawler): 在各爬虫服务中实现代理错误重试逻辑 feat(uni-app): 新增投标项目查看器的uni-app版本
This commit is contained in:
144
uni-app-version/src/components/AiRecommendations.vue
Normal file
144
uni-app-version/src/components/AiRecommendations.vue
Normal file
@@ -0,0 +1,144 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { getAiRecommendations, type AiRecommendation } from '../utils/api'
|
||||
|
||||
const recommendations = ref<AiRecommendation[]>([])
|
||||
const loading = ref<boolean>(true)
|
||||
const error = ref<string>('')
|
||||
|
||||
const loadRecommendations = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
const items = await getAiRecommendations()
|
||||
recommendations.value = items || []
|
||||
} catch (err) {
|
||||
error.value = `加载失败: ${err.message || err}`
|
||||
console.error('加载 AI 推荐失败:', err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const getConfidenceColor = (confidence: number): string => {
|
||||
if (confidence >= 80) return '#27ae60'
|
||||
if (confidence >= 60) return '#f39c12'
|
||||
return '#e74c3c'
|
||||
}
|
||||
|
||||
const getConfidenceLabel = (confidence: number): string => {
|
||||
if (confidence >= 80) return '高'
|
||||
if (confidence >= 60) return '中'
|
||||
return '低'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadRecommendations()
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
loadRecommendations
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="ai-recommendations-container">
|
||||
<view v-if="loading" class="loading">
|
||||
加载中...
|
||||
</view>
|
||||
|
||||
<view v-else-if="error" class="error">
|
||||
{{ error }}
|
||||
</view>
|
||||
|
||||
<view v-else-if="recommendations.length === 0" class="empty">
|
||||
暂无推荐项目
|
||||
</view>
|
||||
|
||||
<view v-else class="recommendation-list">
|
||||
<view
|
||||
v-for="item in recommendations"
|
||||
:key="item.id"
|
||||
class="recommendation-item"
|
||||
>
|
||||
<view class="recommendation-header">
|
||||
<view
|
||||
class="confidence-badge"
|
||||
:style="{ backgroundColor: getConfidenceColor(item.confidence) }"
|
||||
>
|
||||
{{ getConfidenceLabel(item.confidence) }} ({{ item.confidence }}%)
|
||||
</view>
|
||||
<text class="date">{{ item.createdAt }}</text>
|
||||
</view>
|
||||
<text class="recommendation-title">{{ item.title }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ai-recommendations-container {
|
||||
padding: 16rpx;
|
||||
}
|
||||
|
||||
.loading,
|
||||
.error,
|
||||
.empty {
|
||||
text-align: center;
|
||||
padding: 40rpx;
|
||||
color: #666;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
.recommendation-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.recommendation-item {
|
||||
background: #fff;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8rpx;
|
||||
padding: 16rpx;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.recommendation-item:active {
|
||||
border-color: #3498db;
|
||||
box-shadow: 0 2rpx 8rpx rgba(52, 152, 219, 0.15);
|
||||
transform: translateY(-2rpx);
|
||||
}
|
||||
|
||||
.recommendation-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.confidence-badge {
|
||||
color: #fff;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 6rpx;
|
||||
font-size: 20rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.date {
|
||||
font-size: 20rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.recommendation-title {
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
148
uni-app-version/src/components/CrawlInfo.vue
Normal file
148
uni-app-version/src/components/CrawlInfo.vue
Normal file
@@ -0,0 +1,148 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { getCrawlInfoStats, type CrawlInfoStat } from '../utils/api'
|
||||
|
||||
const crawlStats = ref<CrawlInfoStat[]>([])
|
||||
const loading = ref<boolean>(true)
|
||||
const error = ref<string>('')
|
||||
|
||||
const loadCrawlStats = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
const stats = await getCrawlInfoStats()
|
||||
crawlStats.value = stats || []
|
||||
} catch (err) {
|
||||
error.value = `加载失败: ${err.message || err}`
|
||||
console.error('加载爬虫统计信息失败:', err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusText = (stat: CrawlInfoStat): string => {
|
||||
if (stat.error) {
|
||||
return '出错'
|
||||
}
|
||||
if (stat.count > 0) {
|
||||
return '正常'
|
||||
}
|
||||
return '无数据'
|
||||
}
|
||||
|
||||
const getStatusClass = (stat: CrawlInfoStat): string => {
|
||||
if (stat.error) {
|
||||
return 'status-error'
|
||||
}
|
||||
if (stat.count > 0) {
|
||||
return 'status-success'
|
||||
}
|
||||
return 'status-info'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadCrawlStats()
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
loadCrawlStats
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="crawl-info-container">
|
||||
<view v-if="loading" class="loading">
|
||||
加载中...
|
||||
</view>
|
||||
|
||||
<view v-else-if="error" class="error">
|
||||
{{ error }}
|
||||
</view>
|
||||
|
||||
<view v-else-if="crawlStats.length === 0" class="empty">
|
||||
暂无爬虫统计信息
|
||||
</view>
|
||||
|
||||
<view v-else class="crawl-list">
|
||||
<view
|
||||
v-for="stat in crawlStats"
|
||||
:key="stat.source"
|
||||
class="crawl-item"
|
||||
>
|
||||
<text class="source">{{ stat.source }}</text>
|
||||
<view :class="['status', getStatusClass(stat)]">
|
||||
{{ getStatusText(stat) }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.crawl-info-container {
|
||||
padding: 16rpx;
|
||||
}
|
||||
|
||||
.loading,
|
||||
.error,
|
||||
.empty {
|
||||
text-align: center;
|
||||
padding: 40rpx;
|
||||
color: #666;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
.crawl-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.crawl-item {
|
||||
background: #fff;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8rpx;
|
||||
padding: 16rpx 24rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.crawl-item:active {
|
||||
border-color: #3498db;
|
||||
box-shadow: 0 2rpx 8rpx rgba(52, 152, 219, 0.15);
|
||||
}
|
||||
|
||||
.source {
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.status {
|
||||
font-size: 22rpx;
|
||||
padding: 4rpx 16rpx;
|
||||
border-radius: 6rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.status-error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.status-info {
|
||||
background: #e2e3e5;
|
||||
color: #383d41;
|
||||
}
|
||||
</style>
|
||||
147
uni-app-version/src/components/PinnedBids.vue
Normal file
147
uni-app-version/src/components/PinnedBids.vue
Normal file
@@ -0,0 +1,147 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { getPinnedBids, type BidItem } from '../utils/api'
|
||||
|
||||
const bidItems = ref<BidItem[]>([])
|
||||
const loading = ref<boolean>(true)
|
||||
const error = ref<string>('')
|
||||
|
||||
const loadPinnedBids = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
const items = await getPinnedBids()
|
||||
bidItems.value = items || []
|
||||
} catch (err) {
|
||||
error.value = `加载失败: ${err.message || err}`
|
||||
console.error('加载置顶投标项目失败:', err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const openUrl = (url: string) => {
|
||||
// #ifdef H5
|
||||
window.open(url, '_blank')
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
// 在非 H5 平台,可以使用 web-view 或复制链接
|
||||
uni.setClipboardData({
|
||||
data: url,
|
||||
success: () => {
|
||||
uni.showToast({
|
||||
title: '链接已复制',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadPinnedBids()
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
loadPinnedBids
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="pinned-bids-container">
|
||||
<view v-if="loading" class="loading">
|
||||
加载中...
|
||||
</view>
|
||||
|
||||
<view v-else-if="error" class="error">
|
||||
{{ error }}
|
||||
</view>
|
||||
|
||||
<view v-else-if="bidItems.length === 0" class="empty">
|
||||
暂无置顶项目
|
||||
</view>
|
||||
|
||||
<view v-else class="bid-list">
|
||||
<view
|
||||
v-for="item in bidItems"
|
||||
:key="item.id"
|
||||
class="bid-item"
|
||||
@click="openUrl(item.url)"
|
||||
>
|
||||
<view class="bid-header">
|
||||
<text class="source">{{ item.source }}</text>
|
||||
<text class="date">{{ item.publishDate }}</text>
|
||||
</view>
|
||||
<text class="bid-title">{{ item.title }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.pinned-bids-container {
|
||||
padding: 16rpx;
|
||||
}
|
||||
|
||||
.loading,
|
||||
.error,
|
||||
.empty {
|
||||
text-align: center;
|
||||
padding: 40rpx;
|
||||
color: #666;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
.bid-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.bid-item {
|
||||
background: #fff;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8rpx;
|
||||
padding: 16rpx;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.bid-item:active {
|
||||
border-color: #3498db;
|
||||
box-shadow: 0 2rpx 8rpx rgba(52, 152, 219, 0.15);
|
||||
transform: translateY(-2rpx);
|
||||
}
|
||||
|
||||
.bid-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.source {
|
||||
background: #f0f0f0;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 6rpx;
|
||||
font-size: 20rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.date {
|
||||
font-size: 20rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.bid-title {
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user