1ff4dfffb9
发票由一站式平台开具后推给后端(官方网不开票,见工作区根目录 docs/adr/0006), 订单响应里新增 invoices;文件是本平台副本,下载走与附件同一条链路(点击时取票)。 - 抽出 composable useAttachmentDownload:把「先换票据、再取文件」的逻辑从 HjcTenderFiles 里提出来,附件与发票共用(各写一份必然分叉);同时把资源标识解析 扩到 orderNo/projectNo/invoiceId 三种。 - 新增 HjcInvoiceFiles.vue:发票号/类型/开票日期/金额 + 下载;作废(红冲)的也列出来 且仍可下载——它是留档凭证。 - HjcTenderFiles.vue 改用 composable,行为不变。 - 订单详情加发票区块,订单列表加「已开票」标记与发票列表。 - 不判"是否已支付":有没有发票取决于甲方开没开票,与订单当前状态无关。 校验:npm run build 通过。
91 lines
3.8 KiB
TypeScript
91 lines
3.8 KiB
TypeScript
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 }
|
||
}
|