feat(order): 下单页新增发票类型选择

- BuyDocument.vue:购买页新增「发票类型」单选(VAT_SPECIAL 电子专票 / VAT_NORMAL 电子普票),
  必填,未选时拦在 $fetch 之前并点名提示「请选择发票类型」
- 预选值取企业档案的 invoiceType(走 /api/tender/enterprise,用 useRequestFetch 保证 SSR/CSR 一致);
  取不到或值不在枚举内则不预选,由提交那道兜住
- 建单 body 带上 invoiceType
- order.post.ts 本就把 readBody 结果整体透传给后端,未做字段白名单,无需改动;
  仅更新接口契约注释
This commit is contained in:
2026-10-08 14:54:22 +08:00
parent 15539a97b8
commit cadb2b837f
2 changed files with 62 additions and 3 deletions
@@ -39,6 +39,24 @@
<label class="text-sm text-gray-600">购买数量</label> <label class="text-sm text-gray-600">购买数量</label>
<input v-model.number="form.quantity" type="number" min="1" class="mt-1 w-full rounded-md border border-gray-300 px-4 py-2 text-sm" /> <input v-model.number="form.quantity" type="number" min="1" class="mt-1 w-full rounded-md border border-gray-300 px-4 py-2 text-sm" />
</div> </div>
<div>
<span class="text-sm text-gray-600">发票类型</span>
<!--
预选值来自**企业档案**里的 invoiceType(注册时已必填);买家改了以买家为准,
企业档案不因下单而改变(订单是快照)。档案取不到或值非法时**不预选**。
-->
<div class="mt-2 flex flex-wrap items-center gap-6">
<label
v-for="t in INVOICE_TYPES"
:key="t.value"
class="flex items-center gap-2 text-sm text-gray-700"
>
<input v-model="form.invoiceType" type="radio" :value="t.value" :disabled="submitting" />
{{ t.label }}
</label>
</div>
<p class="mt-1 text-xs text-gray-400">默认取企业档案里的发票类型,可改;按下单时选的类型开票</p>
</div>
<button type="submit" class="w-full rounded-md bg-blue-600 py-3 text-white font-medium hover:bg-blue-700 disabled:opacity-50" :disabled="submitting"> <button type="submit" class="w-full rounded-md bg-blue-600 py-3 text-white font-medium hover:bg-blue-700 disabled:opacity-50" :disabled="submitting">
{{ submitting ? '提交中...' : '提交订单并支付' }} {{ submitting ? '提交中...' : '提交订单并支付' }}
@@ -118,15 +136,33 @@ const payDeadline = ref<string | null>(null)
/** 支付时限已到:进支付步就过期、或面板挂着挂着归零(面板的 `timeout` 事件) */ /** 支付时限已到:进支付步就过期、或面板挂着挂着归零(面板的 `timeout` 事件) */
const deadlineExpired = ref(false) const deadlineExpired = ref(false)
const form = reactive({ contactName: '', contactPhone: '', contactEmail: '', quantity: 1 }) const form = reactive({ contactName: '', contactPhone: '', contactEmail: '', quantity: 1, invoiceType: '' })
const redirect = computed(() => `/buy?id=${id}`) const redirect = computed(() => `/buy?id=${id}`)
/**
* 发票类型:单选、必填,取值与后端 `hjc_invoice.invoice_type` 口径一致。
* 初始值取企业档案里的 invoiceType(见下方 setup 取数),买家可改。
*/
const INVOICE_TYPES = [
{ value: 'VAT_SPECIAL', label: '电子专票' },
{ value: 'VAT_NORMAL', label: '电子普票' }
]
function isValidInvoiceType(v: unknown): v is string {
return INVOICE_TYPES.some((t) => t.value === v)
}
function formatMoney(n?: number | string) { function formatMoney(n?: number | string) {
return Number(n ?? 0).toFixed(2) return Number(n ?? 0).toFixed(2)
} }
async function submit() { async function submit() {
error.value = '' error.value = ''
if (!isValidInvoiceType(form.invoiceType)) {
// 必填、点名到项:没有值就拦在发请求之前
error.value = '请选择发票类型'
return
}
submitting.value = true submitting.value = true
try { try {
// 代理原样透传 ApiResult:按 body.code 判定(HTTP 状态码恒为 200,不可用于判定) // 代理原样透传 ApiResult:按 body.code 判定(HTTP 状态码恒为 200,不可用于判定)
@@ -137,7 +173,9 @@ async function submit() {
quantity: form.quantity || 1, quantity: form.quantity || 1,
contactName: form.contactName, contactName: form.contactName,
contactPhone: form.contactPhone, contactPhone: form.contactPhone,
contactEmail: form.contactEmail contactEmail: form.contactEmail,
// 下单那一刻的发票类型(订单是快照,不回写企业档案)
invoiceType: form.invoiceType
} }
}) })
const orderAuth = await handleAuthCode(order, redirect.value) const orderAuth = await handleAuthCode(order, redirect.value)
@@ -205,4 +243,22 @@ try {
} catch { } catch {
notFound.value = true notFound.value = true
} }
/**
* 预选发票类型 = 企业档案里的 `invoiceType`(注册时已必填,正常不为空)。
*
* 与上面同理必须用 `useRequestFetch`:裸 `$fetch` 在 SSR 不转发浏览器原始请求头,
* 代理会当成未登录/空租户,服务端取不到档案而客户端取得到 → hydration mismatch。
* 档案取不到、或值不在枚举内 → **不预选**,由提交那道的必选提示兜住:
* 拿不到档案不该阻断下单(接口失败时买家仍可自己选一个继续)。
*/
if (loggedIn.value) {
try {
const ent: any = await requestFetch('/api/tender/enterprise')
const archiveType = ent?.code === 0 ? ent?.data?.invoiceType : ''
if (isValidInvoiceType(archiveType)) form.invoiceType = archiveType
} catch {
// 取不到档案不影响下单:不预选,提交时必选兜底
}
}
</script> </script>
+4 -1
View File
@@ -5,9 +5,12 @@ import { getTenantFromContext } from '../../utils/tenant'
/** /**
* 创建标书订单 * 创建标书订单
* POST /api/tender/order { projectId, quantity, contactName, contactPhone, contactEmail } * POST /api/tender/order { projectId, quantity, contactName, contactPhone, contactEmail, invoiceType }
* 代理到后端 /api/hjc/order/create;需登录态(从 cookie hjc_token 或 Authorization 透传)。 * 代理到后端 /api/hjc/order/create;需登录态(从 cookie hjc_token 或 Authorization 透传)。
* *
* `invoiceType`(VAT_SPECIAL 电子专票 / VAT_NORMAL 电子普票)由下单页必选;本代理
* **body 整体透传**(不做字段白名单/裁剪),校验在后端。
*
* 约定:原样透传 ApiResult{code,message,data},由前端按 code 判定 * 约定:原样透传 ApiResult{code,message,data},由前端按 code 判定
* (401 未登录/token 失效、403 无权限、其余为业务失败)。 * (401 未登录/token 失效、403 无权限、其余为业务失败)。
*/ */