a7399e0624
hjc-web 此前**没有收银台**——支付步骤内嵌在购标流程(BuyDocument 的 step='pay')里, 买家一旦离开就再也回不到未完成的支付。本提交补上按订单号付款的独立页面。 - 新建 /tender/pay 收银台:只认订单号,金额取自**订单本身**(by-no)而非支付响应 (支付一失败页面就会停在 0.00);进入先判状态,已支付/已取消/已退款**不发**统一下单, 就地显示结论——既无意义,也会多撞一次跨端 OUT_TRADE_NO_USED - 订单列表:待支付卡片加两个按钮 + 手搓 Tailwind 二次确认弹窗(照 ConsultDialog 的风格, 不用 antd 的 Modal:买家页无先例,视觉与站点自建风格不一致) - 状态口径改读 orderStatus(原先只看 payStatus,取消后会把已取消显示成「待支付」 并带上「去支付」),抽 app/utils/orderStatus.ts,与 hjc-h5 刻意保持同一套优先级 - 新增两个 Nitro 代理 order-by-no.get.ts / cancel.post.ts,照既有 my-orders / pay-status 的范式原样透传 ApiResult - 标书详情「购买人数」改读 buyerCount(后端实时统计的已付款订单数) - 跨端 OUT_TRADE_NO_USED:识别后端回带的 data.wechatCode 并给可行动提示, 不匹配 message 字符串(包装方式一改就会静默失效)
127 lines
4.8 KiB
Vue
127 lines
4.8 KiB
Vue
<template>
|
||
<div class="container mx-auto px-4 py-10">
|
||
<div v-if="tender" class="grid lg:grid-cols-[1fr_320px] gap-8">
|
||
<!-- 左侧:项目信息 -->
|
||
<div>
|
||
<div class="mb-3 flex items-center justify-between">
|
||
<NuxtLink to="/tender" class="text-sm text-blue-600 hover:underline">← 返回标书中心</NuxtLink>
|
||
<HjcUserBar />
|
||
</div>
|
||
<h1 class="mt-3 text-2xl font-bold text-gray-900">{{ tender.projectName }}</h1>
|
||
<div class="mt-2 flex flex-wrap gap-3 text-sm text-gray-500">
|
||
<span>项目编号:{{ tender.projectNo }}</span>
|
||
<span class="rounded bg-gray-100 px-2 py-0.5">{{ tender.category || '未分类' }}</span>
|
||
</div>
|
||
|
||
<div class="mt-6 space-y-3 text-sm">
|
||
<div class="flex gap-3">
|
||
<span class="w-28 shrink-0 text-gray-400">发布时间</span>
|
||
<span>{{ fmt(tender.publishTime) }}</span>
|
||
</div>
|
||
<div class="flex gap-3">
|
||
<span class="w-28 shrink-0 text-gray-400">投标截止</span>
|
||
<span>{{ fmt(tender.deadlineTime) }}</span>
|
||
</div>
|
||
<div class="flex gap-3">
|
||
<span class="w-28 shrink-0 text-gray-400">招标人</span>
|
||
<span>{{ tender.tenderer || '-' }}</span>
|
||
</div>
|
||
<div class="flex gap-3">
|
||
<span class="w-28 shrink-0 text-gray-400">开售时间</span>
|
||
<span>{{ fmt(tender.onsaleTime) }}</span>
|
||
</div>
|
||
<div class="flex gap-3">
|
||
<span class="w-28 shrink-0 text-gray-400">停售时间</span>
|
||
<span>{{ fmt(tender.offsaleTime) }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="tender.bulletinTitle || tender.bulletinContent" class="mt-8">
|
||
<h2 class="mb-3 text-lg font-semibold text-gray-900">中标公告</h2>
|
||
<div class="rounded-md border border-gray-200 p-5 text-sm text-gray-700">
|
||
<h3 class="mb-2 font-semibold">{{ tender.bulletinTitle }}</h3>
|
||
<p class="whitespace-pre-line leading-relaxed">{{ tender.bulletinContent }}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右侧:购买卡片 -->
|
||
<aside class="lg:sticky lg:top-24 h-fit rounded-lg border border-gray-200 p-6">
|
||
<div class="flex items-end gap-1 text-blue-600">
|
||
<span class="text-3xl font-bold">¥{{ formatMoney(tender.tenderPrice) }}</span>
|
||
<span class="text-xs text-gray-400 mb-1">信息服务费</span>
|
||
</div>
|
||
<p class="mt-2 text-xs text-gray-400">购买成功后可在「我的订单」查看并下载标书</p>
|
||
|
||
<div class="mt-5 space-y-2 text-sm text-gray-600">
|
||
<!-- 购买人数取后端实时统计的**已付款订单数**(buyerCount),不是 saleCount:
|
||
后者在下单时就 +quantity 且从不回退,含未付款与已取消的单,一直是假数 -->
|
||
<p>购买人数:{{ tender.buyerCount || 0 }}</p>
|
||
<p>售卖方式:{{ sellingMethodText }}</p>
|
||
</div>
|
||
|
||
<button
|
||
class="mt-6 w-full rounded-md bg-blue-600 py-3 text-white font-medium hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-50"
|
||
:disabled="!buyable"
|
||
@click="goBuy"
|
||
>
|
||
{{ buyable ? '立即购买' : '已截止' }}
|
||
</button>
|
||
<p class="mt-3 text-xs text-gray-400 text-center">购买需通过企业资质审核</p>
|
||
</aside>
|
||
</div>
|
||
|
||
<div v-else-if="notFound" class="py-20 text-center text-gray-500">标书项目不存在或已下架</div>
|
||
<SiteLoading v-else />
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import type { Tender } from '~/types/tender'
|
||
|
||
const route = useRoute()
|
||
const id = route.params.id as string
|
||
const tender = shallowRef<Tender | null>(null)
|
||
const notFound = ref(false)
|
||
|
||
const buyable = computed(() => {
|
||
if (!tender.value) return false
|
||
if (tender.value.status !== undefined && tender.value.status !== 1) return false
|
||
if (tender.value.needSell !== undefined && tender.value.needSell === 0) return false
|
||
if (tender.value.offsaleTime) {
|
||
const t = new Date(tender.value.offsaleTime.replace('T', ' ')).getTime()
|
||
if (!Number.isNaN(t) && t < Date.now()) return false
|
||
}
|
||
return true
|
||
})
|
||
|
||
const sellingMethodText = computed(() => {
|
||
const m = tender.value?.sellingMethod
|
||
if (m === 2) return '公众号'
|
||
if (m === 3) return '交易中心'
|
||
if (m === 4) return '政采云'
|
||
return '公司财务'
|
||
})
|
||
|
||
function fmt(s?: string) {
|
||
return s ? s.slice(0, 19).replace('T', ' ') : '-'
|
||
}
|
||
|
||
function formatMoney(n?: number | string) {
|
||
return Number(n ?? 0).toFixed(2)
|
||
}
|
||
|
||
function goBuy() {
|
||
if (!tender.value) return
|
||
return navigateTo(`/buy?id=${tender.value.id}`)
|
||
}
|
||
|
||
try {
|
||
const res: any = await $fetch(`/api/tender/detail?id=${id}`)
|
||
tender.value = res && res.id ? res : null
|
||
if (!res) notFound.value = true
|
||
} catch {
|
||
notFound.value = true
|
||
}
|
||
</script>
|