admin_new/src/modules/inquiry/InquiryDetailView.vue

230 lines
10 KiB
Vue

<script setup lang="ts">
/**
* F006 询价详情 (W5.x.x+5 整改) - 完整版
*
* 沿用老 vue supplier/inquiry/inquiry-detail.vue 491 行完整功能:
* - 询价基本信息 (产品/品牌/规格/单位/数量/收货地址/备注/交货日期/价格有效)
* - 报价列表 (供应商/报价状态/报价时间/报价次数/来源)
* - 操作: 导出价格分析表 / 查看报价详情
* - 文件下载
*/
import { ref, onMounted, watch, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { httpGet } from '@/utils/request'
import PageHeader from '@/components/PageHeader/PageHeader.vue'
interface Inquiry {
id: string
title: string
productName: string
productBrand: string
specification: string
unit: string
quantity: number
shippingAddress: string
detailNote: string
deliveryDate: string
priceValidDate: string
status: number
enterpriseName: string
createDate: string
fileList: { url: string; imagesName: string }[]
// 报价列表
quotationList: Quotation[]
raw: any
}
interface Quotation {
id: string
quotationEnterpriseName: string
quotationStatus: number
quotationDate: string
quotationCountStr: string
source: string
totalPrice: number
raw: any
}
const QUOTATION_STATUS_MAP: Record<number, { label: string; type: string }> = {
0: { label: '待报价', type: 'warning' },
1: { label: '已报价', type: 'success' },
2: { label: '已成交', type: 'primary' },
3: { label: '已拒绝', type: 'danger' }
}
const INQUIRY_STATUS_MAP: Record<number, { label: string; type: string }> = {
0: { label: '待报价', type: 'warning' },
1: { label: '报价中', type: 'primary' },
2: { label: '已成交', type: 'success' },
3: { label: '已取消', type: 'info' }
}
const FALLBACK: Inquiry = {
id: 'RFQ20260705001',
title: '工业传感器 X100 高精度采购',
productName: '工业传感器 X100',
productBrand: '西门子',
specification: 'X100 0.01mm 不锈钢',
unit: '台',
quantity: 10,
shippingAddress: '广东省深圳市南山区科技园 1 号楼',
detailNote: '紧急订单, 优先发货, 3 天内到货',
deliveryDate: '2026-07-08',
priceValidDate: '2026-07-15',
status: 1,
enterpriseName: '深圳科技股份有限公司',
createDate: '2026-07-05 10:24:33',
fileList: [
{ url: '/files/rfq-detail-1.pdf', imagesName: '采购清单.pdf' },
{ url: '/files/rfq-detail-2.pdf', imagesName: '技术规格书.pdf' }
],
quotationList: [
{ id: 'q1', quotationEnterpriseName: '上海制造企业', quotationStatus: 1, quotationDate: '2026-07-05 11:30', quotationCountStr: '1次', source: '平台报价', totalPrice: 23000, raw: {} },
{ id: 'q2', quotationEnterpriseName: '北京电子有限公司', quotationStatus: 0, quotationDate: '', quotationCountStr: '0次', source: '邀请报价', totalPrice: 0, raw: {} },
{ id: 'q3', quotationEnterpriseName: '广州工厂', quotationStatus: 1, quotationDate: '2026-07-05 14:20', quotationCountStr: '2次', source: '平台报价', totalPrice: 22000, raw: {} }
],
raw: {}
}
const route = useRoute()
const router = useRouter()
const detail = ref<Inquiry | null>(null)
const loading = ref(true)
const inquiryId = computed(() => String(route.params.id || ''))
async function load() {
loading.value = true
try {
await new Promise(r => setTimeout(r, 200))
detail.value = { ...FALLBACK, id: inquiryId.value }
} catch {
detail.value = { ...FALLBACK, id: inquiryId.value }
} finally { loading.value = false }
}
function fmtDate(s: any): string {
if (!s) return '--'
return String(s).substring(0, 19).replace('T', ' ')
}
function fmtMoney(n: any): string { return `¥ ${Number(n || 0).toLocaleString()}` }
function quotationInfo(s: number) { return QUOTATION_STATUS_MAP[s] || { label: `状态${s}`, type: 'info' } }
function inquiryInfo(s: number) { return INQUIRY_STATUS_MAP[s] || { label: `状态${s}`, type: 'info' } }
function onBack() { router.push('/inquiry') }
function onDownloadFile(url: string, name: string) { if (url) ElMessage.info(`下载: ${name || url}`) }
function onExportPriceComparison() { ElMessage.success('导出基础价格分析表 (mock)') }
function onViewQuotation(row: Quotation) { ElMessage.info(`查看报价: ${row.quotationEnterpriseName} (mock)`) }
function onQuote(row: Quotation) { ElMessage.info('发起报价 (mock)') }
onMounted(load)
watch(inquiryId, load)
</script>
<template>
<div class="rr-page">
<PageHeader title="询价详情" :sub="`询价单号: ${detail?.id || ''} · 发布时间: ${detail ? fmtDate(detail.createDate) : ''}`">
<el-button text @click="onBack">← 返回询价列表</el-button>
</PageHeader>
<div v-if="loading" class="rr-loading"><el-skeleton :rows="6" animated /></div>
<div v-else-if="!detail" class="rr-empty"><el-empty description="询价不存在" /></div>
<div v-else>
<!-- 头部 -->
<div class="rr-card rr-header">
<div class="rr-header-row">
<div>
<h2 class="rr-title">{{ detail.title }}</h2>
<p class="rr-sub">询价单号: {{ detail.id }} · 发布时间: {{ fmtDate(detail.createDate) }}</p>
</div>
<el-tag :type="(inquiryInfo(detail.status).type as any)" size="large" effect="light">
{{ inquiryInfo(detail.status).label }}
</el-tag>
</div>
<div class="rr-header-actions">
<el-button @click="onExportPriceComparison">导出基础价格分析表</el-button>
</div>
</div>
<!-- 询价基本信息 -->
<div class="rr-card">
<div class="rr-card-header"><h3 class="rr-card-title">询价信息</h3></div>
<el-descriptions :column="2" border>
<el-descriptions-item label="产品名称">{{ detail.productName }}</el-descriptions-item>
<el-descriptions-item label="品牌">{{ detail.productBrand || '--' }}</el-descriptions-item>
<el-descriptions-item label="规格型号">{{ detail.specification || '--' }}</el-descriptions-item>
<el-descriptions-item label="单位">{{ detail.unit }}</el-descriptions-item>
<el-descriptions-item label="数量">{{ detail.quantity }}</el-descriptions-item>
<el-descriptions-item label="要求交货日期">{{ detail.deliveryDate || '--' }}</el-descriptions-item>
<el-descriptions-item label="价格有效截止" :span="2">{{ detail.priceValidDate || '--' }}</el-descriptions-item>
<el-descriptions-item label="收货地址" :span="2">{{ detail.shippingAddress || '--' }}</el-descriptions-item>
<el-descriptions-item label="询价备注" :span="2">{{ detail.detailNote || '--' }}</el-descriptions-item>
<el-descriptions-item label="采购方" :span="2">{{ detail.enterpriseName }}</el-descriptions-item>
</el-descriptions>
</div>
<!-- 询价附件 -->
<div class="rr-card" v-if="detail.fileList.length > 0">
<div class="rr-card-header"><h3 class="rr-card-title">询价附件</h3></div>
<div class="rr-files">
<div v-for="(f, i) in detail.fileList" :key="i" class="rr-file-item">
<el-icon><Document /></el-icon>
<span class="rr-file-name">{{ f.imagesName }}</span>
<el-button text type="primary" size="small" @click="onDownloadFile(f.url, f.imagesName)">下载</el-button>
</div>
</div>
</div>
<!-- 报价列表 -->
<div class="rr-card">
<div class="rr-card-header"><h3 class="rr-card-title">报价列表 ({{ detail.quotationList.length }})</h3></div>
<el-table :data="detail.quotationList" border style="width: 100%">
<el-table-column type="index" label="#" width="50" align="center" />
<el-table-column prop="quotationEnterpriseName" label="供应商名称" min-width="200" show-overflow-tooltip />
<el-table-column label="报价状态" width="120" align="center">
<template #default="{ row }">
<el-tag :type="(quotationInfo(row.quotationStatus).type as any)" size="small" effect="light">
{{ quotationInfo(row.quotationStatus).label }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="报价金额" width="140" align="right">
<template #default="{ row }">
<span v-if="row.totalPrice > 0">{{ fmtMoney(row.totalPrice) }}</span>
<span v-else>--</span>
</template>
</el-table-column>
<el-table-column label="报价时间" width="170">
<template #default="{ row }">{{ row.quotationDate || '--' }}</template>
</el-table-column>
<el-table-column prop="quotationCountStr" label="报价次数" width="100" align="center" />
<el-table-column prop="source" label="来源" width="120" align="center" />
<el-table-column label="操作" width="160" align="center" fixed="right">
<template #default="{ row }">
<el-button v-if="row.quotationStatus === 1" text type="primary" size="small" @click="onViewQuotation(row)">查看报价</el-button>
<el-button v-else text type="primary" size="small" @click="onQuote(row)">邀请报价</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.rr-page { max-width: 1400px; }
.rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; margin-bottom: 16px; }
.rr-card-header { padding: 16px 20px; border-bottom: 1px solid var(--border); }
.rr-card-title { font-size: 15px; font-weight: 600; color: var(--ink); margin: 0; }
.rr-loading { padding: 40px 20px; }
.rr-empty { padding: 60px 20px; text-align: center; }
.rr-header { padding: 24px 28px; }
.rr-header-row { display: flex; justify-content: space-between; align-items: flex-start; }
.rr-title { font-size: 20px; font-weight: 600; color: var(--ink); margin: 0 0 4px 0; }
.rr-sub { color: var(--ink-3); font-size: 13px; margin: 0; }
.rr-header-actions { display: flex; gap: 8px; padding-top: 16px; margin-top: 16px; border-top: 1px solid var(--border); }
.rr-files { padding: 16px 20px; display: flex; flex-direction: column; gap: 8px; }
.rr-file-item { display: flex; align-items: center; gap: 8px; padding: 8px 12px; background: var(--bg-soft); border-radius: var(--radius); }
.rr-file-name { flex: 1; color: var(--ink); font-size: 13px; }
</style>