feat(order): 收银台支付倒计时,超时锁死并引导重新下单
- 到期时刻由后端下发(payDeadline),倒计时与"到点锁页面"都用它,前端不自己算时限 - 解析后端时间时把空格换成 T:后端格式是 `yyyy-MM-dd HH:mm:ss`, 而 iOS Safari / 微信内置浏览器解析不了不带 T 的写法(返回 NaN, 会让倒计时与锁页在最主要的那一端静默失效) - 修正收银台写死的「2 小时」文案:有效期由后端配置,页面不该复述一个具体数字 - 订单列表/详情:超时后不再给「去支付」,并显示「已取消(支付超时)」 - `hjcCanActOn` 收成共享导出,两个页面不再各写一份判定 - 与 hjc-h5 的 utils/orderStatus 保持同一套定义(两仓逐字对齐)
This commit is contained in:
@@ -13,11 +13,11 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* 支付码的有效期。
|
||||
* 支付码有效期的**兜底**值(从调用 `start` 的那一刻起算)。
|
||||
*
|
||||
* 微信 Native 默认 2 小时;支付宝当面付的超时由下单时的 `timeout_express` 决定。
|
||||
* 服务端都不下发过期时刻(`PaymentResponse` 不设 `expireTime`),
|
||||
* 所以只能以「本次拿到支付码的时刻」本地计时;到点停止轮询并提示重新获取。
|
||||
* 优先用后端随订单下发的 `payDeadline`——那才是权威值,且与后端判定超时、渠道单失效
|
||||
* 是同一个时刻(见 ADR 0019),`start` 的第二个参数就是传它。
|
||||
* 拿不到到期时刻时才退回这个本地计时(例如购标流程里还没取到订单对象就被调用)。
|
||||
*/
|
||||
export const HJC_PAY_QR_TTL_MS = 2 * 60 * 60 * 1000
|
||||
|
||||
@@ -143,14 +143,22 @@ export function useHjcPayStatus(
|
||||
}
|
||||
}
|
||||
|
||||
/** 开始(或重新开始)轮询,并重置 2 小时计时 */
|
||||
function start(no: string) {
|
||||
/**
|
||||
* 开始(或重新开始)轮询。
|
||||
*
|
||||
* @param no 订单号
|
||||
* @param deadlineAtMs 该订单支付时限的到期时刻(毫秒)。**传后端下发的 `payDeadline`**——
|
||||
* 用本地倒计时代替它,两者会在边界上错开。不传则退回 {@link HJC_PAY_QR_TTL_MS}
|
||||
*/
|
||||
function start(no: string, deadlineAtMs?: number | null) {
|
||||
orderNo = no
|
||||
paid.value = false
|
||||
expired.value = false
|
||||
error.value = ''
|
||||
status.value = null
|
||||
deadline = Date.now() + HJC_PAY_QR_TTL_MS
|
||||
deadline = typeof deadlineAtMs === 'number' && Number.isFinite(deadlineAtMs)
|
||||
? deadlineAtMs
|
||||
: Date.now() + HJC_PAY_QR_TTL_MS
|
||||
active = true
|
||||
pauseTimer()
|
||||
ensureTimer()
|
||||
|
||||
@@ -66,8 +66,12 @@
|
||||
<HjcInvoiceFiles :files="order.invoices" />
|
||||
</div>
|
||||
|
||||
<!-- 待支付订单的操作区(与列表页同一判定:hjcCanActOnOrder) -->
|
||||
<div v-if="hjcCanActOnOrder(order)" class="mt-4 flex justify-end gap-3 rounded-lg border border-gray-200 p-5">
|
||||
<!--
|
||||
待支付订单的操作区(与列表页同一判定)。
|
||||
`canAct` = hjcCanActOnOrder 再排除「支付时限已到」——已超时的单后端会拒绝出码,
|
||||
留一个点了必然失败的「去支付」只会让人白点一次。
|
||||
-->
|
||||
<div v-if="canAct" class="mt-4 flex justify-end gap-3 rounded-lg border border-gray-200 p-5">
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-md border border-gray-300 px-4 py-1.5 text-sm text-gray-700 hover:bg-gray-50 disabled:opacity-50"
|
||||
@@ -178,7 +182,11 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { HjcOrder } from '~/types/tender'
|
||||
import { hjcCanActOnOrder, hjcOrderStatusClass, hjcOrderStatusLabel } from '~/utils/orderStatus'
|
||||
import {
|
||||
hjcCanActOn,
|
||||
hjcOrderStatusClass,
|
||||
hjcOrderStatusLabel
|
||||
} from '~/utils/orderStatus'
|
||||
|
||||
/**
|
||||
* 订单详情页(PC 端)。
|
||||
@@ -215,6 +223,12 @@ const cancelling = ref(false)
|
||||
const isPaid = computed(() => order.value?.payStatus === 1)
|
||||
const payMethodLabel = computed(() => (order.value?.payMethod === 'ALIPAY' ? '支付宝' : '微信支付'))
|
||||
|
||||
/**
|
||||
* 还能不能操作(去支付 / 取消):共享判定,**不在这里重写一遍**——
|
||||
* 两个页面各写一份必然分叉,出现"列表里不能点、详情里能点"。
|
||||
*/
|
||||
const canAct = computed(() => hjcCanActOn(order.value))
|
||||
|
||||
function formatMoney(n?: number | string) {
|
||||
return Number(n ?? 0).toFixed(2)
|
||||
}
|
||||
|
||||
@@ -63,8 +63,10 @@
|
||||
<!--
|
||||
待支付订单的操作区。判定用 hjcCanActOnOrder(payStatus===0 && orderStatus!==2),
|
||||
**不用** statusKey==='pending'——payStatus=2(支付失败)也会落进 pending 兜底。
|
||||
再排除「支付时限已到」:那种单后端会拒绝出码、渠道单也已随同一时刻失效(ADR 0019),
|
||||
留一个点了必然失败的「去支付」只会让人白点一次。
|
||||
-->
|
||||
<div v-if="hjcCanActOnOrder(o)" class="flex justify-end gap-3">
|
||||
<div v-if="canAct(o)" class="flex justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-md border border-gray-300 px-4 py-1.5 text-sm text-gray-700 hover:bg-gray-50 disabled:opacity-50"
|
||||
@@ -103,7 +105,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { hjcCanActOnOrder, hjcOrderStatusClass, hjcOrderStatusLabel } from '~/utils/orderStatus'
|
||||
import {
|
||||
hjcCanActOn,
|
||||
hjcOrderStatusClass,
|
||||
hjcOrderStatusLabel
|
||||
} from '~/utils/orderStatus'
|
||||
|
||||
const { isLoggedIn, handleAuthCode } = useHjcAuth()
|
||||
/**
|
||||
|
||||
@@ -33,11 +33,37 @@
|
||||
<NuxtLink to="/tender/orders" class="mt-6 inline-block rounded-md bg-blue-600 px-6 py-2 text-white hover:bg-blue-700">返回我的订单</NuxtLink>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
支付时限已到(但后端还没把它落定为已取消)。
|
||||
**必须锁死,不给「重新发起支付」**:同一个订单号对同一渠道重复统一下单会撞
|
||||
OUT_TRADE_NO_USED,而且渠道侧那张支付单此刻已随支付时限一同失效(ADR 0019)——
|
||||
再出一张码要么被渠道拒绝,要么给出一张我们马上就要取消的订单的码。
|
||||
超时 = 这张订单终了,想要就重新下单。
|
||||
-->
|
||||
<div v-else-if="timedOut" class="mx-auto mt-6 max-w-2xl rounded-lg border border-amber-200 bg-amber-50 p-8 text-center text-amber-800">
|
||||
<p class="text-xl font-bold">支付已超时</p>
|
||||
<p class="mt-2 text-sm opacity-80">该订单因支付超时已取消,如需购买请重新下单。</p>
|
||||
<p class="mt-1 text-sm opacity-80">订单号:{{ orderNo }}</p>
|
||||
<div class="mt-6 flex items-center justify-center gap-3">
|
||||
<NuxtLink :to="projectLink" class="rounded-md bg-blue-600 px-6 py-2 text-white hover:bg-blue-700">重新下单</NuxtLink>
|
||||
<NuxtLink to="/tender/orders" class="rounded-md border border-gray-300 px-6 py-2 text-gray-700 hover:bg-gray-50">返回我的订单</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 待支付:进入即自动发起支付(与购标流程的支付步骤、h5 收银台一致) -->
|
||||
<div v-else class="mx-auto mt-6 max-w-2xl rounded-lg border border-gray-200 p-8 text-center">
|
||||
<h2 class="text-xl font-bold text-gray-900">请使用{{ channelLabel }}支付</h2>
|
||||
<p class="mt-2 text-sm text-gray-500">订单号:{{ orderNo }}</p>
|
||||
<p class="mt-1 text-sm text-gray-500">{{ order?.projectName }}</p>
|
||||
|
||||
<!--
|
||||
倒计时:驱动值来自**后端下发的 payDeadline**,不是前端拿创建时刻自己加时限。
|
||||
它是「这张码还有多久失效」的唯一告知渠道——不给它,用户会拿一张已经不能付的码去扫。
|
||||
没下发到期时刻(旧订单/字段缺失)时不显示,也不锁页面。
|
||||
-->
|
||||
<p v-if="remainText" class="mt-3 text-sm" :class="remainMs !== null && remainMs <= 60_000 ? 'text-red-500' : 'text-gray-500'">
|
||||
支付剩余时间 <span class="font-mono font-semibold">{{ remainText }}</span>
|
||||
</p>
|
||||
<!-- 金额一律来自**订单本身**(by-no),不来自支付响应:
|
||||
支付一失败页面就会停在 0.00,而收银台恰恰是最需要看到金额的地方 -->
|
||||
<p class="my-4 text-2xl font-bold text-blue-600">¥{{ formatMoney(amount) }}</p>
|
||||
@@ -92,7 +118,11 @@
|
||||
<div :class="payExpired ? 'opacity-30' : ''">
|
||||
<HjcPayQrcode :value="codeUrl" :size="240" :alt="qrAlt" />
|
||||
</div>
|
||||
<p class="mt-3 text-sm text-gray-500">请在 2 小时内用{{ channelLabel }}扫描上方二维码完成支付</p>
|
||||
<!--
|
||||
这里**不写具体时长**:支付时限由后端配置、且随订单剩余窗口变化,
|
||||
页面上写死一个数字(原先写的是 2 小时)迟早与上面那行倒计时自相矛盾。
|
||||
-->
|
||||
<p class="mt-3 text-sm text-gray-500">请在支付时限内用{{ channelLabel }}扫描上方二维码完成支付</p>
|
||||
<p v-if="payExpired" class="mt-2 text-sm text-amber-600">二维码已过期,请重新获取后再扫码</p>
|
||||
</template>
|
||||
<div v-else class="rounded-md bg-gray-50 px-4 py-6 text-sm text-gray-500">
|
||||
@@ -130,7 +160,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { hjcOrderStatusKey } from '~/utils/orderStatus'
|
||||
import {
|
||||
hjcFormatCountdown,
|
||||
hjcIsTimeoutCancelled,
|
||||
hjcOrderStatusKey,
|
||||
hjcPayDeadlineMs,
|
||||
hjcPayRemainMs
|
||||
} from '~/utils/orderStatus'
|
||||
|
||||
/**
|
||||
* 收银台:买家为一张**已创建的订单**付款的独立页面(CONTEXT.md 的「收银台」)。
|
||||
@@ -201,13 +237,37 @@ function formatMoney(n?: number | string) {
|
||||
return Number(n ?? 0).toFixed(2)
|
||||
}
|
||||
|
||||
/**
|
||||
* 倒计时用的"当前时刻"。
|
||||
*
|
||||
* 每秒只更新一个 ref(而不是让每个用到它的地方各自 setInterval),
|
||||
* 且**秒级**足够了:支付时限是 30 分钟量级,没必要为 1 秒的精度付出多次渲染。
|
||||
*/
|
||||
const now = ref(Date.now())
|
||||
let clock: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
const remainMs = computed(() => hjcPayRemainMs(order.value, now.value))
|
||||
const remainText = computed(() => (remainMs.value == null ? '' : hjcFormatCountdown(remainMs.value)))
|
||||
/** 到期时刻已知且已归零 —— 页面锁死,不再出码、不再轮询 */
|
||||
const timedOut = computed(() => remainMs.value !== null && remainMs.value <= 0)
|
||||
|
||||
/** 「重新下单」的目标:回到该项目详情(不是购标流程,购标流程会再建一张新单) */
|
||||
const projectLink = computed(() => {
|
||||
const id = order.value?.projectId
|
||||
return id ? `/tender/${id}` : '/tender'
|
||||
})
|
||||
|
||||
/** 状态口径与订单列表同源(`~/utils/orderStatus`),避免两处各判一次导致分叉 */
|
||||
function terminalStateOf(o: any) {
|
||||
switch (hjcOrderStatusKey(o)) {
|
||||
case 'done':
|
||||
return { title: '该订单已完成支付', cls: 'border-green-200 bg-green-50 text-green-700' }
|
||||
case 'cancel':
|
||||
return { title: '该订单已取消', cls: 'border-gray-200 bg-gray-50 text-gray-600' }
|
||||
// 超时取消说清是谁取消的:用户什么都没干,只显示「已取消」会让人以为是自己误触的
|
||||
return {
|
||||
title: hjcIsTimeoutCancelled(o) ? '该订单因支付超时已取消' : '该订单已取消',
|
||||
cls: 'border-gray-200 bg-gray-50 text-gray-600'
|
||||
}
|
||||
case 'refund':
|
||||
return { title: '该订单已退款', cls: 'border-gray-200 bg-gray-50 text-gray-600' }
|
||||
default:
|
||||
@@ -231,6 +291,9 @@ function payErrorMessage(res: any) {
|
||||
}
|
||||
|
||||
async function startPay() {
|
||||
// 已过支付时限就不出码:后端也会拒(它按「本单剩余窗口」算,剩余不足一分钟即拒绝),
|
||||
// 前端先拦住只是省一次注定失败的请求,并让页面停在"支付已超时"而不是闪一下错误。
|
||||
if (timedOut.value) return
|
||||
error.value = ''
|
||||
starting.value = true
|
||||
try {
|
||||
@@ -258,7 +321,8 @@ async function startPay() {
|
||||
}
|
||||
// 无论这次有没有拿到支付码都开始查单:订单可能**已经**在渠道侧付过了
|
||||
//(例如在微信内由小程序付掉、或上一次支付成功但页面被关掉),查单会把它认出来并跳列表。
|
||||
if (orderNo.value) startPayPolling(orderNo.value)
|
||||
// 到期时刻传后端下发的那个:让轮询的终点与后端判定超时、渠道单失效对齐(ADR 0019)。
|
||||
if (orderNo.value) startPayPolling(orderNo.value, hjcPayDeadlineMs(order.value))
|
||||
} catch (e: any) {
|
||||
error.value = e?.data?.message || e?.message || '发起支付失败,请稍后重试'
|
||||
} finally {
|
||||
@@ -358,8 +422,10 @@ onMounted(async () => {
|
||||
if (typeof o?.payMethod === 'string' && o.payMethod.startsWith('ALIPAY')) {
|
||||
payType.value = 'alipay'
|
||||
}
|
||||
// 只有"确实还没付、也没取消"才发起支付
|
||||
loading.value = false
|
||||
// 已过支付时限:锁在"支付已超时"那一屏,不出码、不轮询
|
||||
if (timedOut.value) return
|
||||
// 只有"确实还没付、也没取消"才发起支付
|
||||
await startPay()
|
||||
} catch (e: any) {
|
||||
loadError.value = e?.data?.message || e?.message || '订单加载失败'
|
||||
@@ -367,4 +433,19 @@ onMounted(async () => {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// 秒级 ticking 只服务倒计时显示;真正的"到点锁页面"由 timedOut 这个 computed 判定,
|
||||
// 所以哪怕这一拍晚了几百毫秒,也不会出现"倒计时 00:00 了还能付款"。
|
||||
clock = setInterval(() => {
|
||||
now.value = Date.now()
|
||||
}, 1000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (clock) {
|
||||
clearInterval(clock)
|
||||
clock = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -133,6 +133,17 @@ export interface HjcOrder {
|
||||
payTime?: string
|
||||
payStatus?: number
|
||||
orderStatus?: number
|
||||
/** 取消来源:`BUYER`(买家取消)/ `TIMEOUT`(超时取消),仅已取消订单有值 */
|
||||
cancelSource?: string | null
|
||||
/** 取消时间 */
|
||||
cancelTime?: string | null
|
||||
/**
|
||||
* 支付时限的到期时刻(仅**仍待支付**的订单由后端下发)。
|
||||
*
|
||||
* 它是算出来的(创建时刻 + 配置时限),不是一列。收银台靠它显示倒计时——
|
||||
* 前端不要自己拿 `createTime` 加时限,两边各算一次会让「倒计时归零」与「后端判定超时」错开。
|
||||
*/
|
||||
payDeadline?: string | null
|
||||
createTime?: string
|
||||
/** 仅已支付订单由后端下发 */
|
||||
tenderFiles?: HjcTenderFile[] | null
|
||||
|
||||
@@ -18,10 +18,26 @@
|
||||
/** 订单状态归类键。四端(web / h5 / 小程序 / 后台)未来的展示都应落在这四个之一。 */
|
||||
export type HjcOrderStatusKey = 'pending' | 'done' | 'cancel' | 'refund'
|
||||
|
||||
/** 判定所需的订单字段(只用到这两个,避免依赖完整的 HjcOrder 类型) */
|
||||
/**
|
||||
* 取消来源「超时取消」——后端 `HjcCancelSource.TIMEOUT` 的 name。
|
||||
*
|
||||
* 两种取消对外完全同形(都是「已取消」),区别只在这个来源,而它必须被展示出来:
|
||||
* 超时取消是**系统趁用户不在时替他做的**,用户看不到任何自己触发过的动作,
|
||||
* 而「我什么都没干,订单怎么没了」正是客服必然会接到的那类问询。
|
||||
*/
|
||||
export const HJC_CANCEL_SOURCE_TIMEOUT = 'TIMEOUT'
|
||||
|
||||
/** 判定所需的订单字段 */
|
||||
export interface HjcOrderLike {
|
||||
payStatus?: number | null
|
||||
orderStatus?: number | null
|
||||
/** 取消来源:`BUYER`(买家取消)/ `TIMEOUT`(超时取消),仅已取消订单有值 */
|
||||
cancelSource?: string | null
|
||||
/**
|
||||
* 支付时限的到期时刻(ISO 字符串)。**由后端下发**,前端不自己拿创建时刻加时限——
|
||||
* 两边各算一次的后果是「倒计时归零」与「后端判定超时」在边界上错开。
|
||||
*/
|
||||
payDeadline?: string | null
|
||||
}
|
||||
|
||||
export function hjcOrderStatusKey(o?: HjcOrderLike | null): HjcOrderStatusKey {
|
||||
@@ -39,10 +55,81 @@ const LABELS: Record<HjcOrderStatusKey, string> = {
|
||||
refund: '已退款'
|
||||
}
|
||||
|
||||
/** 这张单是不是**超时取消**的(而不是买家自己取消的) */
|
||||
export function hjcIsTimeoutCancelled(o?: HjcOrderLike | null): boolean {
|
||||
return hjcOrderStatusKey(o) === 'cancel' && o?.cancelSource === HJC_CANCEL_SOURCE_TIMEOUT
|
||||
}
|
||||
|
||||
export function hjcOrderStatusLabel(o?: HjcOrderLike | null): string {
|
||||
// 超时取消要说清是谁取消的:只说「已取消」,用户和客服都无法自证
|
||||
if (hjcIsTimeoutCancelled(o)) return '已取消(支付超时)'
|
||||
return LABELS[hjcOrderStatusKey(o)]
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析后端下发的时间串。
|
||||
*
|
||||
* **后端的时间格式是 `yyyy-MM-dd HH:mm:ss`**(`application.yml` 的 `date-format` 与
|
||||
* `JacksonConfig` 共同决定:没有 `T`、没有时区)。而 `Date.parse` 对**不带 `T`** 的写法
|
||||
* 在 **iOS Safari / 微信内置浏览器**上返回 `NaN`——那恰恰是本功能最主要的那一端
|
||||
* (公众号 iOS)。后果是倒计时不显示、「到点锁页面」静默失效,而且不报任何错。
|
||||
*
|
||||
* 所以统一在这里把空格换成 `T`:ES 规范的 ISO 无时区写法按**本地时间**解析,
|
||||
* 与服务端的 `GMT+8` 一致。返回 null 表示**不知道**(解析不出来),
|
||||
* 调用方必须按「不显示倒计时、不锁页面」处理——拿一个假值去锁死收银台更糟。
|
||||
*/
|
||||
export function hjcParseServerTime(raw?: string | null): number | null {
|
||||
if (!raw) return null
|
||||
const text = String(raw).trim()
|
||||
if (!text) return null
|
||||
const normalized = text.includes('T') ? text : text.replace(' ', 'T')
|
||||
const ms = Date.parse(normalized)
|
||||
return Number.isNaN(ms) ? null : ms
|
||||
}
|
||||
|
||||
/** 支付时限到期时刻的毫秒数;后端没下发或解析不出来时返回 `null` */
|
||||
export function hjcPayDeadlineMs(o?: HjcOrderLike | null): number | null {
|
||||
return hjcParseServerTime(o?.payDeadline)
|
||||
}
|
||||
|
||||
/** 收银台倒计时剩余毫秒;没有到期时刻时返回 null,已归零时返回 0(不是负数) */
|
||||
export function hjcPayRemainMs(o?: HjcOrderLike | null, now: number = Date.now()): number | null {
|
||||
const deadline = hjcPayDeadlineMs(o)
|
||||
return deadline == null ? null : Math.max(0, deadline - now)
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付时限是否已到。
|
||||
*
|
||||
* **到期时刻未知时返回 false**(不是 true):拿不到 `payDeadline` 就不该把按钮藏起来,
|
||||
* 那种"因为不知道所以当成过期"的写法会把正常订单的操作入口一并藏掉。
|
||||
*/
|
||||
export function hjcIsPayExpired(o?: HjcOrderLike | null, now: number = Date.now()): boolean {
|
||||
const deadline = hjcPayDeadlineMs(o)
|
||||
return deadline != null && now >= deadline
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面用的完整判定:这张单**能操作**(去支付 / 取消)**且支付时限未到**。
|
||||
*
|
||||
* 为什么必须共享而不是各页面自己拼:`hjcCanActOnOrder` 只管状态,时限要当前时刻,
|
||||
* 于是每个页面都会顺手写一遍 `canAct`——两处一分叉,就会出现"列表里不能点、详情里能点"。
|
||||
* `hjc-h5` 那边导出的是 `canActOn`,两侧是同一套定义(见本文件头注释)。
|
||||
*/
|
||||
export function hjcCanActOn(o?: HjcOrderLike | null, now: number = Date.now()): boolean {
|
||||
return hjcCanActOnOrder(o) && !hjcIsPayExpired(o, now)
|
||||
}
|
||||
|
||||
/** 倒计时文案。超过一小时给 `h:mm:ss`,免得显示成 `90:00` 那样难读 */
|
||||
export function hjcFormatCountdown(ms: number): string {
|
||||
const total = Math.max(0, Math.floor(ms / 1000))
|
||||
const h = Math.floor(total / 3600)
|
||||
const m = Math.floor((total % 3600) / 60)
|
||||
const s = total % 60
|
||||
const pad = (n: number) => String(n).padStart(2, '0')
|
||||
return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}`
|
||||
}
|
||||
|
||||
/** 状态徽标的 Tailwind 配色 */
|
||||
export function hjcOrderStatusClass(o?: HjcOrderLike | null): string {
|
||||
switch (hjcOrderStatusKey(o)) {
|
||||
|
||||
Reference in New Issue
Block a user