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:
@@ -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>
|
||||||
|
|||||||
@@ -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 无权限、其余为业务失败)。
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user