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)
|
||||
|
||||
@@ -154,3 +154,66 @@ export function hjcCanActOnOrder(o?: HjcOrderLike | null): boolean {
|
||||
if (!o) return false
|
||||
return o.payStatus === 0 && o.orderStatus !== 2
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ 列表筛选
|
||||
|
||||
/**
|
||||
* 订单列表的筛选键:`all`(不筛)+ 四个状态键。
|
||||
*
|
||||
* `all` 刻意**不**并进 `HjcOrderStatusKey`,而是单独一层联合:一旦把 `'all'` 混进状态键,
|
||||
* `hjcOrderStatusKey()` 的返回值就不再是"这张单真的处于什么状态",每个 `switch`
|
||||
* 都得补一个永远走不到的 `case 'all'`,漏补时 TS 还会以为已经穷尽。
|
||||
*/
|
||||
export type HjcOrderFilterKey = 'all' | HjcOrderStatusKey
|
||||
|
||||
/**
|
||||
* 列表页的筛选 tab(数组顺序即展示顺序)。
|
||||
*
|
||||
* **没有「已退款」**:标书售卖没有退款功能,`payStatus === 3` 只是防御性展示
|
||||
* (工作区根目录 `docs/adr/0005-no-refund-for-tender-sales.md`)。挂一个永远是空的 tab,
|
||||
* 只会让人追问"我的退款去哪了";真出现退款单时它在「全部」里照样找得到。
|
||||
*
|
||||
* 标签必须与 `hjc-h5/src/pages/order/list.vue` 的 tabs 保持一致——同本文件头注释的口径。
|
||||
*/
|
||||
export const HJC_ORDER_FILTER_TABS: ReadonlyArray<{ key: HjcOrderFilterKey; label: string }> = [
|
||||
{ key: 'all', label: '全部' },
|
||||
{ key: 'pending', label: '待支付' },
|
||||
{ key: 'done', label: '已完成' },
|
||||
{ key: 'cancel', label: '已取消' }
|
||||
]
|
||||
|
||||
/**
|
||||
* 每个筛选键的空列表文案。
|
||||
*
|
||||
* 必须逐键给全(`Record<HjcOrderFilterKey, string>` 会盯着):筛到「已完成」却看到一句
|
||||
* 「暂无订单」,用户无从判断是"这单没有"还是"我的单全丢了"。`refund` 目前没有对应 tab,
|
||||
* 但类型里还在,留着文案免得将来加 tab 时漏掉。
|
||||
*/
|
||||
export const HJC_ORDER_FILTER_EMPTY_TEXT: Record<HjcOrderFilterKey, string> = {
|
||||
all: '暂无订单',
|
||||
pending: '暂无待支付订单',
|
||||
done: '暂无已完成订单',
|
||||
cancel: '暂无已取消订单',
|
||||
refund: '暂无已退款订单'
|
||||
}
|
||||
|
||||
/**
|
||||
* URL 上的 `status` 查询参数 → 筛选键。
|
||||
*
|
||||
* 认不出来的一律回落 `all`,**不是**回落成"空筛选":把非法参数当成一个筛不出东西的条件,
|
||||
* 用户看到的就是"一张单都没有"且没有任何提示,只会当成丢单。
|
||||
* 容忍数组(`?status=a&status=b` 取第一个)与缺失。
|
||||
*/
|
||||
export function hjcOrderFilterFromQuery(raw: unknown): HjcOrderFilterKey {
|
||||
const value = Array.isArray(raw) ? raw[0] : raw
|
||||
const key = typeof value === 'string' ? value : ''
|
||||
return HJC_ORDER_FILTER_TABS.some((t) => t.key === key) ? (key as HjcOrderFilterKey) : 'all'
|
||||
}
|
||||
|
||||
/** 这张订单是否命中筛选键(`all` 恒命中,其余按状态键判定) */
|
||||
export function hjcMatchOrderFilter(
|
||||
o: HjcOrderLike | null | undefined,
|
||||
key: HjcOrderFilterKey
|
||||
): boolean {
|
||||
return key === 'all' || hjcOrderStatusKey(o) === key
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user