Files
hjc-web/app/composables/useAttachmentDownload.ts
weicw1996 1ff4dfffb9 feat(hjc-web): 订单页展示并下载电子发票
发票由一站式平台开具后推给后端(官方网不开票,见工作区根目录 docs/adr/0006),
订单响应里新增 invoices;文件是本平台副本,下载走与附件同一条链路(点击时取票)。

- 抽出 composable useAttachmentDownload:把「先换票据、再取文件」的逻辑从
  HjcTenderFiles 里提出来,附件与发票共用(各写一份必然分叉);同时把资源标识解析
  扩到 orderNo/projectNo/invoiceId 三种。
- 新增 HjcInvoiceFiles.vue:发票号/类型/开票日期/金额 + 下载;作废(红冲)的也列出来
  且仍可下载——它是留档凭证。
- HjcTenderFiles.vue 改用 composable,行为不变。
- 订单详情加发票区块,订单列表加「已开票」标记与发票列表。
- 不判"是否已支付":有没有发票取决于甲方开没开票,与订单当前状态无关。

校验:npm run build 通过。
2026-09-18 17:33:01 +08:00

91 lines
3.8 KiB
TypeScript
Raw Permalink 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.
import { ref } from 'vue'
/**
* 「点击下载」的共用逻辑:**先换票据、再取文件**。
*
* 背景:一站式的静态文件服务已加鉴权,附件/发票不再直连(工作区根目录
* `docs/adr/0007-attachment-download-via-platform-proxy.md`)。后端下发的地址是
* **本平台的资源地址**——只有资源标识(kind + 订单号/项目编号/发票 id + 序号),
* 既没有文件路径(路径由后端查,防越权),也没有票据。票据是**点击时**现取的
* (ADR 0007 补记:现取的永远新鲜,不会出现"页面放久了点不动")。
*
* 抽出来的理由:标书附件、公告附件、电子发票三处都要走同一条链路,各写一份必然分叉
* ——与 `orderStatus.ts`、`hjcCanActOnOrder` 同一动机。
*/
export function useAttachmentDownload() {
/** 正在准备的文件标识(用于禁用按钮、显示"正在准备…"),同一时刻只允许一个 */
const busyKey = ref('')
const errorText = ref('')
/**
* 从资源地址里取出取票所需的标识。
*
* 地址形态由后端固定生成,只有这几个参数;解析不出来就说明地址不是本平台的资源地址
* (例如历史数据里的 OSS 直链),那种直接按普通链接打开即可。
*/
function resourceQueryOf(url: string): Record<string, string> | null {
const query = String(url || '').split('?')[1]
if (!query) return null
const params = new URLSearchParams(query)
const kind = params.get('kind')
const index = params.get('index')
if (!kind || index === null) return null
const target: Record<string, string> = { kind, index }
for (const key of ['orderNo', 'projectNo', 'invoiceId']) {
const value = params.get(key)
if (value) target[key] = value
}
// 至少要有一个资源标识,否则后端也无从查起
return target.orderNo || target.projectNo || target.invoiceId ? target : null
}
/** 票据地址是相对后端的(`/api/hjc/attachment?ticket=…`),补上后端域名 */
function absoluteApiUrl(url: string) {
if (/^https?:\/\//i.test(url)) return url
const base = String(useRuntimeConfig().public.modulesApiBase || '').replace(/\/api\/?$/, '')
return base + (url.startsWith('/') ? url : `/${url}`)
}
/**
* 取票并打开文件。
*
* @param file 后端下发的 `{name, url}`url 是资源地址)
* @param key 用于标识"哪一个正在准备"(同一列表里区分按钮)
*/
async function download(file: { name?: string; url?: string }, key?: string) {
errorText.value = ''
const url = String(file?.url || '')
const query = resourceQueryOf(url)
// 不是本平台的资源地址(历史 OSS 直链):原样打开,没必要绕一圈
if (!query) {
if (/^https?:\/\//i.test(url)) {
window.location.href = url
return
}
errorText.value = '文件地址不可用,请刷新页面后重试'
return
}
const busy = key || url
busyKey.value = busy
try {
const res: any = await $fetch('/api/tender/attachment-ticket', { query })
if (!res || res.code !== 0 || !res.data?.url) {
errorText.value = res?.message || '获取下载链接失败,请稍后重试'
return
}
// 用 location 而不是 window.open:取票是异步的,异步之后再弹窗会被浏览器拦截;
// 而附件响应带 Content-Disposition: attachment,浏览器下载完仍留在本页。
window.location.href = absoluteApiUrl(String(res.data.url))
} catch (error: any) {
errorText.value =
error?.data?.message || error?.statusMessage || error?.message || '下载失败,请稍后重试'
} finally {
busyKey.value = ''
}
}
return { busyKey, errorText, download }
}