fix: 修复订单列表不能查看的问题
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="page" :style="{ paddingTop: navBarHeight }">
|
<view class="page" :style="{ '--status-bar': statusBarHeight + 'px' }">
|
||||||
<CustomNavBar title="订单管理" show-back />
|
<CustomNavBar title="订单管理" show-back />
|
||||||
|
|
||||||
<!-- Summary stats bar -->
|
<!-- Summary stats bar -->
|
||||||
@@ -106,14 +106,14 @@
|
|||||||
</view>
|
</view>
|
||||||
<view class="info-right">
|
<view class="info-right">
|
||||||
<text class="info-label">下单时间</text>
|
<text class="info-label">下单时间</text>
|
||||||
<text class="info-value">{{ formatDate(order.createdAt) }}</text>
|
<text class="info-value">{{ formatDateTime(order.createdAt) }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- Paid time if available -->
|
<!-- Paid time if available -->
|
||||||
<view v-if="order.paidAt && order.status === OrderStatus.PAID" class="info-row">
|
<view v-if="order.paidAt && order.status === OrderStatus.PAID" class="info-row">
|
||||||
<text class="info-label">支付时间</text>
|
<text class="info-label">支付时间</text>
|
||||||
<text class="info-value">{{ formatDate(order.paidAt) }}</text>
|
<text class="info-value">{{ formatDateTime(order.paidAt) }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -133,19 +133,21 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import CustomNavBar from '../../components/CustomNavBar.vue'
|
import CustomNavBar from '../../components/CustomNavBar.vue'
|
||||||
import { getSystemLayout } from '../../utils/system'
|
|
||||||
import { useAdminStore } from '../../stores/admin'
|
import { useAdminStore } from '../../stores/admin'
|
||||||
import { formatPrice, formatDate } from '../../utils/format'
|
import { formatPrice, formatDateTime } from '../../utils/format'
|
||||||
import { OrderStatus } from '@mp-pilates/shared'
|
import { OrderStatus } from '@mp-pilates/shared'
|
||||||
import type { OrderWithDetails } from '@mp-pilates/shared'
|
import type { OrderWithDetails } from '@mp-pilates/shared'
|
||||||
|
|
||||||
const adminStore = useAdminStore()
|
const adminStore = useAdminStore()
|
||||||
|
|
||||||
const navBarHeight = ref('64px')
|
// 动态计算顶部模块高度
|
||||||
|
const statusBarHeight = ref(0)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
navBarHeight.value = `${getSystemLayout().navBarHeight}px`
|
const windowInfo = uni.getWindowInfo()
|
||||||
|
statusBarHeight.value = windowInfo.statusBarHeight ?? 20
|
||||||
})
|
})
|
||||||
|
|
||||||
const filters = [
|
const filters = [
|
||||||
@@ -165,6 +167,21 @@ const totalCount = ref<number | null>(null)
|
|||||||
const paidCount = ref<number | null>(null)
|
const paidCount = ref<number | null>(null)
|
||||||
const pendingCount = ref<number | null>(null)
|
const pendingCount = ref<number | null>(null)
|
||||||
|
|
||||||
|
// 每个 tab 单独缓存数据
|
||||||
|
const orderCache: Record<string, { items: OrderWithDetails[]; total: number; page: number; hasMore: boolean }> = {}
|
||||||
|
|
||||||
|
function getCacheKey(filter: string): string {
|
||||||
|
return filter || 'all'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCachedData(filter: string) {
|
||||||
|
return orderCache[getCacheKey(filter)]
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCachedData(filter: string, data: { items: OrderWithDetails[]; total: number; page: number; hasMore: boolean }) {
|
||||||
|
orderCache[getCacheKey(filter)] = data
|
||||||
|
}
|
||||||
|
|
||||||
const LIMIT = 20
|
const LIMIT = 20
|
||||||
|
|
||||||
function statusLabel(s: string) {
|
function statusLabel(s: string) {
|
||||||
@@ -191,23 +208,45 @@ function statusAccentClass(s: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadOrders(reset = false) {
|
async function loadOrders(reset = false) {
|
||||||
|
const filter = activeFilter.value
|
||||||
|
|
||||||
|
// 如果有缓存且是重置(切换tab),直接用缓存数据
|
||||||
|
if (reset) {
|
||||||
|
const cached = getCachedData(filter)
|
||||||
|
if (cached) {
|
||||||
|
orders.value = [...cached.items]
|
||||||
|
hasMore.value = cached.hasMore
|
||||||
|
page.value = cached.page
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始加载或下拉刷新,需要请求接口
|
||||||
if (loading.value) return
|
if (loading.value) return
|
||||||
if (reset) page.value = 1
|
if (reset) page.value = 1
|
||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const params: { page: number; limit: number; status?: string } = {
|
const params: { page: number; limit: number; status?: string } = {
|
||||||
page: page.value,
|
page: page.value,
|
||||||
limit: LIMIT,
|
limit: LIMIT,
|
||||||
}
|
}
|
||||||
if (activeFilter.value) params.status = activeFilter.value
|
if (filter) params.status = filter
|
||||||
const result = await adminStore.fetchAdminOrders(params)
|
const result = await adminStore.fetchAdminOrders(params)
|
||||||
if (reset) {
|
|
||||||
orders.value = [...result.items]
|
const newItems = reset ? [...result.items] : [...orders.value, ...result.items]
|
||||||
} else {
|
const newHasMore = newItems.length < result.total
|
||||||
orders.value.push(...result.items)
|
|
||||||
}
|
// 缓存数据
|
||||||
hasMore.value = orders.value.length < result.total
|
setCachedData(filter, {
|
||||||
totalCount.value = result.total
|
items: newItems,
|
||||||
|
total: result.total,
|
||||||
|
page: page.value,
|
||||||
|
hasMore: newHasMore,
|
||||||
|
})
|
||||||
|
|
||||||
|
orders.value = newItems
|
||||||
|
hasMore.value = newHasMore
|
||||||
} catch {
|
} catch {
|
||||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||||
} finally {
|
} finally {
|
||||||
@@ -216,30 +255,79 @@ async function loadOrders(reset = false) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadSummaryCounts() {
|
// 初始加载所有分类的数据
|
||||||
|
async function loadAllFiltersData() {
|
||||||
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const [allResult, paidResult, pendingResult] = await Promise.all([
|
// 并行请求所有分类(第一页数据)
|
||||||
adminStore.fetchAdminOrders({ page: 1, limit: 1 }),
|
const [allResult, paidResult, pendingResult, refundedResult] = await Promise.all([
|
||||||
adminStore.fetchAdminOrders({ page: 1, limit: 1, status: OrderStatus.PAID }),
|
adminStore.fetchAdminOrders({ page: 1, limit: LIMIT }),
|
||||||
adminStore.fetchAdminOrders({ page: 1, limit: 1, status: OrderStatus.PENDING }),
|
adminStore.fetchAdminOrders({ page: 1, limit: LIMIT, status: OrderStatus.PAID }),
|
||||||
|
adminStore.fetchAdminOrders({ page: 1, limit: LIMIT, status: OrderStatus.PENDING }),
|
||||||
|
adminStore.fetchAdminOrders({ page: 1, limit: LIMIT, status: OrderStatus.REFUNDED }),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
// 缓存全部
|
||||||
|
setCachedData('', {
|
||||||
|
items: [...allResult.items],
|
||||||
|
total: allResult.total,
|
||||||
|
page: 1,
|
||||||
|
hasMore: allResult.items.length < allResult.total,
|
||||||
|
})
|
||||||
totalCount.value = allResult.total
|
totalCount.value = allResult.total
|
||||||
|
|
||||||
|
// 缓存已支付
|
||||||
|
setCachedData(OrderStatus.PAID, {
|
||||||
|
items: [...paidResult.items],
|
||||||
|
total: paidResult.total,
|
||||||
|
page: 1,
|
||||||
|
hasMore: paidResult.items.length < paidResult.total,
|
||||||
|
})
|
||||||
paidCount.value = paidResult.total
|
paidCount.value = paidResult.total
|
||||||
|
|
||||||
|
// 缓存待支付
|
||||||
|
setCachedData(OrderStatus.PENDING, {
|
||||||
|
items: [...pendingResult.items],
|
||||||
|
total: pendingResult.total,
|
||||||
|
page: 1,
|
||||||
|
hasMore: pendingResult.items.length < pendingResult.total,
|
||||||
|
})
|
||||||
pendingCount.value = pendingResult.total
|
pendingCount.value = pendingResult.total
|
||||||
|
|
||||||
|
// 缓存已退款
|
||||||
|
setCachedData(OrderStatus.REFUNDED, {
|
||||||
|
items: [...refundedResult.items],
|
||||||
|
total: refundedResult.total,
|
||||||
|
page: 1,
|
||||||
|
hasMore: refundedResult.items.length < refundedResult.total,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 设置当前 tab 的数据
|
||||||
|
orders.value = [...allResult.items]
|
||||||
|
hasMore.value = allResult.items.length < allResult.total
|
||||||
} catch {
|
} catch {
|
||||||
// non-critical, ignore
|
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
refreshing.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectFilter(value: string) {
|
function selectFilter(value: string) {
|
||||||
activeFilter.value = value
|
activeFilter.value = value
|
||||||
totalCount.value = null
|
// 切换 tab 直接从缓存读取
|
||||||
loadOrders(true)
|
const cached = getCachedData(value)
|
||||||
|
if (cached) {
|
||||||
|
orders.value = [...cached.items]
|
||||||
|
hasMore.value = cached.hasMore
|
||||||
|
page.value = cached.page
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onRefresh() {
|
async function onRefresh() {
|
||||||
refreshing.value = true
|
refreshing.value = true
|
||||||
await Promise.all([loadOrders(true), loadSummaryCounts()])
|
// 下拉刷新重新请求所有分类的数据
|
||||||
|
await loadAllFiltersData()
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadMore() {
|
function loadMore() {
|
||||||
@@ -249,8 +337,7 @@ function loadMore() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadOrders(true)
|
loadAllFiltersData()
|
||||||
loadSummaryCounts()
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -266,13 +353,18 @@ onMounted(() => {
|
|||||||
|
|
||||||
/* ── Stats bar ──────────────────────────────── */
|
/* ── Stats bar ──────────────────────────────── */
|
||||||
.stats-bar {
|
.stats-bar {
|
||||||
|
position: fixed;
|
||||||
|
top: calc(var(--status-bar) + 44px);
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
height: 96rpx;
|
||||||
background: #FFFFFF;
|
background: #FFFFFF;
|
||||||
padding: 28rpx 0;
|
padding: 0;
|
||||||
margin: 0;
|
|
||||||
border-bottom: 1rpx solid rgba(180, 160, 130, 0.2);
|
border-bottom: 1rpx solid rgba(180, 160, 130, 0.2);
|
||||||
box-shadow: 0 2rpx 12rpx rgba(180, 160, 130, 0.08);
|
box-shadow: 0 2rpx 12rpx rgba(180, 160, 130, 0.08);
|
||||||
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-item {
|
.stat-item {
|
||||||
@@ -309,9 +401,13 @@ onMounted(() => {
|
|||||||
|
|
||||||
/* ── Filter pills ───────────────────────────── */
|
/* ── Filter pills ───────────────────────────── */
|
||||||
.filter-wrap {
|
.filter-wrap {
|
||||||
|
position: fixed;
|
||||||
|
top: calc(var(--status-bar) + 92px);
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
background: #FAF8F5;
|
background: #FAF8F5;
|
||||||
border-bottom: 1rpx solid rgba(180, 160, 130, 0.15);
|
border-bottom: 1rpx solid rgba(180, 160, 130, 0.15);
|
||||||
flex-shrink: 0;
|
z-index: 99;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-scroll { overflow: hidden; }
|
.filter-scroll { overflow: hidden; }
|
||||||
@@ -364,7 +460,11 @@ onMounted(() => {
|
|||||||
|
|
||||||
/* ── List ───────────────────────────────────── */
|
/* ── List ───────────────────────────────────── */
|
||||||
.list-scroll {
|
.list-scroll {
|
||||||
flex: 1;
|
position: fixed;
|
||||||
|
top: calc(var(--status-bar) + 144px);
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -120,7 +120,10 @@ export const useAdminStore = defineStore('admin', () => {
|
|||||||
limit?: number
|
limit?: number
|
||||||
status?: string
|
status?: string
|
||||||
}): Promise<PaginatedData<OrderWithDetails>> {
|
}): Promise<PaginatedData<OrderWithDetails>> {
|
||||||
return get<PaginatedData<OrderWithDetails>>('/admin/orders', params)
|
console.log('[admin] fetchAdminOrders params:', params)
|
||||||
|
const result = await get<PaginatedData<OrderWithDetails>>('/admin/orders', params)
|
||||||
|
console.log('[admin] fetchAdminOrders result:', result)
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Bookings ─────────────────────────────────────────────────────
|
// ── Bookings ─────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -135,14 +135,15 @@ export function getStockPercent(soldCount: number, totalStock: number): string {
|
|||||||
return `${Math.min(100, getStockRatio(soldCount, totalStock) * 100)}%`
|
return `${Math.min(100, getStockRatio(soldCount, totalStock) * 100)}%`
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 格式化日期时间为 MM-DD HH:mm */
|
/** 格式化日期时间为 MM-DD HH:mm:ss */
|
||||||
export function formatDateTime(dateStr: string): string {
|
export function formatDateTime(dateStr: string): string {
|
||||||
const d = new Date(dateStr)
|
const d = new Date(dateStr)
|
||||||
const month = String(d.getMonth() + 1).padStart(2, '0')
|
const month = String(d.getMonth() + 1).padStart(2, '0')
|
||||||
const day = String(d.getDate()).padStart(2, '0')
|
const day = String(d.getDate()).padStart(2, '0')
|
||||||
const hour = String(d.getHours()).padStart(2, '0')
|
const hour = String(d.getHours()).padStart(2, '0')
|
||||||
const min = String(d.getMinutes()).padStart(2, '0')
|
const min = String(d.getMinutes()).padStart(2, '0')
|
||||||
return `${month}-${day} ${hour}:${min}`
|
const sec = String(d.getSeconds()).padStart(2, '0')
|
||||||
|
return `${month}-${day} ${hour}:${min}:${sec}`
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 格式化 Date 为 YYYY-MM-DD(本地时间,用于 picker) */
|
/** 格式化 Date 为 YYYY-MM-DD(本地时间,用于 picker) */
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export interface CreateOrderResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface PaginatedOrders<T> {
|
export interface PaginatedOrders<T> {
|
||||||
data: T[]
|
items: T[]
|
||||||
total: number
|
total: number
|
||||||
page: number
|
page: number
|
||||||
limit: number
|
limit: number
|
||||||
@@ -176,7 +176,7 @@ export class PaymentService {
|
|||||||
])
|
])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: data.map((o) => ({ ...o, cardType: { ...o.cardType } })),
|
items: data.map((o) => ({ ...o, cardType: { ...o.cardType } })),
|
||||||
total,
|
total,
|
||||||
page,
|
page,
|
||||||
limit,
|
limit,
|
||||||
@@ -207,8 +207,9 @@ export class PaymentService {
|
|||||||
this.prisma.order.count({ where }),
|
this.prisma.order.count({ where }),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
this.logger.log(`getAllOrders: page=${page}, limit=${limit}, status=${status}, count=${total}`)
|
||||||
return {
|
return {
|
||||||
data: data.map((o) => ({
|
items: data.map((o) => ({
|
||||||
...o,
|
...o,
|
||||||
cardType: { ...o.cardType },
|
cardType: { ...o.cardType },
|
||||||
user: { ...o.user },
|
user: { ...o.user },
|
||||||
|
|||||||
Reference in New Issue
Block a user