新增退款待审核角标提醒
This commit is contained in:
@@ -248,6 +248,15 @@ func (h *Handler) ListPendingRefund(c *gin.Context) {
|
||||
response.OK(c, gin.H{"items": items})
|
||||
}
|
||||
|
||||
func (h *Handler) PendingRefundCount(c *gin.Context) {
|
||||
count, err := h.service.PendingRefundCount(c.Request.Context())
|
||||
if err != nil {
|
||||
writeOrderError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{"pending_refund_count": count})
|
||||
}
|
||||
|
||||
func (h *Handler) adminAction(c *gin.Context, fn func(context.Context, uint64, uint64, AdminActionRequest, AuditMeta) error, okData gin.H) {
|
||||
adminID, ok := currentAdminID(c)
|
||||
if !ok {
|
||||
|
||||
@@ -2,6 +2,8 @@ package order
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
)
|
||||
|
||||
// ListPendingRefund 查询所有待客服审核退款的订单。
|
||||
@@ -26,3 +28,13 @@ func (r *Repository) ListPendingRefund(ctx context.Context) ([]OrderDTO, error)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// PendingRefundCount 返回待客服审核退款数量,用于后台导航角标。
|
||||
func (r *Repository) PendingRefundCount(ctx context.Context) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.WithContext(ctx).
|
||||
Model(&model.RentalOrder{}).
|
||||
Where("refund_status = ?", refundStatusPendingReview).
|
||||
Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
@@ -351,6 +351,13 @@ func (s *Service) ListPendingRefund(ctx context.Context) ([]OrderDTO, error) {
|
||||
return s.repo.ListPendingRefund(ctx)
|
||||
}
|
||||
|
||||
func (s *Service) PendingRefundCount(ctx context.Context) (int64, error) {
|
||||
if s.repo == nil {
|
||||
return 0, ErrDependencyUnavailable
|
||||
}
|
||||
return s.repo.PendingRefundCount(ctx)
|
||||
}
|
||||
|
||||
func (s *Service) FindForUser(ctx context.Context, userID uint64, orderID uint64) (*OrderDTO, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
|
||||
@@ -620,6 +620,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
|
||||
adminRoutes.POST("/orders/:id/deposit-hold", requirePerm("order:deposit_hold"), orderHandler.AdminHoldDeposit)
|
||||
adminRoutes.POST("/orders/:id/deposit-release", requirePerm("order:deposit_hold"), orderHandler.AdminReleaseDeposit)
|
||||
adminRoutes.GET("/orders/refund-pending", requirePerm("order:close"), orderHandler.ListPendingRefund)
|
||||
adminRoutes.GET("/orders/refund-pending-count", requirePerm("order:close"), orderHandler.PendingRefundCount)
|
||||
adminRoutes.POST("/orders/:id/dispute", requirePerm("dispute:arbitrate"), disputeHandler.AdminCreateByOrder)
|
||||
|
||||
// 管理员线下提号(独立于正常订单流程)
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
||||
|
||||
import { fetchPendingRefundCount } from '@/features/orders/api/orders'
|
||||
import { useAdminSessionStore } from '@/stores/adminSession'
|
||||
|
||||
export const pendingRefundCountChangedEvent = 'pending-refund-count-changed'
|
||||
|
||||
export function usePendingRefundCount(route: RouteLocationNormalizedLoaded) {
|
||||
const adminSession = useAdminSessionStore()
|
||||
const count = ref(0)
|
||||
let timer: number | null = null
|
||||
let requestID = 0
|
||||
|
||||
const label = computed(() => (count.value > 99 ? '99+' : String(count.value)))
|
||||
const canReviewRefund = computed(
|
||||
() => adminSession.isSuperAdmin || adminSession.hasPermission('order:close')
|
||||
)
|
||||
|
||||
function stopPolling() {
|
||||
if (timer === null) return
|
||||
window.clearInterval(timer)
|
||||
timer = null
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
if (!canReviewRefund.value) {
|
||||
count.value = 0
|
||||
return
|
||||
}
|
||||
const currentID = ++requestID
|
||||
try {
|
||||
const nextCount = await fetchPendingRefundCount()
|
||||
if (currentID === requestID) count.value = nextCount
|
||||
} catch {
|
||||
// 角标加载失败时保持当前数字,避免影响后台其他操作。
|
||||
}
|
||||
}
|
||||
|
||||
function startPolling() {
|
||||
stopPolling()
|
||||
void refresh()
|
||||
timer = window.setInterval(refresh, 30_000)
|
||||
}
|
||||
|
||||
function handleVisibilityChange() {
|
||||
if (document.hidden) {
|
||||
stopPolling()
|
||||
return
|
||||
}
|
||||
startPolling()
|
||||
}
|
||||
|
||||
function handleCountChanged() {
|
||||
void refresh()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener(pendingRefundCountChangedEvent, handleCountChanged)
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange)
|
||||
if (!document.hidden) startPolling()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopPolling()
|
||||
window.removeEventListener(pendingRefundCountChangedEvent, handleCountChanged)
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange)
|
||||
})
|
||||
|
||||
watch(canReviewRefund, canReview => {
|
||||
if (canReview && !document.hidden) {
|
||||
startPolling()
|
||||
return
|
||||
}
|
||||
stopPolling()
|
||||
count.value = 0
|
||||
})
|
||||
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
() => void refresh()
|
||||
)
|
||||
|
||||
return {
|
||||
pendingRefundCount: count,
|
||||
pendingRefundLabel: label,
|
||||
refreshPendingRefundCount: refresh,
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import { formatCentWithSymbol } from '@/shared/utils/money'
|
||||
import { orderStatusLabel, handoffStatusLabel } from '@/shared/utils/statusLabels'
|
||||
import { readError } from '@/shared/utils/error'
|
||||
import { adminPath } from '@/shared/utils/adminPath'
|
||||
import { pendingRefundCountChangedEvent } from '@/features/admin/composables/usePendingRefundCount'
|
||||
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
@@ -46,6 +47,7 @@ async function handleApprove(row: Order) {
|
||||
submitting.value = true
|
||||
try {
|
||||
await adminApproveRefund(row.id)
|
||||
window.dispatchEvent(new Event(pendingRefundCountChangedEvent))
|
||||
ElMessage.success(`退款审核通过,${row.order_no} 已发起退款`)
|
||||
await loadOrders()
|
||||
} catch (error) {
|
||||
@@ -75,6 +77,7 @@ async function handleRejectRestore() {
|
||||
submitting.value = true
|
||||
try {
|
||||
await adminRejectRefund(row.id, 'restore')
|
||||
window.dispatchEvent(new Event(pendingRefundCountChangedEvent))
|
||||
ElMessage.success(`退款已驳回,${row.order_no} 已恢复至待交接状态`)
|
||||
rejectVisible.value = false
|
||||
await loadOrders()
|
||||
@@ -100,6 +103,7 @@ async function handleRejectClose() {
|
||||
submitting.value = true
|
||||
try {
|
||||
await adminRejectRefund(row.id, 'close')
|
||||
window.dispatchEvent(new Event(pendingRefundCountChangedEvent))
|
||||
ElMessage.success(`退款已驳回,${row.order_no} 已关闭`)
|
||||
rejectVisible.value = false
|
||||
await loadOrders()
|
||||
|
||||
@@ -509,3 +509,11 @@ export async function fetchPendingRefundOrders() {
|
||||
)
|
||||
return data.data.items
|
||||
}
|
||||
|
||||
export async function fetchPendingRefundCount() {
|
||||
const { data } = await apiClient.get<ApiResponse<{ pending_refund_count: number }>>(
|
||||
'/admin/orders/refund-pending-count',
|
||||
{ silent: true }
|
||||
)
|
||||
return data.data.pending_refund_count
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import { changeAdminPassword, logoutAdmin, updateSupportStatus } from '@/features/admin'
|
||||
import { useAdminNotificationUnreadCount } from '@/features/admin/composables/useAdminNotificationUnreadCount'
|
||||
import { usePendingRefundCount } from '@/features/admin/composables/usePendingRefundCount'
|
||||
import { useAdminSessionStore } from '@/stores/adminSession'
|
||||
import { adminPath, ADMIN_DASHBOARD_PATH, ADMIN_LOGIN_PATH } from '@/shared/utils/adminPath'
|
||||
import { readError } from '@/shared/utils/error'
|
||||
@@ -303,6 +304,8 @@ const hasNotificationPermission = computed(
|
||||
)
|
||||
const { unreadCount: adminNotificationUnreadCount, unreadLabel: adminNotificationUnreadLabel } =
|
||||
useAdminNotificationUnreadCount(route)
|
||||
const { pendingRefundCount, pendingRefundLabel } = usePendingRefundCount(route)
|
||||
const refundReviewPath = adminPath('orders/refund-review')
|
||||
|
||||
const statusLabels: Record<string, string> = {
|
||||
online: '在线',
|
||||
@@ -410,7 +413,15 @@ async function handleForcedPasswordChange() {
|
||||
</template>
|
||||
<el-menu-item v-for="item in group.children" :key="item.to" :index="item.to">
|
||||
<el-icon><component :is="item.icon" /></el-icon>
|
||||
<template #title>{{ item.label }}</template>
|
||||
<template #title>
|
||||
<span class="menu-item-label">{{ item.label }}</span>
|
||||
<em
|
||||
v-if="item.to === refundReviewPath && pendingRefundCount > 0"
|
||||
class="menu-item-badge"
|
||||
>
|
||||
{{ pendingRefundLabel }}
|
||||
</em>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</el-sub-menu>
|
||||
</el-menu>
|
||||
@@ -807,6 +818,40 @@ async function handleForcedPasswordChange() {
|
||||
background-color: #1f2d3d;
|
||||
}
|
||||
|
||||
.menu-item-label {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.menu-item-badge {
|
||||
margin-left: auto;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
padding: 0 5px;
|
||||
border-radius: 9px;
|
||||
background: #f43f5e;
|
||||
color: #ffffff;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
line-height: 18px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
:deep(.el-menu--collapse .menu-item-badge) {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 5px;
|
||||
min-width: 8px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
padding: 0;
|
||||
border: 2px solid #304156;
|
||||
border-radius: 50%;
|
||||
color: transparent;
|
||||
font-size: 0;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
:global(.admin-menu-popper) {
|
||||
--el-menu-bg-color: #304156;
|
||||
--el-menu-text-color: #bfcbd9;
|
||||
|
||||
Reference in New Issue
Block a user