feat(hjc-web): 附件下载改为「点击时取票再下载」

一站式的静态文件服务已加鉴权,附件不再直连(工作区根目录 docs/adr/0007):
后端下发的地址现在是**本平台资源地址**(kind + 订单号/项目编号 + 序号),
既没有文件路径(路径由后端查,防越权)也没有票据。

- 新增 Nitro 透传 server/api/tender/attachment-ticket.get.ts:带 Authorization
  (请求头或 hjc_token cookie)+ TenantId 调后端取票,原样透传 ApiResult。
- HjcTenderFiles.vue:`<a :href>` → 按钮 + 点击时取票 + `window.location.href`。
  用 location 而不是 window.open:取票是异步的,异步之后弹窗会被浏览器拦截;
  附件响应带 Content-Disposition: attachment,下载完仍留在本页。
  该组件同时被订单列表/订单详情/项目详情(公告附件)复用,一处改动覆盖三处。
- 错误提示直接复用后端 401/403 约定,组件内联展示,不引入新的 toast 依赖。

校验:npm run build 通过(新增路由已出现在构建产物中)。
This commit is contained in:
2026-09-18 17:09:39 +08:00
parent d1cf0db89e
commit 6c40165f86
2 changed files with 139 additions and 20 deletions
@@ -0,0 +1,53 @@
import { $fetch } from 'ofetch'
import { createError, defineEventHandler, getHeader, getCookie, getQuery } from 'h3'
import { useRuntimeConfig } from '#imports'
import { getTenantFromContext } from '../../utils/tenant'
/**
* 取附件下载票据(一次性、短时)。
* GET /api/tender/attachment-ticket?kind=tender&orderNo=xxx&index=0
* GET /api/tender/attachment-ticket?kind=bulletin&projectNo=xxx&index=0
*
* 为什么要它:一站式的静态文件服务加了鉴权,附件不再直连,改由后端反代
* (工作区根目录 `docs/adr/0007-attachment-download-via-platform-proxy.md`)。
* 下发给前端的附件地址是**本平台的资源地址**(只有 kind/订单号或项目编号/序号,
* 没有路径、也没有票据);**点击下载时**才来这里换一张票据,再用票据去
* `{modulesApiBase}/api/hjc/attachment?ticket=…` 取文件。
*
* 为什么票据要现取而不能直接放进列表里的地址:票据很短命,"页面开着放一会儿再点"
* 会撞过期;现取则永远新鲜(ADR 0007 补记)。
*
* 约定:原样透传 ApiResult{code,message,data}HTTP 状态码恒为 200。
* `code=401`(未登录)与 `code=403`(未支付/无权/已下架)都是**业务码**,
* 前端必须按 code 判定——401 要清凭据跳登录,403 只提示。
*/
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig()
const ctx = getTenantFromContext(event, config)
const { kind, orderNo, projectNo, index } = getQuery(event)
const cookieToken = getCookie(event, 'hjc_token')
const auth = getHeader(event, 'authorization') || (cookieToken ? `Bearer ${cookieToken}` : null)
try {
return await $fetch('/hjc/attachment/ticket', {
baseURL: config.public.modulesApiBase,
method: 'GET',
headers: {
TenantId: ctx.tenantId,
...(auth ? { Authorization: auth } : {})
},
query: {
TenantId: ctx.tenantId,
kind,
index,
...(orderNo ? { orderNo } : {}),
...(projectNo ? { projectNo } : {})
}
})
} catch (error: any) {
throw createError({
statusCode: error?.statusCode || error?.response?.status || 502,
statusMessage: error?.statusMessage || 'Failed to fetch attachment ticket'
})
}
})