feat(order): 官网「我的订单」列表加状态筛选 tab
加一排 tab(全部/待支付/已完成/已取消)与各状态计数,点一下只看该状态的订单。数据本来就是全量返回(/hjc/order/my 不分页、也没有状态参数),所以在前端筛,切 tab 不重新请求。选中项写进 URL(?status=pending),刷新、分享、浏览器后退都保持;认不出的值回落「全部」而不是筛成空列表。状态口径复用 utils/orderStatus.ts,页面不重写判定。不新增「已退款」tab:标书售卖没有退款(ADR 0005),payStatus=3 只是防御性展示。 Closes .scratch/web-order-status-tabs/issues/01-orders-status-tabs.md
This commit is contained in:
@@ -15,8 +15,35 @@
|
||||
<template v-else>
|
||||
<p v-if="notice" class="mb-4 rounded-md bg-gray-50 px-4 py-3 text-sm text-gray-700">{{ notice }}</p>
|
||||
|
||||
<div v-if="orders.length" class="space-y-4">
|
||||
<div v-for="o in orders" :key="o.id" class="rounded-lg border border-gray-200 p-5">
|
||||
<!--
|
||||
状态筛选 tab。**在前端筛**:`/api/tender/my-orders` 一次返回本企业全部订单
|
||||
(后端 `/hjc/order/my` 不分页、也没有状态参数),为四个 tab 各发一次请求等于把
|
||||
同一份数据拉四遍,切 tab 还要多一次白屏。判定复用共享的 `orderStatus.ts`,
|
||||
页面里**不重写**"什么算待支付"——列表与详情两处各写一份必然分叉。
|
||||
-->
|
||||
<div
|
||||
class="mb-6 flex flex-wrap gap-2 border-b border-gray-200 pb-3"
|
||||
role="group"
|
||||
aria-label="订单状态筛选"
|
||||
>
|
||||
<button
|
||||
v-for="t in STATUS_TABS"
|
||||
:key="t.key"
|
||||
type="button"
|
||||
:aria-pressed="status === t.key"
|
||||
class="rounded-md px-4 py-1.5 text-sm transition-colors"
|
||||
:class="status === t.key ? 'bg-blue-600 text-white' : 'text-gray-600 hover:bg-gray-100'"
|
||||
@click="selectTab(t.key)"
|
||||
>
|
||||
{{ t.label }}
|
||||
<span class="ml-1 text-xs" :class="status === t.key ? 'text-white/80' : 'text-gray-400'">
|
||||
{{ countOf(t.key) }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="filtered.length" class="space-y-4">
|
||||
<div v-for="o in filtered" :key="o.id" class="rounded-lg border border-gray-200 p-5">
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<p class="font-semibold text-gray-900">{{ o.projectName }}</p>
|
||||
@@ -86,7 +113,21 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="loaded" class="py-20 text-center text-gray-500">暂无订单</div>
|
||||
<div v-else-if="loaded" class="py-20 text-center text-gray-500">
|
||||
<p>{{ emptyText }}</p>
|
||||
<!--
|
||||
筛空但确实有单:给一条一步回到「全部」的出路。只留一句「暂无」,
|
||||
用户很难自己判断是"这个状态没有"还是"我的订单全丢了"。
|
||||
-->
|
||||
<button
|
||||
v-if="orders.length && status !== 'all'"
|
||||
type="button"
|
||||
class="mt-4 rounded-md border border-gray-300 px-5 py-1.5 text-sm text-gray-700 hover:bg-gray-50"
|
||||
@click="selectTab('all')"
|
||||
>
|
||||
查看全部订单({{ orders.length }})
|
||||
</button>
|
||||
</div>
|
||||
<SiteLoading v-else />
|
||||
</template>
|
||||
|
||||
@@ -106,9 +147,15 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
HJC_ORDER_FILTER_EMPTY_TEXT,
|
||||
HJC_ORDER_FILTER_TABS,
|
||||
hjcCanActOn,
|
||||
hjcMatchOrderFilter,
|
||||
hjcOrderFilterFromQuery,
|
||||
hjcOrderStatusClass,
|
||||
hjcOrderStatusLabel
|
||||
hjcOrderStatusKey,
|
||||
hjcOrderStatusLabel,
|
||||
type HjcOrderFilterKey
|
||||
} from '~/utils/orderStatus'
|
||||
|
||||
const { isLoggedIn, handleAuthCode } = useHjcAuth()
|
||||
@@ -132,6 +179,56 @@ const error = ref('')
|
||||
/** 操作结果提示(取消成功/已支付/未核对上),与 error 分开:它不是错误 */
|
||||
const notice = ref('')
|
||||
|
||||
/**
|
||||
* 当前选中的状态 tab。初值取自 URL(`?status=pending`)——SSR 首屏就按它渲染,
|
||||
* 于是刷新、分享链接、从订单详情页返回都还停在同一个 tab 上。
|
||||
*
|
||||
* 认不出的值由 `hjcOrderFilterFromQuery` 回落成「全部」,**不是**筛成空列表。
|
||||
*/
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const STATUS_TABS = HJC_ORDER_FILTER_TABS
|
||||
const status = ref<HjcOrderFilterKey>(hjcOrderFilterFromQuery(route.query.status))
|
||||
|
||||
/** 各状态的订单数。一次遍历数完,不为每个 tab 各 filter 一遍整个列表 */
|
||||
const statusCounts = computed<Record<string, number>>(() => {
|
||||
const acc: Record<string, number> = {}
|
||||
for (const o of orders.value) {
|
||||
const key = hjcOrderStatusKey(o)
|
||||
acc[key] = (acc[key] || 0) + 1
|
||||
}
|
||||
return acc
|
||||
})
|
||||
|
||||
/** tab 上的计数:`all` 是总数,其余查上面那张表 */
|
||||
function countOf(key: HjcOrderFilterKey) {
|
||||
return key === 'all' ? orders.value.length : statusCounts.value[key] || 0
|
||||
}
|
||||
|
||||
/** 当前 tab 下要显示的订单 */
|
||||
const filtered = computed(() => orders.value.filter((o) => hjcMatchOrderFilter(o, status.value)))
|
||||
|
||||
/** 空列表文案随 tab 变化(筛到「已完成」却只写「暂无订单」,用户分不清是没单还是丢了单) */
|
||||
const emptyText = computed(() => HJC_ORDER_FILTER_EMPTY_TEXT[status.value])
|
||||
|
||||
/**
|
||||
* 切 tab:先改本地状态(立刻重渲染),再把选中项同步进 URL。
|
||||
*
|
||||
* 用 `replace` 而不是 `push`——点四个 tab 就要按四次「后退」才回得到上一页。
|
||||
* `all` 不进 URL:干净地址本身就表示"不筛",不必留一个 `?status=all`。
|
||||
*/
|
||||
function selectTab(key: HjcOrderFilterKey) {
|
||||
if (status.value === key) return
|
||||
status.value = key
|
||||
const query = { ...route.query } as Record<string, string | undefined>
|
||||
if (key === 'all') {
|
||||
delete query.status
|
||||
} else {
|
||||
query.status = key
|
||||
}
|
||||
router.replace({ query })
|
||||
}
|
||||
|
||||
/** 正在确认取消的那张订单 + 弹窗开合 */
|
||||
const cancelTarget = shallowRef<any | null>(null)
|
||||
const cancelDialogOpen = ref(false)
|
||||
|
||||
Reference in New Issue
Block a user