|
|
|
@ -0,0 +1,415 @@
|
|
|
|
|
<template>
|
|
|
|
|
<GiPageLayout>
|
|
|
|
|
<div class="project-list-container">
|
|
|
|
|
<!-- 顶部搜索和操作区域 -->
|
|
|
|
|
<div class="header-section">
|
|
|
|
|
<div class="search-area">
|
|
|
|
|
<a-input-search
|
|
|
|
|
v-model="searchKeyword"
|
|
|
|
|
placeholder="搜索项目名称"
|
|
|
|
|
allow-clear
|
|
|
|
|
@search="handleSearch"
|
|
|
|
|
@clear="handleClear"
|
|
|
|
|
style="width: 300px"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="action-area">
|
|
|
|
|
<a-space>
|
|
|
|
|
<a-button type="primary" @click="startCrawler">
|
|
|
|
|
<template #icon><icon-play-arrow /></template>
|
|
|
|
|
开始爬虫
|
|
|
|
|
</a-button>
|
|
|
|
|
<a-button @click="refreshData">
|
|
|
|
|
<template #icon><icon-refresh /></template>
|
|
|
|
|
刷新数据
|
|
|
|
|
</a-button>
|
|
|
|
|
<a-button @click="exportData">
|
|
|
|
|
<template #icon><icon-download /></template>
|
|
|
|
|
导出数据
|
|
|
|
|
</a-button>
|
|
|
|
|
<a-button @click="openCrawlerSettings">
|
|
|
|
|
<template #icon><icon-settings /></template>
|
|
|
|
|
爬虫设置
|
|
|
|
|
</a-button>
|
|
|
|
|
</a-space>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 选项卡区域 -->
|
|
|
|
|
<div class="tabs-section">
|
|
|
|
|
<a-tabs
|
|
|
|
|
v-model:active-key="activeTab"
|
|
|
|
|
type="line"
|
|
|
|
|
size="large"
|
|
|
|
|
@change="handleTabChange"
|
|
|
|
|
>
|
|
|
|
|
<a-tab-pane key="all" tab="信息检索" title="信息检索">
|
|
|
|
|
<ProjectTable
|
|
|
|
|
:data="pagedData"
|
|
|
|
|
:loading="loading"
|
|
|
|
|
@view="handleView"
|
|
|
|
|
@enter="handleEnter"
|
|
|
|
|
/>
|
|
|
|
|
<div class="pagination-wrapper">
|
|
|
|
|
<a-pagination
|
|
|
|
|
v-model:current="currentPage"
|
|
|
|
|
v-model:page-size="pageSize"
|
|
|
|
|
:total="totalItems"
|
|
|
|
|
show-total
|
|
|
|
|
show-jumper
|
|
|
|
|
show-page-size
|
|
|
|
|
:page-size-options="[10, 20, 50, 100]"
|
|
|
|
|
@change="handlePageChange"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</a-tab-pane>
|
|
|
|
|
<a-tab-pane key="my" tab="投标响应" title="投标响应">
|
|
|
|
|
<ProjectTable
|
|
|
|
|
:data="myPagedData"
|
|
|
|
|
:loading="loading"
|
|
|
|
|
@view="handleView"
|
|
|
|
|
@enter="handleEnter"
|
|
|
|
|
/>
|
|
|
|
|
<div class="pagination-wrapper">
|
|
|
|
|
<a-pagination
|
|
|
|
|
v-model:current="myCurrentPage"
|
|
|
|
|
v-model:page-size="pageSize"
|
|
|
|
|
:total="myTotalItems"
|
|
|
|
|
show-total
|
|
|
|
|
show-jumper
|
|
|
|
|
show-page-size
|
|
|
|
|
:page-size-options="[10, 20, 50, 100]"
|
|
|
|
|
@change="handleMyPageChange"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</a-tab-pane>
|
|
|
|
|
</a-tabs>
|
|
|
|
|
</div>
|
|
|
|
|
<CrawlerSettings
|
|
|
|
|
v-model:visible="crawlerSettingsVisible"
|
|
|
|
|
@save="handleSaveSettings"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</GiPageLayout>
|
|
|
|
|
<BiddingDetailModal
|
|
|
|
|
v-model:visible="detailVisible"
|
|
|
|
|
:detail="currentDetail"
|
|
|
|
|
:uploading="uploading"
|
|
|
|
|
@upload="handleUploadDocument"
|
|
|
|
|
/>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { ref, reactive, computed, onMounted } from 'vue'
|
|
|
|
|
import { Message } from '@arco-design/web-vue'
|
|
|
|
|
import ProjectTable from './components/InformationTable.vue'
|
|
|
|
|
import CrawlerSettings from './components/CrawlerSettings.vue'
|
|
|
|
|
import BiddingDetailModal from './components/BiddingDetailModal.vue'
|
|
|
|
|
|
|
|
|
|
import type { ProjectData, TabKey,BiddingDetail } from './types'
|
|
|
|
|
import { ProjectStatus } from './types'
|
|
|
|
|
|
|
|
|
|
defineOptions({ name: 'ProjectList' })
|
|
|
|
|
|
|
|
|
|
// 响应式数据
|
|
|
|
|
const loading = ref(false)
|
|
|
|
|
const activeTab = ref<TabKey>('all')
|
|
|
|
|
const searchKeyword = ref('')
|
|
|
|
|
|
|
|
|
|
const crawlerSettingsVisible = ref(false)
|
|
|
|
|
|
|
|
|
|
// 分页相关数据
|
|
|
|
|
const currentPage = ref(1)
|
|
|
|
|
const myCurrentPage = ref(1)
|
|
|
|
|
const pageSize = ref(10)
|
|
|
|
|
|
|
|
|
|
// 添加状态
|
|
|
|
|
const detailVisible = ref(false)
|
|
|
|
|
const uploading = ref(false)
|
|
|
|
|
const currentDetail = ref<BiddingDetail>({
|
|
|
|
|
id: 0,
|
|
|
|
|
projectName: '',
|
|
|
|
|
biddingUnit: '',
|
|
|
|
|
budgetAmount: 0,
|
|
|
|
|
deadline: '',
|
|
|
|
|
crawlingTime: '',
|
|
|
|
|
projectLocation: '',
|
|
|
|
|
projectDuration: '',
|
|
|
|
|
biddingScope: '',
|
|
|
|
|
qualificationRequirements: '',
|
|
|
|
|
sourcePlatform: '',
|
|
|
|
|
biddingDocuments: '',
|
|
|
|
|
biddingContent: '',
|
|
|
|
|
contentItems: []
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 项目数据
|
|
|
|
|
const projectList = ref<ProjectData[]>([
|
|
|
|
|
{
|
|
|
|
|
id: 1,
|
|
|
|
|
projectName: 'A风场2023年检查',
|
|
|
|
|
biddingUnit: 15,
|
|
|
|
|
budgetAmount:111,
|
|
|
|
|
deadline: '2023-10-01',
|
|
|
|
|
crawlingTime: '2023-12-31',
|
|
|
|
|
sourcePlatform:"中国招标投标网",
|
|
|
|
|
biddingDocuments:"333.pdf",
|
|
|
|
|
status: ProjectStatus.IN_PROGRESS,
|
|
|
|
|
manager: '张三',
|
|
|
|
|
isMyProject: true
|
|
|
|
|
},
|
|
|
|
|
// 添加更多示例数据用于测试分页
|
|
|
|
|
...Array.from({ length: 25 }, (_, i) => ({
|
|
|
|
|
id: i + 2,
|
|
|
|
|
projectName: `项目${i + 2}`,
|
|
|
|
|
biddingUnit: Math.floor(Math.random() * 20) + 1,
|
|
|
|
|
budgetAmount: Math.floor(Math.random() * 1000) + 100,
|
|
|
|
|
deadline: `2023-${(Math.floor(Math.random() * 12) + 1).toString().padStart(2, '0')}-${(Math.floor(Math.random() * 28) + 1).toString().padStart(2, '0')}`,
|
|
|
|
|
crawlingTime: `2023-${(Math.floor(Math.random() * 12) + 1).toString().padStart(2, '0')}-${(Math.floor(Math.random() * 28) + 1).toString().padStart(2, '0')}`,
|
|
|
|
|
sourcePlatform: "中国招标投标网",
|
|
|
|
|
biddingDocuments: `文档${i + 2}.pdf`,
|
|
|
|
|
status: Math.random() > 0.5 ? ProjectStatus.IN_PROGRESS : ProjectStatus.COMPLETED,
|
|
|
|
|
manager: ['张三', '李四', '王五'][Math.floor(Math.random() * 3)],
|
|
|
|
|
isMyProject: Math.random() > 0.7
|
|
|
|
|
}))
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
const handleSaveSettings = (settings: any) => {
|
|
|
|
|
console.log('Saved settings:', settings)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 处理查看详情
|
|
|
|
|
const handleView = (project: ProjectData) => {
|
|
|
|
|
// 这里应该根据项目ID获取详细数据,这里简化处理
|
|
|
|
|
currentDetail.value = {
|
|
|
|
|
id: project.id,
|
|
|
|
|
projectName: project.projectName,
|
|
|
|
|
biddingUnit: project.biddingUnit.toString(),
|
|
|
|
|
budgetAmount: project.budgetAmount,
|
|
|
|
|
deadline: project.deadline,
|
|
|
|
|
crawlingTime: project.crawlingTime,
|
|
|
|
|
projectLocation: '内蒙古自治区', // 示例数据
|
|
|
|
|
projectDuration: '6个月', // 示例数据
|
|
|
|
|
biddingScope: '风电场50台机组叶片检查服务', // 示例数据
|
|
|
|
|
qualificationRequirements: '具备风电叶片检查资质', // 示例数据
|
|
|
|
|
sourcePlatform: project.sourcePlatform,
|
|
|
|
|
biddingDocuments: project.biddingDocuments,
|
|
|
|
|
biddingContent: '本项目为某风电场2023年叶片检查服务采购,包括但不限于:',
|
|
|
|
|
contentItems: [
|
|
|
|
|
'叶片外观检查',
|
|
|
|
|
'无损检测',
|
|
|
|
|
'缺陷记录与评估',
|
|
|
|
|
'检查报告编制'
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
detailVisible.value = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 计算属性 - 根据搜索关键词过滤项目
|
|
|
|
|
const filteredProjects = computed(() => {
|
|
|
|
|
if (!searchKeyword.value) {
|
|
|
|
|
return projectList.value
|
|
|
|
|
}
|
|
|
|
|
return projectList.value.filter(project =>
|
|
|
|
|
project.projectName.toLowerCase().includes(searchKeyword.value.toLowerCase())
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 计算属性 - 我的项目
|
|
|
|
|
const myProjects = computed(() => {
|
|
|
|
|
return filteredProjects.value.filter(project => project.isMyProject)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 分页相关计算属性
|
|
|
|
|
const totalItems = computed(() => filteredProjects.value.length)
|
|
|
|
|
const myTotalItems = computed(() => myProjects.value.length)
|
|
|
|
|
|
|
|
|
|
const pagedData = computed(() => {
|
|
|
|
|
const start = (currentPage.value - 1) * pageSize.value
|
|
|
|
|
const end = start + pageSize.value
|
|
|
|
|
return filteredProjects.value.slice(start, end)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const myPagedData = computed(() => {
|
|
|
|
|
const start = (myCurrentPage.value - 1) * pageSize.value
|
|
|
|
|
const end = start + pageSize.value
|
|
|
|
|
return myProjects.value.slice(start, end)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 搜索处理
|
|
|
|
|
const handleSearch = (value: string) => {
|
|
|
|
|
searchKeyword.value = value
|
|
|
|
|
currentPage.value = 1 // 搜索时重置到第一页
|
|
|
|
|
myCurrentPage.value = 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleClear = () => {
|
|
|
|
|
searchKeyword.value = ''
|
|
|
|
|
currentPage.value = 1 // 清空搜索时重置到第一页
|
|
|
|
|
myCurrentPage.value = 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 选项卡切换
|
|
|
|
|
const handleTabChange = (key: string) => {
|
|
|
|
|
activeTab.value = key as TabKey
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 分页变化处理
|
|
|
|
|
const handlePageChange = (page: number) => {
|
|
|
|
|
currentPage.value = page
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleMyPageChange = (page: number) => {
|
|
|
|
|
myCurrentPage.value = page
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 刷新数据
|
|
|
|
|
const refreshData = async () => {
|
|
|
|
|
loading.value = true
|
|
|
|
|
try {
|
|
|
|
|
// 模拟API请求
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 800))
|
|
|
|
|
Message.success('数据已刷新')
|
|
|
|
|
} catch (error) {
|
|
|
|
|
Message.error('刷新失败')
|
|
|
|
|
} finally {
|
|
|
|
|
loading.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 开始爬虫
|
|
|
|
|
const startCrawler = () => {
|
|
|
|
|
Message.info('开始爬虫任务')
|
|
|
|
|
// 这里添加爬虫启动逻辑
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 导出数据
|
|
|
|
|
const exportData = () => {
|
|
|
|
|
Message.info('导出数据')
|
|
|
|
|
// 这里添加数据导出逻辑
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 打开爬虫设置
|
|
|
|
|
const openCrawlerSettings = () => {
|
|
|
|
|
crawlerSettingsVisible.value = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleUploadDocument = async (file: File) => {
|
|
|
|
|
uploading.value = true
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const formData = new FormData()
|
|
|
|
|
formData.append('file', file)
|
|
|
|
|
formData.append('projectId', currentDetail.value.id.toString())
|
|
|
|
|
|
|
|
|
|
// 替换为实际API调用
|
|
|
|
|
const response = await uploadBiddingDocument(formData)
|
|
|
|
|
|
|
|
|
|
Message.success('文件上传成功')
|
|
|
|
|
currentDetail.value.biddingDocuments = response.data.fileUrl
|
|
|
|
|
} catch (error) {
|
|
|
|
|
Message.error(`文件上传失败: ${error.message}`)
|
|
|
|
|
} finally {
|
|
|
|
|
uploading.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 模拟上传API
|
|
|
|
|
const uploadBiddingDocument = (formData: FormData) => {
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
const file = formData.get('file') as File
|
|
|
|
|
const projectId = formData.get('projectId')
|
|
|
|
|
const extension = file.name.split('.').pop()
|
|
|
|
|
resolve({
|
|
|
|
|
data: {
|
|
|
|
|
fileUrl: `https://your-api-domain.com/uploads/${projectId}.${extension}`
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}, 1500)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
// 初始化加载数据
|
|
|
|
|
})
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped lang="less">
|
|
|
|
|
.project-list-container {
|
|
|
|
|
height: 100%;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.header-section {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
padding: 0 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.search-area {
|
|
|
|
|
flex: 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.action-area {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tabs-section {
|
|
|
|
|
flex: 1;
|
|
|
|
|
min-height: 0;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.pagination-wrapper {
|
|
|
|
|
margin-top: 16px;
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.arco-tabs-content) {
|
|
|
|
|
flex: 1;
|
|
|
|
|
min-height: 0;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.arco-tabs-pane) {
|
|
|
|
|
flex: 1;
|
|
|
|
|
min-height: 0;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.arco-tabs-nav-tab) {
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.arco-tabs-nav-tab-list) {
|
|
|
|
|
padding: 0 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 响应式处理
|
|
|
|
|
@media (max-width: 768px) {
|
|
|
|
|
.header-section {
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 16px;
|
|
|
|
|
align-items: stretch;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.search-area {
|
|
|
|
|
flex: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.action-area {
|
|
|
|
|
justify-content: center;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|