feat: 添加代理隧道连接失败的重试机制

refactor(crawler): 在各爬虫服务中实现代理错误重试逻辑
feat(uni-app): 新增投标项目查看器的uni-app版本
This commit is contained in:
dmy
2026-01-15 14:03:46 +08:00
parent 37200aa115
commit 20c7c0da0c
31 changed files with 1693 additions and 12 deletions

View 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>

View 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>

View 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>