feat(hjc-web): 新建订单详情页,订单列表与详情支持下载已支付订单的标书
此前 PC 没有订单详情页,标书下载只有两句文案(`.scratch/hjc-web-order-detail/01`)。 - 新增 `app/pages/tender/orders/[id].vue`:订单状态、标书附件、待支付操作区、 标书/支付/购买人信息;取数走 `useRequestFetch()`(裸 `$fetch` 在 SSR 不转发 Cookie, 会把用户登出) - `app/pages/tender/orders.vue` → `orders/index.vue`:Nuxt 里同名 file+目录会生成父子路由, 详情页会被父页吞掉,必须让列表页是 `index.vue` - 列表页每张卡片加「标书附件」(仅已支付)与「查看详情」,不整卡可点(卡内已有按钮与链接) - 新增 `app/components/HjcTenderFiles.vue` 与代理 `server/api/tender/order-detail.get.ts` - 附件地址来自后端按 `payStatus=1` 下发的 `tenderFiles`,前端不做权限判断、不自己拼地址 规格与验收:`.scratch/hjc-tender-download/`
This commit is contained in:
@@ -0,0 +1,77 @@
|
|||||||
|
<template>
|
||||||
|
<ul v-if="files.length" class="space-y-2">
|
||||||
|
<li
|
||||||
|
v-for="(file, idx) in files"
|
||||||
|
:key="file.url + idx"
|
||||||
|
class="flex items-center gap-3 rounded-lg border border-gray-200 px-4 py-2.5"
|
||||||
|
>
|
||||||
|
<!-- 用内联 SVG,避免为一个小图标引入图标库依赖(同 PageAttachments.vue) -->
|
||||||
|
<span class="shrink-0 text-blue-600" aria-hidden="true">
|
||||||
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.8"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
||||||
|
<path d="M14 2v6h6" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<a
|
||||||
|
:href="file.url"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
class="min-w-0 flex-1 truncate text-sm text-blue-600 hover:underline"
|
||||||
|
:title="nameOf(file)"
|
||||||
|
>
|
||||||
|
{{ nameOf(file) }}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
附件是第三方 OSS 直链:`download` 属性跨域不生效,直接新标签打开
|
||||||
|
(PDF/图片由浏览器预览,其余类型浏览器自己下载)。
|
||||||
|
-->
|
||||||
|
<a
|
||||||
|
:href="file.url"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
class="shrink-0 text-sm text-blue-600 hover:underline"
|
||||||
|
>
|
||||||
|
下载
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { HjcTenderFile } from '~/types/tender'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标书附件列表(订单列表页与订单详情页共用)。
|
||||||
|
*
|
||||||
|
* 只渲染后端下发的 `tenderFiles`:地址本身是付费内容,未支付订单后端不下发,
|
||||||
|
* 所以这里**不做任何权限判断**,也不从项目字段里自己拼地址。
|
||||||
|
*/
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
files?: HjcTenderFile[] | null
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
files: () => []
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
/** 过滤掉没有地址的脏数据(后端已保证,前端再兜一层,避免渲染出死链) */
|
||||||
|
const files = computed<HjcTenderFile[]>(() =>
|
||||||
|
(props.files || []).filter((f) => !!f && typeof f.url === 'string' && f.url.length > 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
function nameOf(file: HjcTenderFile) {
|
||||||
|
return file.name || '标书附件'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -72,7 +72,7 @@ export function useHjcAuth() {
|
|||||||
// 因此凡是**服务端渲染期间**发出的、需要登录态的站内请求,必须用
|
// 因此凡是**服务端渲染期间**发出的、需要登录态的站内请求,必须用
|
||||||
// `useRequestFetch()` 而不是裸 `$fetch`——裸 $fetch 不会把浏览器 Cookie 转发给
|
// `useRequestFetch()` 而不是裸 `$fetch`——裸 $fetch 不会把浏览器 Cookie 转发给
|
||||||
// 站内 /api 代理,代理会变成匿名调用并拿到 code=401,从而被这里误判为
|
// 站内 /api 代理,代理会变成匿名调用并拿到 code=401,从而被这里误判为
|
||||||
// 「凭据失效」:刷新页面 = 把用户登出。参见 app/pages/tender/orders.vue。
|
// 「凭据失效」:刷新页面 = 把用户登出。参见 app/pages/tender/orders/index.vue。
|
||||||
clearCredentials()
|
clearCredentials()
|
||||||
await navigateTo(`/login?redirect=${encodeURIComponent(redirect)}`)
|
await navigateTo(`/login?redirect=${encodeURIComponent(redirect)}`)
|
||||||
return { handled: true }
|
return { handled: true }
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ const { isLoggedIn, handleAuthCode } = useHjcAuth()
|
|||||||
* **不会**把浏览器请求里的 Cookie 转发给站内 `/api` 代理。代理拿不到 `hjc_token`
|
* **不会**把浏览器请求里的 Cookie 转发给站内 `/api` 代理。代理拿不到 `hjc_token`
|
||||||
* 就等于匿名调用,后端按契约返回 `code=401`;`handleAuthCode` 据此判定「凭据失效」
|
* 就等于匿名调用,后端按契约返回 `code=401`;`handleAuthCode` 据此判定「凭据失效」
|
||||||
* → 在服务端清 Token → 结果是**刷新「我的收藏」= 把自己登出**。
|
* → 在服务端清 Token → 结果是**刷新「我的收藏」= 把自己登出**。
|
||||||
* (同一段坑的完整说明见 app/pages/tender/orders.vue:77-88。)
|
* (同一段坑的完整说明见 app/pages/tender/orders/index.vue:101-110。)
|
||||||
*
|
*
|
||||||
* 客户端侧 `useRequestFetch()` 等价于 `globalThis.$fetch`,行为不变。
|
* 客户端侧 `useRequestFetch()` 等价于 `globalThis.$fetch`,行为不变。
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,289 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container mx-auto px-4 py-10">
|
||||||
|
<div class="mb-6 flex items-center justify-between">
|
||||||
|
<h1 class="text-2xl font-bold text-gray-900">订单详情</h1>
|
||||||
|
<HjcUserBar />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!loggedIn" class="py-20 text-center">
|
||||||
|
<p class="text-gray-500">请先登录后查看订单</p>
|
||||||
|
<NuxtLink
|
||||||
|
:to="`/login?redirect=${loginRedirect}`"
|
||||||
|
class="mt-4 inline-block rounded-md bg-blue-600 px-6 py-2 text-white hover:bg-blue-700"
|
||||||
|
>
|
||||||
|
去登录
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="error" class="py-20 text-center">
|
||||||
|
<p class="text-red-500">{{ error }}</p>
|
||||||
|
<NuxtLink
|
||||||
|
to="/tender/orders"
|
||||||
|
class="mt-6 inline-block rounded-md border border-gray-300 px-6 py-2 text-gray-700 hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
返回我的订单
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SiteLoading v-else-if="!loaded" />
|
||||||
|
|
||||||
|
<template v-else-if="order">
|
||||||
|
<p v-if="notice" class="mb-4 rounded-md bg-gray-50 px-4 py-3 text-sm text-gray-700">{{ notice }}</p>
|
||||||
|
|
||||||
|
<!-- 标题与状态 -->
|
||||||
|
<div class="rounded-lg border border-gray-200 p-5">
|
||||||
|
<div class="flex items-start justify-between gap-4">
|
||||||
|
<p class="font-semibold text-gray-900">{{ order.projectName }}</p>
|
||||||
|
<span class="shrink-0 rounded px-2 py-0.5 text-xs" :class="hjcOrderStatusClass(order)">
|
||||||
|
{{ hjcOrderStatusLabel(order) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p class="mt-1 text-sm text-gray-500">订单号:{{ order.orderNo }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
标书附件:**已支付订单**才有(`tenderFiles` 由后端按 payStatus=1 下发)。
|
||||||
|
未支付的单这里给一句说明而不是空区块——用户要知道「下载在什么时候出现」,
|
||||||
|
而不是以为平台没有这个功能。
|
||||||
|
-->
|
||||||
|
<div v-if="isPaid" class="mt-4 rounded-lg border border-gray-200 p-5">
|
||||||
|
<h2 class="mb-3 font-bold text-gray-900">标书附件</h2>
|
||||||
|
<HjcTenderFiles v-if="order.tenderFiles?.length" :files="order.tenderFiles" />
|
||||||
|
<p v-else class="text-sm text-gray-500">该项目暂无标书附件,如有疑问请联系平台客服。</p>
|
||||||
|
</div>
|
||||||
|
<div v-else class="mt-4 rounded-lg border border-gray-200 p-5">
|
||||||
|
<h2 class="mb-2 font-bold text-gray-900">标书附件</h2>
|
||||||
|
<p class="text-sm text-gray-500">支付成功后可在此下载标书附件。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 待支付订单的操作区(与列表页同一判定:hjcCanActOnOrder) -->
|
||||||
|
<div v-if="hjcCanActOnOrder(order)" class="mt-4 flex justify-end gap-3 rounded-lg border border-gray-200 p-5">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-md border border-gray-300 px-4 py-1.5 text-sm text-gray-700 hover:bg-gray-50 disabled:opacity-50"
|
||||||
|
:disabled="cancelling"
|
||||||
|
@click="askCancel"
|
||||||
|
>
|
||||||
|
{{ cancelling ? '取消中...' : '取消订单' }}
|
||||||
|
</button>
|
||||||
|
<NuxtLink
|
||||||
|
:to="`/tender/pay?orderNo=${encodeURIComponent(order.orderNo)}`"
|
||||||
|
class="rounded-md bg-blue-600 px-4 py-1.5 text-sm text-white hover:bg-blue-700"
|
||||||
|
>
|
||||||
|
去支付
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 标书信息 -->
|
||||||
|
<div class="mt-4 rounded-lg border border-gray-200 p-5">
|
||||||
|
<h2 class="mb-3 font-bold text-gray-900">标书信息</h2>
|
||||||
|
<dl class="space-y-2 text-sm">
|
||||||
|
<div class="flex justify-between gap-4">
|
||||||
|
<dt class="text-gray-500">项目编号</dt>
|
||||||
|
<dd class="text-right text-gray-900">{{ order.projectNo || '-' }}</dd>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between gap-4">
|
||||||
|
<dt class="text-gray-500">单价</dt>
|
||||||
|
<dd class="text-right text-gray-900">¥{{ formatMoney(order.unitPrice) }}</dd>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between gap-4">
|
||||||
|
<dt class="text-gray-500">数量</dt>
|
||||||
|
<dd class="text-right text-gray-900">x{{ order.quantity ?? 1 }}</dd>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between gap-4">
|
||||||
|
<dt class="text-gray-500">实付金额</dt>
|
||||||
|
<dd class="text-right font-bold text-blue-600">¥{{ formatMoney(order.totalAmount) }}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 支付信息 -->
|
||||||
|
<div class="mt-4 rounded-lg border border-gray-200 p-5">
|
||||||
|
<h2 class="mb-3 font-bold text-gray-900">支付信息</h2>
|
||||||
|
<dl class="space-y-2 text-sm">
|
||||||
|
<div class="flex justify-between gap-4">
|
||||||
|
<dt class="text-gray-500">订单号</dt>
|
||||||
|
<dd class="break-all text-right text-gray-900">{{ order.orderNo }}</dd>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between gap-4">
|
||||||
|
<dt class="text-gray-500">支付方式</dt>
|
||||||
|
<dd class="text-right text-gray-900">{{ payMethodLabel }}</dd>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between gap-4">
|
||||||
|
<dt class="text-gray-500">支付状态</dt>
|
||||||
|
<dd class="text-right text-gray-900">{{ hjcOrderStatusLabel(order) }}</dd>
|
||||||
|
</div>
|
||||||
|
<div v-if="order.payTime" class="flex justify-between gap-4">
|
||||||
|
<dt class="text-gray-500">支付时间</dt>
|
||||||
|
<dd class="text-right text-gray-900">{{ fmtTime(order.payTime) }}</dd>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between gap-4">
|
||||||
|
<dt class="text-gray-500">下单时间</dt>
|
||||||
|
<dd class="text-right text-gray-900">{{ fmtTime(order.createTime) }}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 购买人信息 -->
|
||||||
|
<div class="mt-4 rounded-lg border border-gray-200 p-5">
|
||||||
|
<h2 class="mb-3 font-bold text-gray-900">购买人信息</h2>
|
||||||
|
<dl class="space-y-2 text-sm">
|
||||||
|
<div class="flex justify-between gap-4">
|
||||||
|
<dt class="text-gray-500">企业</dt>
|
||||||
|
<dd class="text-right text-gray-900">{{ order.enterpriseName || '-' }}</dd>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between gap-4">
|
||||||
|
<dt class="text-gray-500">联系人</dt>
|
||||||
|
<dd class="text-right text-gray-900">{{ order.contactName || '-' }}</dd>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between gap-4">
|
||||||
|
<dt class="text-gray-500">联系电话</dt>
|
||||||
|
<dd class="text-right text-gray-900">{{ order.contactPhone || '-' }}</dd>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between gap-4">
|
||||||
|
<dt class="text-gray-500">联系邮箱</dt>
|
||||||
|
<dd class="break-all text-right text-gray-900">{{ order.contactEmail || '-' }}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<NuxtLink to="/tender/orders" class="mt-6 inline-block text-sm text-blue-600 hover:underline">
|
||||||
|
← 返回我的订单
|
||||||
|
</NuxtLink>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<HjcConfirmDialog
|
||||||
|
:open="cancelDialogOpen"
|
||||||
|
title="确定取消该订单吗?"
|
||||||
|
content="取消后订单不可恢复,如需购买请重新下单。"
|
||||||
|
confirm-text="确定取消"
|
||||||
|
cancel-text="再想想"
|
||||||
|
loading-text="取消中..."
|
||||||
|
:loading="cancelling"
|
||||||
|
@confirm="confirmCancel"
|
||||||
|
@cancel="cancelDialogOpen = false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { HjcOrder } from '~/types/tender'
|
||||||
|
import { hjcCanActOnOrder, hjcOrderStatusClass, hjcOrderStatusLabel } from '~/utils/orderStatus'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订单详情页(PC 端)。
|
||||||
|
*
|
||||||
|
* 此前 PC 没有这个页面:买家从列表只能看状态、点不了任何东西,「已支付后下载标书」在 PC
|
||||||
|
* 完全没有落点(`.scratch/hjc-web-order-detail/issues/01`)。本页的两个职责就是
|
||||||
|
* ①展示订单全貌 ②给**已支付**订单提供标书附件下载。
|
||||||
|
*
|
||||||
|
* 附件地址来自后端 `tenderFiles`(只有 payStatus=1 才下发),本页**不做权限判断**、
|
||||||
|
* 也不从项目字段拼地址——门槛在服务端。
|
||||||
|
*
|
||||||
|
* 取数必须用 `useRequestFetch()`,不能用裸 `$fetch`:SSR 期间裸 `$fetch` 不转发浏览器
|
||||||
|
* Cookie,代理会变成匿名调用并拿到 code=401,`handleAuthCode` 于是清掉 Token ——
|
||||||
|
* 刷新详情页 = 把自己登出(完整说明见 app/pages/tender/orders/index.vue:101-110)。
|
||||||
|
*/
|
||||||
|
const route = useRoute()
|
||||||
|
const { isLoggedIn, handleAuthCode } = useHjcAuth()
|
||||||
|
const requestFetch = useRequestFetch()
|
||||||
|
|
||||||
|
const orderId = computed(() => String(route.params.id || ''))
|
||||||
|
const loginRedirect = computed(() => encodeURIComponent(route.fullPath))
|
||||||
|
|
||||||
|
const loggedIn = ref(false)
|
||||||
|
const order = shallowRef<HjcOrder | null>(null)
|
||||||
|
const loaded = ref(false)
|
||||||
|
const error = ref('')
|
||||||
|
/** 操作结果提示(取消成功/已支付),与 error 分开:它不是错误 */
|
||||||
|
const notice = ref('')
|
||||||
|
|
||||||
|
const cancelDialogOpen = ref(false)
|
||||||
|
const cancelling = ref(false)
|
||||||
|
|
||||||
|
/** 已支付:标书附件是否下发的唯一依据(与后端 `isPaid` 同一口径,只看 payStatus) */
|
||||||
|
const isPaid = computed(() => order.value?.payStatus === 1)
|
||||||
|
const payMethodLabel = computed(() => (order.value?.payMethod === 'ALIPAY' ? '支付宝' : '微信支付'))
|
||||||
|
|
||||||
|
function formatMoney(n?: number | string) {
|
||||||
|
return Number(n ?? 0).toFixed(2)
|
||||||
|
}
|
||||||
|
function fmtTime(s?: string) {
|
||||||
|
return s ? s.slice(0, 19).replace('T', ' ') : '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reload() {
|
||||||
|
try {
|
||||||
|
// 代理原样透传 ApiResult:按 body.code 判定(HTTP 状态码恒为 200,不可用于判定)
|
||||||
|
const res: any = await requestFetch(`/api/tender/order-detail?id=${encodeURIComponent(orderId.value)}`)
|
||||||
|
const auth = await handleAuthCode(res, route.fullPath)
|
||||||
|
if (auth.handled) {
|
||||||
|
// 401 已清凭据并跳登录页;403 只提示,不清 token
|
||||||
|
error.value = auth.message || ''
|
||||||
|
loggedIn.value = isLoggedIn()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (res?.code === 0 && res.data) {
|
||||||
|
order.value = res.data as HjcOrder
|
||||||
|
error.value = ''
|
||||||
|
} else {
|
||||||
|
order.value = null
|
||||||
|
error.value = res?.message || '订单加载失败'
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
order.value = null
|
||||||
|
error.value = e?.data?.message || e?.message || '订单加载失败'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function askCancel() {
|
||||||
|
notice.value = ''
|
||||||
|
cancelDialogOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmCancel() {
|
||||||
|
const o = order.value
|
||||||
|
if (!o) return
|
||||||
|
cancelling.value = true
|
||||||
|
notice.value = ''
|
||||||
|
try {
|
||||||
|
const res: any = await $fetch('/api/tender/cancel', { method: 'POST', body: { orderNo: o.orderNo } })
|
||||||
|
const auth = await handleAuthCode(res, route.fullPath)
|
||||||
|
if (auth.handled) {
|
||||||
|
notice.value = auth.message || ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (res?.code !== 0) {
|
||||||
|
notice.value = res?.message || '取消失败,请稍后重试'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 「其实已经付了」**不是错误**:后端同样返回 code=0,结论在结构化字段里。
|
||||||
|
// 按字段判定,绝不匹配 message 字符串(与列表页同一口径)。
|
||||||
|
const d = res.data || {}
|
||||||
|
if (d.cancelled) {
|
||||||
|
notice.value = d.verified === false
|
||||||
|
// 微信不可达时如实告知没核对上,而不是替它下结论
|
||||||
|
? '订单已取消(未能在微信侧核对,如你已付款请稍后刷新订单)'
|
||||||
|
: '订单已取消'
|
||||||
|
} else if (d.paid) {
|
||||||
|
notice.value = '该订单已支付成功,无需取消'
|
||||||
|
} else {
|
||||||
|
notice.value = res?.message || '取消未生效,请刷新后查看订单状态'
|
||||||
|
}
|
||||||
|
// 无论哪种结论都就地重取:状态可能已在服务端变化(含被自愈成已支付),
|
||||||
|
// 而详情页正是「支没支付、能不能下载」要显示对的地方。
|
||||||
|
await reload()
|
||||||
|
} catch (e: any) {
|
||||||
|
notice.value = e?.data?.message || e?.message || '取消失败,请稍后重试'
|
||||||
|
} finally {
|
||||||
|
cancelling.value = false
|
||||||
|
cancelDialogOpen.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loggedIn.value = isLoggedIn()
|
||||||
|
if (loggedIn.value) {
|
||||||
|
await reload()
|
||||||
|
}
|
||||||
|
loaded.value = true
|
||||||
|
</script>
|
||||||
@@ -29,11 +29,33 @@
|
|||||||
<span class="text-blue-600 font-bold">¥{{ formatMoney(o.totalAmount) }}</span>
|
<span class="text-blue-600 font-bold">¥{{ formatMoney(o.totalAmount) }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
标书附件:**只有已支付订单**才有这一块——`tenderFiles` 由后端按 payStatus=1
|
||||||
|
下发(hjc-h5 的列表与详情同一口径)。未支付订单这里什么都不渲染,
|
||||||
|
因为地址压根没下发,不是前端藏起来的。
|
||||||
|
-->
|
||||||
|
<div v-if="o.tenderFiles?.length" class="mt-4 border-t border-gray-100 pt-4">
|
||||||
|
<p class="mb-2 text-sm font-medium text-gray-700">标书附件</p>
|
||||||
|
<HjcTenderFiles :files="o.tenderFiles" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 flex items-center justify-between border-t border-gray-100 pt-4">
|
||||||
|
<!--
|
||||||
|
详情入口。**不**把整张卡片做成可点:卡片里已经有「取消订单」「去支付」
|
||||||
|
和被撑开的一列附件链接,嵌套点击的误触代价比省一次点击更高。
|
||||||
|
-->
|
||||||
|
<NuxtLink
|
||||||
|
:to="`/tender/orders/${o.id}`"
|
||||||
|
class="text-sm text-blue-600 hover:underline"
|
||||||
|
>
|
||||||
|
查看详情
|
||||||
|
</NuxtLink>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
待支付订单的操作区。判定用 hjcCanActOnOrder(payStatus===0 && orderStatus!==2),
|
待支付订单的操作区。判定用 hjcCanActOnOrder(payStatus===0 && orderStatus!==2),
|
||||||
**不用** statusKey==='pending'——payStatus=2(支付失败)也会落进 pending 兜底。
|
**不用** statusKey==='pending'——payStatus=2(支付失败)也会落进 pending 兜底。
|
||||||
-->
|
-->
|
||||||
<div v-if="hjcCanActOnOrder(o)" class="mt-4 flex justify-end gap-3 border-t border-gray-100 pt-4">
|
<div v-if="hjcCanActOnOrder(o)" class="flex justify-end gap-3">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="rounded-md border border-gray-300 px-4 py-1.5 text-sm text-gray-700 hover:bg-gray-50 disabled:opacity-50"
|
class="rounded-md border border-gray-300 px-4 py-1.5 text-sm text-gray-700 hover:bg-gray-50 disabled:opacity-50"
|
||||||
@@ -51,6 +73,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-else-if="loaded" class="py-20 text-center text-gray-500">暂无订单</div>
|
<div v-else-if="loaded" class="py-20 text-center text-gray-500">暂无订单</div>
|
||||||
<SiteLoading v-else />
|
<SiteLoading v-else />
|
||||||
@@ -123,7 +123,7 @@ const { isLoggedIn, handleAuthCode } = useHjcAuth()
|
|||||||
* 详情响应里内联了 `favorited`,而站内代理只有在 **Cookie 被转发** 时才认得出登录态:
|
* 详情响应里内联了 `favorited`,而站内代理只有在 **Cookie 被转发** 时才认得出登录态:
|
||||||
* Nuxt 只在 `useRequestFetch` / `useFetch` 里做转发,裸 `$fetch` 在 SSR 期间不转发 Cookie,
|
* Nuxt 只在 `useRequestFetch` / `useFetch` 里做转发,裸 `$fetch` 在 SSR 期间不转发 Cookie,
|
||||||
* 代理就是匿名调用,后端按契约返回 `favorited = false` —— 已收藏的项目首屏会显示成「未收藏」。
|
* 代理就是匿名调用,后端按契约返回 `favorited = false` —— 已收藏的项目首屏会显示成「未收藏」。
|
||||||
* (同一个坑的另一种表现见 app/pages/tender/orders.vue:77-88:那次是刷新把自己登出。)
|
* (同一个坑的另一种表现见 app/pages/tender/orders/index.vue:101-110:那次是刷新把自己登出。)
|
||||||
*
|
*
|
||||||
* 客户端侧 `useRequestFetch()` 等价于 `globalThis.$fetch`,行为不变。
|
* 客户端侧 `useRequestFetch()` 等价于 `globalThis.$fetch`,行为不变。
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -86,6 +86,48 @@ export interface TenderPage {
|
|||||||
count: number
|
count: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标书附件(对应 mp-java `HjcTenderFileVo`)。
|
||||||
|
*
|
||||||
|
* 由**后端**把项目的 `tender_file`(JSON 数组或逗号分隔)解析成结构化列表,
|
||||||
|
* 前端只渲染、不解析也不判权限。
|
||||||
|
*/
|
||||||
|
export interface HjcTenderFile {
|
||||||
|
/** 展示名;后端取不到时会兜底成「标书附件」 */
|
||||||
|
name?: string
|
||||||
|
/** 可下载地址 */
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订单(对应 mp-java `HjcOrder`,只列页面用得到的字段)。
|
||||||
|
*
|
||||||
|
* `tenderFiles` 是**付费内容**:只有 `payStatus === 1`(已支付)的订单,后端才会下发;
|
||||||
|
* 待支付 / 支付失败 / 已退款一律为 `null`(空数组表示已支付但项目没有附件)。
|
||||||
|
* 前端**不根据 payStatus 自己拼地址**——地址没下发就是拿不到。
|
||||||
|
*/
|
||||||
|
export interface HjcOrder {
|
||||||
|
id: number
|
||||||
|
orderNo: string
|
||||||
|
projectId?: number
|
||||||
|
projectNo?: string
|
||||||
|
projectName?: string
|
||||||
|
enterpriseName?: string
|
||||||
|
contactName?: string
|
||||||
|
contactPhone?: string
|
||||||
|
contactEmail?: string
|
||||||
|
quantity?: number
|
||||||
|
unitPrice?: number
|
||||||
|
totalAmount?: number
|
||||||
|
payMethod?: string
|
||||||
|
payTime?: string
|
||||||
|
payStatus?: number
|
||||||
|
orderStatus?: number
|
||||||
|
createTime?: string
|
||||||
|
/** 仅已支付订单由后端下发 */
|
||||||
|
tenderFiles?: HjcTenderFile[] | null
|
||||||
|
}
|
||||||
|
|
||||||
/** 登录用户(对应后端 common.system.User 关键字段) */
|
/** 登录用户(对应后端 common.system.User 关键字段) */
|
||||||
export interface HjcUser {
|
export interface HjcUser {
|
||||||
userId: number
|
userId: number
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { $fetch } from 'ofetch'
|
||||||
|
import { createError, defineEventHandler, getHeader, getCookie, getQuery } from 'h3'
|
||||||
|
import { useRuntimeConfig } from '#imports'
|
||||||
|
import { getTenantFromContext } from '../../utils/tenant'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订单详情(含已支付订单的标书附件)
|
||||||
|
* GET /api/tender/order-detail?id=16
|
||||||
|
* 代理到 mp-api /api/hjc/order/{id};需登录态 + 订单归属(后端按 401/403 区分)。
|
||||||
|
*
|
||||||
|
* 为什么要这个代理:`/hjc/order/{id}` 会带上 `project`,且对**已支付**订单附带
|
||||||
|
* `tenderFiles`(后端解析好的标书附件列表)——标书下载就靠它。未支付订单两张表都不给,
|
||||||
|
* 所以页面上「没有附件」不是前端藏起来的,而是后端压根没下发地址。
|
||||||
|
*
|
||||||
|
* 约定:原样透传 ApiResult{code,message,data}。HTTP 状态码恒为 200,
|
||||||
|
* 「未登录/token 失效」是 code=401、「无权查看该订单」是 code=403,前端必须按 code 判定;
|
||||||
|
* 故这里**不能**只取 data,否则登录态失败会被当成「订单不存在」。
|
||||||
|
*/
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const ctx = getTenantFromContext(event, config)
|
||||||
|
const { id } = getQuery(event)
|
||||||
|
const cookieToken = getCookie(event, 'hjc_token')
|
||||||
|
const auth = getHeader(event, 'authorization') || (cookieToken ? `Bearer ${cookieToken}` : null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await $fetch(`/hjc/order/${encodeURIComponent(String(id ?? ''))}`, {
|
||||||
|
baseURL: config.public.modulesApiBase,
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
TenantId: ctx.tenantId,
|
||||||
|
...(auth ? { Authorization: auth } : {})
|
||||||
|
},
|
||||||
|
query: { TenantId: ctx.tenantId }
|
||||||
|
})
|
||||||
|
} catch (error: any) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: error?.statusCode || error?.response?.status || 502,
|
||||||
|
statusMessage: error?.statusMessage || 'Failed to fetch order detail'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user