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:
2026-09-17 18:09:27 +08:00
parent 9299485899
commit 2e847b5e09
8 changed files with 490 additions and 17 deletions
+77
View File
@@ -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>