招标解析

This commit is contained in:
cwchen 2025-11-27 11:21:04 +08:00
parent 16c61cdd1b
commit ed078f2dc9
4 changed files with 1036 additions and 30 deletions

View File

@ -1,64 +1,401 @@
<!-- 标段解析 -->
<template>
<el-card class="analysis-container">
<div class="back-container">
<el-button type="default" size="small" @click="handleBack" class="back-btn">
返回
</el-button>
<div class="analysis-bid-view">
<!-- 头部导航 -->
<AnalysisHeader @back="handleBack" />
<!-- 主内容区域 -->
<div class="main-content">
<!-- 左侧解析结果面板 -->
<div class="left-panel">
<AnalysisResultPanel ref="resultPanel" :main-tabs="mainTabs" :default-main-tab="defaultMainTab"
:default-sub-tab="defaultSubTab" @main-tab-change="handleMainTabChange"
@sub-tab-change="handleSubTabChange">
<!-- 项目信息 - 招标人 -->
<template #project-tenderer>
<div class="content-section">
<div class="section-title">招标人信息</div>
<div class="section-content">{{ analysisData.tenderer || '--' }}</div>
</div>
</template>
<!-- 项目信息 - 基础信息 -->
<template #project-basic>
<div class="content-section">
<div class="section-title">基础信息</div>
<div class="section-content">{{ analysisData.basicInfo || '--' }}</div>
</div>
</template>
<!-- 项目信息 - 代理机构 -->
<template #project-agency>
<div class="content-section">
<div class="section-title">代理机构</div>
<div class="section-content">{{ analysisData.agency || '--' }}</div>
</div>
</template>
<!-- 项目信息 - 关键时间 -->
<template #project-keyTime>
<div class="content-section">
<div class="section-title">关键时间</div>
<div class="section-content">{{ analysisData.keyTime || '--' }}</div>
</div>
</template>
<!-- 项目信息 - 联合体要求 -->
<template #project-consortium>
<div class="content-section">
<div class="section-title">联合体要求</div>
<div class="section-content">{{ analysisData.consortium || '--' }}</div>
</div>
</template>
<!-- 项目信息 - 分包要求 -->
<template #project-subcontract>
<div class="content-section">
<div class="section-title">分包要求</div>
<div class="section-content">{{ analysisData.subcontract || '--' }}</div>
</div>
</template>
<!-- 项目信息 - 最高限价 -->
<template #project-maxPrice>
<div class="content-section">
<div class="section-title">最高限价</div>
<div class="section-content">{{ analysisData.maxPrice || '--' }}</div>
</div>
</template>
<!-- 项目信息 - 响应和偏差 -->
<template #project-response>
<div class="content-section">
<div class="section-title">响应和偏差要求</div>
<div class="section-content">{{ analysisData.response || '招标文件无此内容' }}</div>
</div>
<div class="content-section">
<div class="section-title">澄清要求</div>
<div class="section-content">{{ analysisData.clarification || '--' }}</div>
</div>
</template>
<!-- 标段信息 -->
<template #bid>
<div class="content-section">
<div class="section-title">标段信息</div>
<div class="section-content">{{ analysisData.bidInfo || '--' }}</div>
</div>
</template>
<!-- 保证金 - 投标保证金 -->
<template #deposit-bidDeposit>
<div class="content-section">
<div class="section-title">投标保证金金额</div>
<div class="section-content">{{ analysisData.bidDepositAmount || '--' }}</div>
</div>
<div class="content-section">
<div class="section-title">投标保证金形式</div>
<div class="section-content">{{ analysisData.bidDepositForm || '--' }}</div>
</div>
<div class="content-section">
<div class="section-title">投标保证金提交期限</div>
<div class="section-content">{{ analysisData.bidDepositSubmitDeadline || '--' }}</div>
</div>
<div class="content-section">
<div class="section-title">投标保证金退还期限</div>
<div class="section-content">{{ analysisData.bidDepositRefundDeadline || '--' }}</div>
</div>
</template>
<!-- 保证金 - 履约保证金 -->
<template #deposit-performanceDeposit>
<div class="content-section">
<div class="section-title">履约保证金</div>
<div class="section-content">{{ analysisData.performanceDeposit || '--' }}</div>
</div>
</template>
<!-- 其他标签页内容 -->
<template #qualification>
<div class="content-section">
<div class="section-title">资格要求</div>
<div class="section-content">{{ analysisData.qualification || '--' }}</div>
</div>
</template>
<template #performance>
<div class="content-section">
<div class="section-title">业绩要求</div>
<div class="section-content">{{ analysisData.performance || '--' }}</div>
</div>
</template>
<template #finance>
<div class="content-section">
<div class="section-title">财务要求</div>
<div class="section-content">{{ analysisData.finance || '--' }}</div>
</div>
</template>
<template #personnel>
<div class="content-section">
<div class="section-title">人员要求</div>
<div class="section-content">{{ analysisData.personnel || '--' }}</div>
</div>
</template>
<template #evaluation>
<div class="content-section">
<div class="section-title">开评定标要求</div>
<div class="section-content">{{ analysisData.evaluation || '--' }}</div>
</div>
</template>
<template #rejection>
<div class="content-section">
<div class="section-title">废标项</div>
<div class="section-content">{{ analysisData.rejection || '--' }}</div>
</div>
</template>
<template #document>
<div class="content-section">
<div class="section-title">投标文件要求</div>
<div class="section-content">{{ analysisData.document || '--' }}</div>
</div>
</template>
</AnalysisResultPanel>
</div>
<!-- 右侧文档预览面板 -->
<div class="right-panel">
<DocumentPreviewPanel :tender-document-url="tenderDocumentUrl"
:tender-document-title="tenderDocumentTitle" :tender-document-key="tenderDocumentKey"
:bid-document-url="bidDocumentUrl" :bid-document-title="bidDocumentTitle"
:bid-document-key="bidDocumentKey" @document-switch="handleDocumentSwitch" @search="handleSearch" />
</div>
</div>
</el-card>
</div>
</template>
<script>
import { decryptWithSM4, encryptWithSM4 } from '@/utils/sm'
import AnalysisHeader from './child/AnalysisHeader.vue'
import AnalysisResultPanel from './child/AnalysisResultPanel.vue'
import DocumentPreviewPanel from './child/DocumentPreviewPanel.vue'
// import { getBidDetailAPI } from '@/api/analysis/analysis'
export default {
name: 'AnalysisBidView',
components: {
AnalysisHeader,
AnalysisResultPanel,
DocumentPreviewPanel
},
data() {
return {
proId: decryptWithSM4(this.$route.query.proId),
bidId: decryptWithSM4(this.$route.query.bidId),
defaultMainTab: 'project',
defaultSubTab: 'response',
analysisData: {},
tenderDocumentUrl: '',
tenderDocumentTitle: '',
tenderDocumentKey: '',
bidDocumentUrl: '',
bidDocumentTitle: '',
bidDocumentKey: '',
mainTabs: [
{
name: 'project',
label: '项目信息',
subTabs: [
{ name: 'tenderer', label: '招标人' },
{ name: 'basic', label: '基础信息' },
{ name: 'agency', label: '代理机构' },
{ name: 'keyTime', label: '关键时间' },
{ name: 'consortium', label: '联合体要求' },
{ name: 'subcontract', label: '分包要求' },
{ name: 'maxPrice', label: '最高限价' },
{ name: 'response', label: '响应和偏差' },
]
},
{
name: 'bid',
label: '标段信息',
subTabs: []
},
{
name: 'deposit',
label: '保证金',
subTabs: [
{ name: 'bidDeposit', label: '投标保证金' },
{ name: 'performanceDeposit', label: '履约保证金' },
]
},
{
name: 'qualification',
label: '资格要求',
subTabs: []
},
{
name: 'performance',
label: '业绩要求',
subTabs: []
},
{
name: 'finance',
label: '财务要求',
subTabs: []
},
{
name: 'personnel',
label: '人员要求',
subTabs: []
},
{
name: 'evaluation',
label: '开评定标要求',
subTabs: []
},
{
name: 'rejection',
label: '废标项',
subTabs: []
},
{
name: 'document',
label: '投标文件要求',
subTabs: []
},
]
}
},
created() {
this.getBidDetail()
},
methods: {
handleBack() {
const obj = { name: "AnalysisBidIndex", query: { proId: encryptWithSM4(this.proId) } }
this.$tab.closeOpenPage(obj)
},
//
async getBidDetail() {
try {
// TODO: API
// const res = await getBidDetailAPI({ proId: this.proId, bidId: this.bidId })
// if (res.code === 200) {
// this.analysisData = res.data || {}
// this.initDocumentData(res.data)
// }
//
this.analysisData = {
tenderer: '中国南方电网有限责任公司',
basic: '基础信息内容',
agency: '代理机构信息',
keyTime: '关键时间信息',
consortium: '联合体要求内容',
subcontract: '分包要求内容',
maxPrice: '最高限价信息',
response: '招标文件无此内容',
clarification: '澄清要求内容...',
bidInfo: '标段信息内容',
bidDepositAmount: '投标保证金金额',
bidDepositForm: '投标保证金形式',
bidDepositSubmitDeadline: '投标截止日前一天',
bidDepositRefundDeadline: '退还期限信息',
performanceDeposit: '履约保证金信息',
qualification: '资格要求内容',
performance: '业绩要求内容',
finance: '财务要求内容',
personnel: '人员要求内容',
evaluation: '开评定标要求内容',
rejection: '废标项内容',
document: '投标文件要求内容'
}
} catch (error) {
console.error('获取标段详情失败:', error)
}
},
//
initDocumentData(data) {
if (data) {
this.tenderDocumentUrl = data.tenderDocumentUrl || ''
this.tenderDocumentTitle = data.tenderDocumentTitle || ''
this.tenderDocumentKey = data.tenderDocumentKey || ''
this.bidDocumentUrl = data.bidDocumentUrl || ''
this.bidDocumentTitle = data.bidDocumentTitle || ''
this.bidDocumentKey = data.bidDocumentKey || ''
}
},
handleMainTabChange(mainTab, subTab) {
console.log('主标签切换:', mainTab, subTab)
},
handleSubTabChange(mainTab, subTab) {
console.log('子标签切换:', mainTab, subTab)
},
handleDocumentSwitch(type) {
console.log('文档切换:', type)
},
handleSearch() {
console.log('搜索')
}
},
}
</script>
<style scoped lang="scss">
.analysis-container {
.analysis-bid-view {
height: calc(100vh - 84px);
overflow: hidden;
background: linear-gradient(180deg, #F1F6FF 20%, #E5EFFF 100%);
}
.back-container {
display: flex;
justify-content: flex-end;
align-items: center;
margin-bottom: 20px;
padding: 0 20px;
flex-direction: column;
background: linear-gradient(180deg, #F1F6FF 20%, #E5EFFF 100%);
overflow: hidden;
.back-btn {
width: 98px;
height: 36px;
background: #FFFFFF;
box-shadow: 0px 4px 8px 0px rgba(76, 76, 76, 0.2);
border-radius: 4px;
border: none;
color: #666;
font-size: 14px;
transition: all 0.3s ease;
.main-content {
flex: 1;
display: flex;
gap: 20px;
padding: 0 20px 20px 20px;
overflow: hidden;
&:hover {
background: #f5f5f5;
color: #409EFF;
box-shadow: 0px 6px 12px 0px rgba(76, 76, 76, 0.3);
.left-panel {
flex: 0 0 50%;
min-width: 0;
height: 100%;
}
.right-panel {
flex: 0 0 50%;
min-width: 0;
height: 100%;
}
}
}
.content-section {
margin-bottom: 24px;
&:last-child {
margin-bottom: 0;
}
.section-title {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
}
.section-content {
font-size: 14px;
color: #606266;
line-height: 1.8;
white-space: pre-wrap;
word-break: break-word;
background: #F5F7FA;
padding: 16px;
border-radius: 4px;
}
}
</style>

View File

@ -0,0 +1,67 @@
<!-- 分析页面头部 -->
<template>
<div class="analysis-header">
<div class="header-right">
<el-button
class="back-btn"
@click="handleBack"
>
返回
</el-button>
</div>
</div>
</template>
<script>
export default {
name: 'AnalysisHeader',
methods: {
handleBack() {
this.$emit('back')
}
}
}
</script>
<style scoped lang="scss">
.analysis-header {
display: flex;
justify-content: flex-end;
align-items: center;
padding: 12px 20px;
background: transparent;
border-bottom: none;
.header-right {
display: flex;
align-items: center;
gap: 12px;
.back-btn {
width: 98px;
height: 36px;
background: #FFFFFF;
box-shadow: 0px 4px 8px 0px rgba(76, 76, 76, 0.2);
border-radius: 4px;
border: none;
color: #606266;
font-size: 14px;
font-weight: 600;
transition: all 0.3s ease;
cursor: pointer;
&:hover {
background: #f5f7fa;
color: #409EFF;
box-shadow: 0px 6px 12px 0px rgba(76, 76, 76, 0.3);
}
&:active {
transform: translateY(1px);
box-shadow: 0px 2px 4px 0px rgba(76, 76, 76, 0.2);
}
}
}
}
</style>

View File

@ -0,0 +1,313 @@
<!-- 解析结果面板 -->
<template>
<div class="analysis-result-panel">
<div class="panel-header">
<h3>解析结果</h3>
</div>
<div class="panel-content">
<!-- 主标签页 -->
<el-tabs v-model="activeMainTab" @tab-click="handleMainTabClick" class="main-tabs">
<el-tab-pane
v-for="tab in mainTabs"
:key="tab.name"
:label="tab.label"
:name="tab.name"
>
<!-- 子标签页 -->
<el-tabs
v-if="tab.subTabs && tab.subTabs.length > 0"
v-model="activeSubTab"
@tab-click="handleSubTabClick"
class="sub-tabs"
>
<el-tab-pane
v-for="subTab in tab.subTabs"
:key="subTab.name"
:label="subTab.label"
:name="subTab.name"
>
<div class="tab-content">
<slot :name="`${tab.name}-${subTab.name}`">
<div class="empty-content">{{ subTab.label }}内容</div>
</slot>
</div>
</el-tab-pane>
</el-tabs>
<!-- 没有子标签页的内容 -->
<div v-else class="tab-content">
<slot :name="tab.name">
<div class="empty-content">{{ tab.label }}内容</div>
</slot>
</div>
</el-tab-pane>
</el-tabs>
</div>
</div>
</template>
<script>
export default {
name: 'AnalysisResultPanel',
props: {
//
mainTabs: {
type: Array,
default: () => [
{
name: 'project',
label: '项目信息',
subTabs: [
{ name: 'tenderer', label: '招标人' },
{ name: 'basic', label: '基础信息' },
{ name: 'agency', label: '代理机构' },
{ name: 'keyTime', label: '关键时间' },
{ name: 'consortium', label: '联合体要求' },
{ name: 'subcontract', label: '分包要求' },
{ name: 'maxPrice', label: '最高限价' },
{ name: 'response', label: '响应和偏差' },
]
},
{
name: 'bid',
label: '标段信息',
subTabs: []
},
{
name: 'deposit',
label: '保证金',
subTabs: [
{ name: 'bidDeposit', label: '投标保证金' },
{ name: 'performanceDeposit', label: '履约保证金' },
]
},
{
name: 'qualification',
label: '资格要求',
subTabs: []
},
{
name: 'performance',
label: '业绩要求',
subTabs: []
},
{
name: 'finance',
label: '财务要求',
subTabs: []
},
{
name: 'personnel',
label: '人员要求',
subTabs: []
},
{
name: 'evaluation',
label: '开评定标要求',
subTabs: []
},
{
name: 'rejection',
label: '废标项',
subTabs: []
},
{
name: 'document',
label: '投标文件要求',
subTabs: []
},
]
},
//
defaultMainTab: {
type: String,
default: 'project'
},
//
defaultSubTab: {
type: String,
default: ''
}
},
data() {
return {
activeMainTab: this.defaultMainTab,
activeSubTab: this.defaultSubTab || this.getDefaultSubTab(this.defaultMainTab),
}
},
watch: {
defaultMainTab(newVal) {
this.activeMainTab = newVal
this.activeSubTab = this.getDefaultSubTab(newVal)
}
},
methods: {
handleMainTabClick(tab) {
//
const currentMainTab = this.mainTabs.find(t => t.name === tab.name)
if (currentMainTab && currentMainTab.subTabs && currentMainTab.subTabs.length > 0) {
this.activeSubTab = currentMainTab.subTabs[0].name
} else {
this.activeSubTab = ''
}
this.$emit('main-tab-change', tab.name, this.activeSubTab)
},
handleSubTabClick(tab) {
this.$emit('sub-tab-change', this.activeMainTab, tab.name)
},
getDefaultSubTab(mainTabName) {
const mainTab = this.mainTabs.find(t => t.name === mainTabName)
if (mainTab && mainTab.subTabs && mainTab.subTabs.length > 0) {
return mainTab.subTabs[0].name
}
return ''
}
}
}
</script>
<style scoped lang="scss">
.analysis-result-panel {
height: 100%;
display: flex;
flex-direction: column;
background: #fff;
border-radius: 8px;
overflow: hidden;
.panel-header {
padding: 16px 20px;
border-bottom: 1px solid #EBEEF5;
background: #fff;
h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #303133;
}
}
.panel-content {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
::v-deep .main-tabs {
height: 100%;
display: flex;
flex-direction: column;
.el-tabs__header {
margin: 0;
padding: 0 20px;
border-bottom: 1px solid #EBEEF5;
background: #FAFAFA;
}
.el-tabs__nav-wrap {
&::after {
display: none;
}
}
.el-tabs__item {
height: 48px;
line-height: 48px;
padding: 0 20px;
font-size: 14px;
color: #606266;
border-bottom: 2px solid transparent;
&.is-active {
color: #409EFF;
border-bottom-color: #409EFF;
font-weight: 500;
}
&:hover {
color: #409EFF;
}
}
.el-tabs__content {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
padding: 0;
}
.el-tab-pane {
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
}
}
::v-deep .sub-tabs {
height: 100%;
display: flex;
flex-direction: column;
.el-tabs__header {
margin: 0;
padding: 0 20px;
border-bottom: 1px solid #EBEEF5;
background: #F5F7FA;
}
.el-tabs__nav-wrap {
&::after {
display: none;
}
}
.el-tabs__item {
height: 40px;
line-height: 40px;
padding: 0 16px;
font-size: 13px;
color: #606266;
border-bottom: 2px solid transparent;
&.is-active {
color: #409EFF;
border-bottom-color: #409EFF;
font-weight: 500;
}
&:hover {
color: #409EFF;
}
}
.el-tabs__content {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
padding: 20px;
}
.el-tab-pane {
height: 100%;
}
}
.tab-content {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
padding: 20px;
.empty-content {
text-align: center;
color: #909399;
padding: 40px 0;
}
}
}
}
</style>

View File

@ -0,0 +1,289 @@
<!-- 文档预览面板 -->
<template>
<div class="document-preview-panel">
<div class="panel-header">
<div class="header-actions">
<el-button
type="text"
icon="el-icon-search"
class="search-btn"
@click="handleSearch"
></el-button>
</div>
<div class="document-switch">
<el-button
:type="activeDocType === 'tender' ? 'primary' : 'default'"
class="doc-btn"
@click="switchDocument('tender')"
>
招标文件
</el-button>
<el-button
:type="activeDocType === 'bid' ? 'primary' : 'default'"
class="doc-btn"
@click="switchDocument('bid')"
>
标段文件
</el-button>
</div>
</div>
<div class="panel-content">
<div v-if="documentUrl" class="document-viewer">
<OnlyOfficeViewer
v-if="showOnlyOffice"
:document-url="documentUrl"
:document-title="documentTitle"
:document-key="documentKey"
:mode="viewMode"
:type="viewType"
@document-ready="handleDocumentReady"
@app-ready="handleAppReady"
@error="handleError"
/>
<div v-else class="document-placeholder">
<div class="placeholder-content">
<i class="el-icon-document placeholder-icon"></i>
<p class="placeholder-text">文档预览</p>
<p class="placeholder-desc">{{ documentTitle || '暂无文档' }}</p>
</div>
</div>
</div>
<div v-else class="empty-state">
<i class="el-icon-document-empty empty-icon"></i>
<p class="empty-text">暂无文档</p>
</div>
</div>
</div>
</template>
<script>
import OnlyOfficeViewer from '@/views/common/OnlyOfficeViewer.vue'
export default {
name: 'DocumentPreviewPanel',
components: {
OnlyOfficeViewer
},
props: {
// URL
tenderDocumentUrl: {
type: String,
default: ''
},
//
tenderDocumentTitle: {
type: String,
default: ''
},
// Key
tenderDocumentKey: {
type: String,
default: ''
},
// URL
bidDocumentUrl: {
type: String,
default: ''
},
//
bidDocumentTitle: {
type: String,
default: ''
},
// Key
bidDocumentKey: {
type: String,
default: ''
},
//
viewMode: {
type: String,
default: 'view'
},
//
viewType: {
type: String,
default: 'desktop'
}
},
data() {
return {
activeDocType: 'tender', // 'tender' 'bid'
showOnlyOffice: false
}
},
computed: {
documentUrl() {
return this.activeDocType === 'tender' ? this.tenderDocumentUrl : this.bidDocumentUrl
},
documentTitle() {
return this.activeDocType === 'tender' ? this.tenderDocumentTitle : this.bidDocumentTitle
},
documentKey() {
return this.activeDocType === 'tender' ? this.tenderDocumentKey : this.bidDocumentKey
}
},
watch: {
documentUrl(newVal) {
if (newVal) {
this.showOnlyOffice = true
}
}
},
methods: {
switchDocument(type) {
this.activeDocType = type
this.showOnlyOffice = false
this.$nextTick(() => {
if (this.documentUrl) {
this.showOnlyOffice = true
}
})
this.$emit('document-switch', type)
},
handleSearch() {
this.$emit('search')
},
handleDocumentReady() {
this.$emit('document-ready')
},
handleAppReady() {
this.$emit('app-ready')
},
handleError(error) {
this.$emit('error', error)
}
}
}
</script>
<style scoped lang="scss">
.document-preview-panel {
height: 100%;
display: flex;
flex-direction: column;
background: #fff;
border-radius: 8px;
overflow: hidden;
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 20px;
border-bottom: 1px solid #EBEEF5;
background: #fff;
.header-actions {
.search-btn {
padding: 8px;
font-size: 18px;
color: #606266;
&:hover {
color: #409EFF;
}
}
}
.document-switch {
display: flex;
gap: 8px;
.doc-btn {
padding: 8px 16px;
font-size: 14px;
border-radius: 4px;
transition: all 0.3s;
&.el-button--primary {
background: #409EFF;
border-color: #409EFF;
}
&.el-button--default {
background: #F5F7FA;
border-color: #DCDFE6;
color: #606266;
&:hover {
background: #ECF5FF;
border-color: #B3D8FF;
color: #409EFF;
}
}
}
}
}
.panel-content {
flex: 1;
overflow: hidden;
position: relative;
.document-viewer {
width: 100%;
height: 100%;
::v-deep .onlyoffice-container {
width: 100%;
height: 100%;
}
}
.document-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: #FAFAFA;
.placeholder-content {
text-align: center;
.placeholder-icon {
font-size: 64px;
color: #C0C4CC;
margin-bottom: 16px;
}
.placeholder-text {
font-size: 16px;
color: #606266;
margin: 0 0 8px 0;
}
.placeholder-desc {
font-size: 14px;
color: #909399;
margin: 0;
}
}
}
.empty-state {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #FAFAFA;
.empty-icon {
font-size: 64px;
color: #C0C4CC;
margin-bottom: 16px;
}
.empty-text {
font-size: 14px;
color: #909399;
margin: 0;
}
}
}
}
</style>