Files
hjc-web/app/templates/template-07/pages/TenderDetail.vue
T
weicw1996 a7399e0624 feat(tender): 我的订单加「去支付」「取消订单」入口,并新建收银台
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 字符串(包装方式一改就会静默失效)
2026-09-17 03:39:59 +08:00

127 lines
4.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>