feat: 退款月度汇总支持点击查看明细
月度合计移入汇总表最后一行,汇总数字支持按平台和月份查看明细。
This commit is contained in:
parent
026733e919
commit
c7654fe74d
@ -7040,7 +7040,6 @@ class Pages extends Controller
|
||||
$pending = false;
|
||||
$indexPending = false;
|
||||
$pageLabel = $pendingConfirmation ? '退款待确认' : '售后(退货退款)';
|
||||
$refundMonthlySummary = [];
|
||||
$refundPlatformMonthlySummary = ['months' => [], 'platforms' => []];
|
||||
|
||||
try {
|
||||
@ -7065,7 +7064,6 @@ class Pages extends Controller
|
||||
$defaultTo,
|
||||
),
|
||||
);
|
||||
$refundMonthlySummary = $refundPlatformMonthlySummary['months'] ?? [];
|
||||
} else {
|
||||
$result = $this->nonJingxiRefunds->paginate($filters, $page, $limit);
|
||||
$rows = new LengthAwarePaginator(
|
||||
@ -7096,7 +7094,6 @@ class Pages extends Controller
|
||||
'refundPageLabel' => $pageLabel,
|
||||
'refundPendingConfirmationMode' => $pendingConfirmation,
|
||||
'refundOnlineStatusLabel' => $pendingConfirmation ? '线上状态' : '店铺状态',
|
||||
'refundMonthlySummary' => $refundMonthlySummary,
|
||||
'refundPlatformMonthlySummary' => $refundPlatformMonthlySummary,
|
||||
'refundShowExport' => ! $pendingConfirmation
|
||||
|| BusinessOperationPermissions::can(
|
||||
@ -7148,20 +7145,29 @@ class Pages extends Controller
|
||||
public function refundPendingMonthDetails(Request $request)
|
||||
{
|
||||
$month = trim((string) $request->query('month', ''));
|
||||
if (preg_match('/^\d{4}-\d{2}$/', $month) !== 1) {
|
||||
$allMonths = $month === '' || $month === 'all';
|
||||
if (! $allMonths && preg_match('/^\d{4}-\d{2}$/', $month) !== 1) {
|
||||
return response()->json(['message' => '退款月份格式不正确'], 422);
|
||||
}
|
||||
|
||||
$start = Carbon::createFromFormat('!Y-m', $month)->startOfMonth();
|
||||
$today = now()->startOfDay();
|
||||
if ($start->format('Y-m') < '2026-10' || $start->greaterThan($today)) {
|
||||
return response()->json(['message' => '退款月份超出可查询范围'], 422);
|
||||
}
|
||||
|
||||
$end = $start->copy()->endOfMonth()->startOfDay();
|
||||
if ($end->greaterThan($today)) {
|
||||
if ($allMonths) {
|
||||
$monthKey = 'all';
|
||||
$start = Carbon::createFromFormat('!Y-m-d', '2026-10-01')->startOfDay();
|
||||
$end = $today;
|
||||
} else {
|
||||
$monthKey = $month;
|
||||
$start = Carbon::createFromFormat('!Y-m', $month)->startOfMonth();
|
||||
if ($start->format('Y-m') < '2026-10' || $start->greaterThan($today)) {
|
||||
return response()->json(['message' => '退款月份超出可查询范围'], 422);
|
||||
}
|
||||
|
||||
$end = $start->copy()->endOfMonth()->startOfDay();
|
||||
if ($end->greaterThan($today)) {
|
||||
$end = $today;
|
||||
}
|
||||
}
|
||||
$platform = mb_substr(trim((string) $request->query('platform', '')), 0, 120);
|
||||
$page = max((int) $request->query('page', 1), 1);
|
||||
$limit = min(max((int) $request->query('limit', 25), 1), 50);
|
||||
$filters = [
|
||||
@ -7171,6 +7177,7 @@ class Pages extends Controller
|
||||
'waybill_no' => '',
|
||||
'shop_name' => '',
|
||||
'sku_id' => '',
|
||||
'platform' => $platform,
|
||||
];
|
||||
|
||||
try {
|
||||
@ -7180,8 +7187,9 @@ class Pages extends Controller
|
||||
return response()->json(['message' => '退款待确认镜像尚未就绪'], 503);
|
||||
}
|
||||
|
||||
$platformKey = sha1($platform);
|
||||
$details = \Illuminate\Support\Facades\Cache::remember(
|
||||
"refund-pending.month-details.v1:{$month}:{$page}:{$limit}",
|
||||
"refund-pending.details.v2:{$monthKey}:{$end->toDateString()}:{$platformKey}:{$page}:{$limit}",
|
||||
now()->addMinutes(5),
|
||||
function () use ($filters, $page, $limit): array {
|
||||
$result = $this->nonJingxiRefunds->pagePendingConfirmation($filters, $page, $limit);
|
||||
@ -7215,7 +7223,8 @@ class Pages extends Controller
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'month' => $start->format('Y-m'),
|
||||
'month' => $monthKey,
|
||||
'platform' => $platform,
|
||||
'page' => $page,
|
||||
'limit' => $limit,
|
||||
'has_more' => $details['has_more'],
|
||||
|
||||
@ -48,6 +48,9 @@ class NonJingxiRefundMirrorRepository
|
||||
|
||||
private ?array $excludedShopIds = null;
|
||||
|
||||
/** @var array<string, list<string>>|null shop_id => historical shop_site values */
|
||||
private ?array $supplementShopSites = null;
|
||||
|
||||
private ?string $supplementDateIndex = null;
|
||||
|
||||
public function __construct(
|
||||
@ -158,7 +161,7 @@ class NonJingxiRefundMirrorRepository
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{from:string,to:string,order_no:string,waybill_no:string,shop_name:string,sku_id:string} $filters
|
||||
* @param array{from:string,to:string,order_no:string,waybill_no:string,shop_name:string,sku_id:string,platform?:string} $filters
|
||||
* @return array{data:list<object>,total:int}
|
||||
*/
|
||||
public function paginate(array $filters, int $page, int $perPage): array
|
||||
@ -180,7 +183,7 @@ class NonJingxiRefundMirrorRepository
|
||||
/**
|
||||
* 售后状态为待确认、且线上退款已成功的记录。
|
||||
*
|
||||
* @param array{from:string,to:string,order_no:string,waybill_no:string,shop_name:string,sku_id:string} $filters
|
||||
* @param array{from:string,to:string,order_no:string,waybill_no:string,shop_name:string,sku_id:string,platform?:string} $filters
|
||||
* @return array{data:list<object>,total:int}
|
||||
*/
|
||||
public function paginatePendingConfirmation(array $filters, int $page, int $perPage): array
|
||||
@ -196,7 +199,7 @@ class NonJingxiRefundMirrorRepository
|
||||
/**
|
||||
* 按页读取待确认明细,不做全月 COUNT;has_more 由下一页键探测。
|
||||
*
|
||||
* @param array{from:string,to:string,order_no:string,waybill_no:string,shop_name:string,sku_id:string} $filters
|
||||
* @param array{from:string,to:string,order_no:string,waybill_no:string,shop_name:string,sku_id:string,platform?:string} $filters
|
||||
* @return array{data:list<object>,has_more:bool}
|
||||
*/
|
||||
public function pagePendingConfirmation(array $filters, int $page, int $perPage): array
|
||||
@ -452,10 +455,9 @@ class NonJingxiRefundMirrorRepository
|
||||
if ($supplement) {
|
||||
// A shop may have several historical rows. An ambiguous platform is
|
||||
// left unknown, and must never multiply the refund rows.
|
||||
$this->excludedShopIds ??= $this->connection->table('ops_jst_shop_info')
|
||||
->get(['shop_id', 'shop_site'])->groupBy('shop_id')
|
||||
->filter(function ($rows): bool {
|
||||
$sites = $rows->pluck('shop_site')->filter()->unique();
|
||||
$this->excludedShopIds ??= collect($this->supplementShopSites())
|
||||
->filter(function (array $sites): bool {
|
||||
$sites = collect($sites)->filter()->unique();
|
||||
$site = (string) $sites->first();
|
||||
return $sites->count() === 1 && (str_contains($site, '京喜') || str_contains(strtolower($site), 'jingxi'));
|
||||
})->keys()->map('strval')->all();
|
||||
@ -484,6 +486,7 @@ class NonJingxiRefundMirrorRepository
|
||||
});
|
||||
});
|
||||
}
|
||||
$this->applyPlatformFilter($query, $filters, $supplement);
|
||||
$this->applyFilters($query, $filters);
|
||||
if ($filters['sku_id'] !== '') {
|
||||
if ($withItems) {
|
||||
@ -640,7 +643,7 @@ class NonJingxiRefundMirrorRepository
|
||||
return implode('|', [$row->_source_rank, $row->_head_id, $row->_item_id, $row->_source_id]);
|
||||
}
|
||||
|
||||
/** @param array{from:string,to:string,order_no:string,waybill_no:string,shop_name:string,sku_id:string} $filters */
|
||||
/** @param array{from:string,to:string,order_no:string,waybill_no:string,shop_name:string,sku_id:string,platform?:string} $filters */
|
||||
private function applyFilters(Builder $query, array $filters): void
|
||||
{
|
||||
$query->where('r.as_date', '>=', $filters['from'] . ' 00:00:00')
|
||||
@ -665,8 +668,84 @@ class NonJingxiRefundMirrorRepository
|
||||
}
|
||||
}
|
||||
|
||||
private function applyPlatformFilter(Builder $query, array $filters, bool $supplement): void
|
||||
{
|
||||
$target = trim((string) ($filters['platform'] ?? ''));
|
||||
if ($target === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $supplement) {
|
||||
if ($target === '待确认') {
|
||||
$query->where(function (Builder $platform): void {
|
||||
$platform->whereNull('r.shop_site')
|
||||
->orWhere('r.shop_site', '')
|
||||
->orWhere('r.shop_site', '待确认');
|
||||
});
|
||||
} else {
|
||||
$query->where('r.shop_site', $target);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$matchedShopIds = [];
|
||||
$knownShopIds = [];
|
||||
foreach ($this->supplementShopSites() as $shopId => $sites) {
|
||||
$sites = array_values(array_unique(array_filter(array_map(
|
||||
static fn (mixed $site): string => trim((string) $site),
|
||||
$sites,
|
||||
))));
|
||||
$site = count($sites) === 1 ? $sites[0] : '待确认';
|
||||
if ($site === $target) {
|
||||
$matchedShopIds[] = (string) $shopId;
|
||||
}
|
||||
if ($site !== '待确认') {
|
||||
$knownShopIds[] = (string) $shopId;
|
||||
}
|
||||
}
|
||||
|
||||
if ($target === '待确认') {
|
||||
if ($knownShopIds !== []) {
|
||||
$query->where(function (Builder $platform) use ($knownShopIds): void {
|
||||
$platform->whereNull('r.shop_id')
|
||||
->orWhereNotIn('r.shop_id', $knownShopIds);
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($matchedShopIds === []) {
|
||||
$query->whereRaw('1 = 0');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$query->whereIn('r.shop_id', $matchedShopIds);
|
||||
}
|
||||
|
||||
/** @return array<string, list<string>> */
|
||||
private function supplementShopSites(): array
|
||||
{
|
||||
if ($this->supplementShopSites !== null) {
|
||||
return $this->supplementShopSites;
|
||||
}
|
||||
|
||||
$this->supplementShopSites = [];
|
||||
foreach ($this->connection->table('ops_jst_shop_info')->get(['shop_id', 'shop_site']) as $row) {
|
||||
$shopId = trim((string) ($row->shop_id ?? ''));
|
||||
if ($shopId === '') {
|
||||
continue;
|
||||
}
|
||||
$this->supplementShopSites[$shopId][] = (string) ($row->shop_site ?? '');
|
||||
}
|
||||
|
||||
return $this->supplementShopSites;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{from:string,to:string,order_no:string,waybill_no:string,shop_name:string,sku_id:string} $filters
|
||||
* @param array{from:string,to:string,order_no:string,waybill_no:string,shop_name:string,sku_id:string,platform?:string} $filters
|
||||
* @return array{from:string,to:string,order_no:string,waybill_no:string,shop_name:string,sku_id:string,status:string,shop_status:string}
|
||||
*/
|
||||
private function pendingConfirmationFilters(array $filters): array
|
||||
|
||||
@ -218,61 +218,92 @@
|
||||
.refund-platform-summary__table td:first-child { text-align: left; }
|
||||
.refund-platform-summary__table th { position: sticky; top: 0; z-index: 1; background: var(--vip-content-accent-soft, #f7f0de); font-weight: 600; }
|
||||
.refund-platform-summary__table td:first-child { font-weight: 600; }
|
||||
.refund-month-group + .refund-month-group { border-top: 1px solid var(--vip-border, #d8d2c7); }
|
||||
.refund-month-toggle {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
min-height: 3.25rem;
|
||||
grid-template-columns: minmax(7rem, 1fr) auto auto;
|
||||
.refund-summary-count {
|
||||
display: inline-flex;
|
||||
min-width: 2.75rem;
|
||||
min-height: 1.65rem;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 0.65rem 1rem;
|
||||
justify-content: flex-end;
|
||||
padding: 0 0.35rem;
|
||||
border: 0;
|
||||
background: var(--vip-surface, #ffffff);
|
||||
color: var(--vip-content-ink, #171717);
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
color: var(--vip-content-accent-dark, #856632);
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
font-variant-numeric: tabular-nums;
|
||||
text-decoration: underline dotted;
|
||||
text-underline-offset: 3px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.refund-month-toggle:hover { background: #fbf8f0; }
|
||||
.refund-month-toggle:focus-visible { outline: 2px solid var(--vip-content-accent-dark, #856632); outline-offset: -2px; }
|
||||
.refund-month-toggle__month { font-size: 0.8125rem; font-weight: 600; }
|
||||
.refund-month-toggle__count { color: var(--vip-content-accent-dark, #856632); font-size: 0.8125rem; font-weight: 600; }
|
||||
.refund-month-toggle__icon { transition: transform 150ms ease; }
|
||||
.refund-month-toggle[aria-expanded="true"] .refund-month-toggle__icon { transform: rotate(180deg); }
|
||||
.refund-month-panel {
|
||||
padding: 0 1rem 1rem;
|
||||
border-top: 1px solid #ece8df;
|
||||
.refund-summary-count:hover,
|
||||
.refund-summary-count[aria-pressed="true"] { background: var(--vip-content-accent-soft, #f7f0de); text-decoration-style: solid; }
|
||||
.refund-summary-count:focus-visible { outline: 2px solid var(--vip-content-accent-dark, #856632); outline-offset: 1px; }
|
||||
.refund-platform-summary__table .refund-platform-summary__total-row th,
|
||||
.refund-platform-summary__table .refund-platform-summary__total-row td {
|
||||
position: sticky;
|
||||
top: auto;
|
||||
bottom: 0;
|
||||
z-index: 2;
|
||||
border-top: 2px solid var(--vip-content-accent, #b9954f);
|
||||
background: #fffaf0;
|
||||
font-weight: 700;
|
||||
}
|
||||
.refund-summary-detail {
|
||||
padding: 1rem;
|
||||
border-top: 1px solid var(--vip-border, #d8d2c7);
|
||||
background: #fbfaf6;
|
||||
}
|
||||
.refund-month-panel[hidden] { display: none; }
|
||||
.refund-month-panel__status {
|
||||
padding: 1rem 0.25rem;
|
||||
.refund-summary-detail[hidden] { display: none; }
|
||||
.refund-summary-detail__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
.refund-summary-detail__title {
|
||||
color: var(--vip-content-ink, #171717);
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.refund-summary-detail__close {
|
||||
min-height: 1.85rem;
|
||||
padding: 0 0.65rem;
|
||||
border: 1px solid var(--vip-border, #d8d2c7);
|
||||
border-radius: 6px;
|
||||
background: var(--vip-surface, #ffffff);
|
||||
color: var(--vip-content-muted, #595959);
|
||||
font: inherit;
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.refund-summary-detail__status {
|
||||
padding: 0.5rem 0.25rem;
|
||||
color: var(--vip-content-muted, #595959);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
.refund-month-detail-scroll {
|
||||
.refund-summary-detail__scroll {
|
||||
overflow: auto;
|
||||
max-height: min(34rem, 62vh);
|
||||
}
|
||||
.refund-month-detail-table {
|
||||
.refund-summary-detail__table {
|
||||
width: 100%;
|
||||
min-width: 92rem;
|
||||
border-collapse: collapse;
|
||||
background: var(--vip-surface, #ffffff);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
.refund-month-detail-table th,
|
||||
.refund-month-detail-table td {
|
||||
.refund-summary-detail__table th,
|
||||
.refund-summary-detail__table td {
|
||||
padding: 0.55rem 0.65rem;
|
||||
border-right: 1px solid var(--vip-border, #d8d2c7);
|
||||
border-bottom: 1px solid var(--vip-border, #d8d2c7);
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.refund-month-detail-table th { position: sticky; top: 0; z-index: 2; background: var(--vip-content-accent-soft, #f7f0de); font-weight: 600; }
|
||||
.refund-month-detail-table .is-number { text-align: right; font-variant-numeric: tabular-nums; }
|
||||
.refund-month-panel__pagination {
|
||||
.refund-summary-detail__table th { position: sticky; top: 0; z-index: 2; background: var(--vip-content-accent-soft, #f7f0de); font-weight: 600; }
|
||||
.refund-summary-detail__table .is-number { text-align: right; font-variant-numeric: tabular-nums; }
|
||||
.refund-summary-detail__pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
@ -281,7 +312,7 @@
|
||||
color: var(--vip-content-muted, #595959);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
.refund-month-panel__pagination button {
|
||||
.refund-summary-detail__pagination button {
|
||||
min-height: 1.85rem;
|
||||
padding: 0 0.65rem;
|
||||
border: 1px solid var(--vip-border, #d8d2c7);
|
||||
@ -291,10 +322,7 @@
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
.refund-month-panel__pagination button:disabled { cursor: default; opacity: 0.45; }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.refund-month-toggle__icon { transition: none; }
|
||||
}
|
||||
.refund-summary-detail__pagination button:disabled { cursor: default; opacity: 0.45; }
|
||||
</style>
|
||||
|
||||
@if ($refundWarningMode)
|
||||
@ -346,7 +374,7 @@
|
||||
<div class="refund-month-summary__head">
|
||||
<div class="refund-month-summary__heading">
|
||||
<strong>按退款月份统计</strong>
|
||||
<span>点击月份展开待确认明细</span>
|
||||
<span>点击汇总数字查看对应待确认明细</span>
|
||||
</div>
|
||||
@if ($refundShowExport)
|
||||
<a
|
||||
@ -362,7 +390,14 @@
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
@if (!empty($refundPlatformMonthlySummary['platforms']))
|
||||
@php
|
||||
$summaryMonths = $refundPlatformMonthlySummary['months'] ?? [];
|
||||
$summaryGrandTotal = array_sum(array_map(
|
||||
static fn (array $summaryMonth): int => (int) ($summaryMonth['total'] ?? 0),
|
||||
$summaryMonths,
|
||||
));
|
||||
@endphp
|
||||
@if (!empty($summaryMonths))
|
||||
<div class="refund-platform-summary">
|
||||
<div class="refund-platform-summary__title">平台月度汇总</div>
|
||||
<div class="refund-platform-summary__scroll">
|
||||
@ -370,7 +405,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>平台</th>
|
||||
@foreach ($refundPlatformMonthlySummary['months'] as $summaryMonth)
|
||||
@foreach ($summaryMonths as $summaryMonth)
|
||||
<th>{{ \Illuminate\Support\Carbon::parse($summaryMonth['month'] . '-01')->format('Y年n月') }}</th>
|
||||
@endforeach
|
||||
<th>合计</th>
|
||||
@ -380,49 +415,81 @@
|
||||
@foreach ($refundPlatformMonthlySummary['platforms'] as $platformSummary)
|
||||
<tr>
|
||||
<td>{{ $platformSummary['platform'] }}</td>
|
||||
@foreach ($refundPlatformMonthlySummary['months'] as $summaryMonth)
|
||||
<td>{{ number_format((int) ($platformSummary['months'][$summaryMonth['month']] ?? 0)) }}</td>
|
||||
@foreach ($summaryMonths as $summaryMonth)
|
||||
@php
|
||||
$summaryMonthLabel = \Illuminate\Support\Carbon::parse($summaryMonth['month'] . '-01')->format('Y年n月');
|
||||
$platformMonthCount = (int) ($platformSummary['months'][$summaryMonth['month']] ?? 0);
|
||||
@endphp
|
||||
<td>
|
||||
<button
|
||||
type="button"
|
||||
class="refund-summary-count"
|
||||
data-refund-detail-trigger
|
||||
data-refund-detail-month="{{ $summaryMonth['month'] }}"
|
||||
data-refund-detail-platform="{{ $platformSummary['platform'] }}"
|
||||
aria-pressed="false"
|
||||
aria-label="查看{{ $summaryMonthLabel }} {{ $platformSummary['platform'] }}待确认明细"
|
||||
>{{ number_format($platformMonthCount) }}</button>
|
||||
</td>
|
||||
@endforeach
|
||||
<td>{{ number_format((int) $platformSummary['total']) }}</td>
|
||||
<td>
|
||||
<button
|
||||
type="button"
|
||||
class="refund-summary-count"
|
||||
data-refund-detail-trigger
|
||||
data-refund-detail-platform="{{ $platformSummary['platform'] }}"
|
||||
aria-pressed="false"
|
||||
aria-label="查看{{ $platformSummary['platform'] }}全部月份待确认明细"
|
||||
>{{ number_format((int) $platformSummary['total']) }}</button>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
<tr class="refund-platform-summary__total-row">
|
||||
<th scope="row">月度合计</th>
|
||||
@foreach ($summaryMonths as $summaryMonth)
|
||||
<td>
|
||||
<button
|
||||
type="button"
|
||||
class="refund-summary-count"
|
||||
data-refund-detail-trigger
|
||||
data-refund-detail-month="{{ $summaryMonth['month'] }}"
|
||||
aria-pressed="false"
|
||||
aria-label="查看{{ \Illuminate\Support\Carbon::parse($summaryMonth['month'] . '-01')->format('Y年n月') }}全部平台待确认明细"
|
||||
>{{ number_format((int) $summaryMonth['total']) }}</button>
|
||||
</td>
|
||||
@endforeach
|
||||
<td>
|
||||
<button
|
||||
type="button"
|
||||
class="refund-summary-count"
|
||||
data-refund-detail-trigger
|
||||
aria-pressed="false"
|
||||
aria-label="查看全部平台、全部月份待确认明细"
|
||||
>{{ number_format($summaryGrandTotal) }}</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="refund-summary-detail__status">从 2026 年 10 月起暂无待确认记录。</div>
|
||||
@endif
|
||||
@forelse ($refundMonthlySummary as $monthSummary)
|
||||
@php
|
||||
$monthKey = $monthSummary['month'];
|
||||
$panelId = 'refund-month-panel-' . str_replace('-', '', $monthKey);
|
||||
@endphp
|
||||
<section class="refund-month-group">
|
||||
<button
|
||||
type="button"
|
||||
class="refund-month-toggle"
|
||||
data-refund-month-toggle="{{ $monthKey }}"
|
||||
aria-expanded="false"
|
||||
aria-controls="{{ $panelId }}"
|
||||
>
|
||||
<span class="refund-month-toggle__month">{{ \Illuminate\Support\Carbon::parse($monthKey . '-01')->format('Y年n月') }}</span>
|
||||
<span class="refund-month-toggle__count">{{ number_format((int) $monthSummary['total']) }} 条待确认</span>
|
||||
<span class="material-icons-outlined refund-month-toggle__icon" aria-hidden="true">expand_more</span>
|
||||
</button>
|
||||
<div id="{{ $panelId }}" class="refund-month-panel" data-refund-month-panel="{{ $monthKey }}" hidden>
|
||||
<div class="refund-month-panel__status" role="status" data-refund-month-status>点击后加载明细数据。</div>
|
||||
<div class="refund-month-detail-scroll" data-refund-month-detail hidden></div>
|
||||
<div class="refund-month-panel__pagination" data-refund-month-pagination hidden>
|
||||
<span data-refund-month-page-label></span>
|
||||
<span>
|
||||
<button type="button" data-refund-month-page="-1">上一页</button>
|
||||
<button type="button" data-refund-month-page="1">下一页</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@empty
|
||||
<div class="refund-month-panel__status">从 2026 年 10 月起暂无待确认记录。</div>
|
||||
@endforelse
|
||||
<section class="refund-summary-detail" data-refund-summary-detail hidden>
|
||||
<div class="refund-summary-detail__head">
|
||||
<strong class="refund-summary-detail__title" data-refund-summary-detail-title>待确认明细</strong>
|
||||
<button type="button" class="refund-summary-detail__close" data-refund-summary-detail-close>关闭</button>
|
||||
</div>
|
||||
<div class="refund-summary-detail__status" role="status" data-refund-summary-detail-status>请选择汇总数字查看明细。</div>
|
||||
<div class="refund-summary-detail__scroll" data-refund-summary-detail-scroll hidden></div>
|
||||
<div class="refund-summary-detail__pagination" data-refund-summary-detail-pagination hidden>
|
||||
<span data-refund-summary-detail-page-label></span>
|
||||
<span>
|
||||
<button type="button" data-refund-summary-detail-page="-1">上一页</button>
|
||||
<button type="button" data-refund-summary-detail-page="1">下一页</button>
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
@ -431,6 +498,12 @@
|
||||
if (!root || root.dataset.bound === 'true') return;
|
||||
root.dataset.bound = 'true';
|
||||
|
||||
const detailPanel = root.querySelector('[data-refund-summary-detail]');
|
||||
const detailTitle = root.querySelector('[data-refund-summary-detail-title]');
|
||||
const detailStatus = root.querySelector('[data-refund-summary-detail-status]');
|
||||
const detailScroll = root.querySelector('[data-refund-summary-detail-scroll]');
|
||||
const pagination = root.querySelector('[data-refund-summary-detail-pagination]');
|
||||
const triggers = [...root.querySelectorAll('[data-refund-detail-trigger]')];
|
||||
const columns = [
|
||||
['售后单号', 'after_sales_no'],
|
||||
['外部售后单号', 'external_after_sales_no'],
|
||||
@ -448,6 +521,8 @@
|
||||
['明细退款金额', 'detail_refund_amount', true],
|
||||
['运单号', 'waybill_no'],
|
||||
];
|
||||
const selected = { month: '', platform: '', page: 1 };
|
||||
let requestSerial = 0;
|
||||
|
||||
const textValue = (value) => {
|
||||
const text = value === null || value === undefined ? '' : String(value).trim();
|
||||
@ -464,13 +539,16 @@
|
||||
const text = textValue(value);
|
||||
return text === '-' ? text : text.slice(0, 19);
|
||||
};
|
||||
const monthLabel = (month) => {
|
||||
if (!month) return '全部月份';
|
||||
const [year, monthNumber] = month.split('-');
|
||||
return `${year}年${Number(monthNumber)}月`;
|
||||
};
|
||||
const detailLabel = () => `${monthLabel(selected.month)} · ${selected.platform || '全部平台'}`;
|
||||
|
||||
const render = (panel, payload) => {
|
||||
const detail = panel.querySelector('[data-refund-month-detail]');
|
||||
const status = panel.querySelector('[data-refund-month-status]');
|
||||
const pagination = panel.querySelector('[data-refund-month-pagination]');
|
||||
const render = (payload) => {
|
||||
const table = document.createElement('table');
|
||||
table.className = 'refund-month-detail-table';
|
||||
table.className = 'refund-summary-detail__table';
|
||||
const head = document.createElement('thead');
|
||||
const headRow = document.createElement('tr');
|
||||
columns.forEach(([label]) => {
|
||||
@ -486,7 +564,7 @@
|
||||
const row = document.createElement('tr');
|
||||
const cell = document.createElement('td');
|
||||
cell.colSpan = columns.length;
|
||||
cell.textContent = '当前月份暂无待确认明细。';
|
||||
cell.textContent = '当前范围暂无待确认明细。';
|
||||
row.appendChild(cell);
|
||||
body.appendChild(row);
|
||||
} else {
|
||||
@ -506,27 +584,29 @@
|
||||
});
|
||||
}
|
||||
table.appendChild(body);
|
||||
detail.replaceChildren(table);
|
||||
detail.hidden = false;
|
||||
status.hidden = true;
|
||||
detailScroll.replaceChildren(table);
|
||||
detailScroll.hidden = false;
|
||||
detailStatus.hidden = true;
|
||||
pagination.hidden = false;
|
||||
pagination.dataset.currentPage = String(payload.page);
|
||||
pagination.querySelector('[data-refund-month-page-label]').textContent =
|
||||
root.querySelector('[data-refund-summary-detail-page-label]').textContent =
|
||||
`明细第 ${payload.page} 页 · 当前显示 ${Number(payload.rows.length).toLocaleString('zh-CN')} 条`;
|
||||
pagination.querySelector('[data-refund-month-page="-1"]').disabled = payload.page <= 1;
|
||||
pagination.querySelector('[data-refund-month-page="1"]').disabled = !payload.has_more;
|
||||
root.querySelector('[data-refund-summary-detail-page="-1"]').disabled = payload.page <= 1;
|
||||
root.querySelector('[data-refund-summary-detail-page="1"]').disabled = !payload.has_more;
|
||||
};
|
||||
|
||||
const load = async (month, page, panel) => {
|
||||
const status = panel.querySelector('[data-refund-month-status]');
|
||||
const detail = panel.querySelector('[data-refund-month-detail]');
|
||||
const pagination = panel.querySelector('[data-refund-month-pagination]');
|
||||
status.hidden = false;
|
||||
status.textContent = '正在加载明细数据…';
|
||||
detail.hidden = true;
|
||||
const load = async (page = 1) => {
|
||||
selected.page = page;
|
||||
const serial = ++requestSerial;
|
||||
detailPanel.hidden = false;
|
||||
detailTitle.textContent = detailLabel();
|
||||
detailStatus.hidden = false;
|
||||
detailStatus.textContent = '正在加载明细数据…';
|
||||
detailScroll.hidden = true;
|
||||
pagination.hidden = true;
|
||||
const url = new URL(root.dataset.detailsUrl, window.location.origin);
|
||||
url.searchParams.set('month', month);
|
||||
url.searchParams.set('month', selected.month || 'all');
|
||||
if (selected.platform) url.searchParams.set('platform', selected.platform);
|
||||
url.searchParams.set('page', String(page));
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
@ -535,39 +615,39 @@
|
||||
});
|
||||
if (!response.ok) throw new Error('request_failed');
|
||||
const payload = await response.json();
|
||||
render(panel, payload);
|
||||
panel.dataset.loaded = 'true';
|
||||
if (serial !== requestSerial) return;
|
||||
render(payload);
|
||||
} catch (_) {
|
||||
status.hidden = false;
|
||||
status.textContent = '明细加载失败,请收起后重新展开重试。';
|
||||
if (serial !== requestSerial) return;
|
||||
detailStatus.hidden = false;
|
||||
detailStatus.textContent = '明细加载失败,请稍后重试。';
|
||||
}
|
||||
};
|
||||
|
||||
root.querySelectorAll('[data-refund-month-toggle]').forEach((button) => {
|
||||
triggers.forEach((button) => {
|
||||
button.addEventListener('click', () => {
|
||||
const panel = document.getElementById(button.getAttribute('aria-controls'));
|
||||
if (!panel) return;
|
||||
const opening = panel.hidden;
|
||||
panel.hidden = !opening;
|
||||
button.setAttribute('aria-expanded', opening ? 'true' : 'false');
|
||||
if (opening && panel.dataset.loaded !== 'true') {
|
||||
load(button.dataset.refundMonthToggle, 1, panel);
|
||||
}
|
||||
triggers.forEach((item) => item.setAttribute('aria-pressed', 'false'));
|
||||
button.setAttribute('aria-pressed', 'true');
|
||||
selected.month = button.dataset.refundDetailMonth || '';
|
||||
selected.platform = button.dataset.refundDetailPlatform || '';
|
||||
load(1);
|
||||
window.requestAnimationFrame(() => detailPanel.scrollIntoView({ behavior: 'smooth', block: 'nearest' }));
|
||||
});
|
||||
});
|
||||
|
||||
root.querySelectorAll('[data-refund-month-panel]').forEach((panel) => {
|
||||
panel.querySelectorAll('[data-refund-month-page]').forEach((button) => {
|
||||
button.addEventListener('click', () => {
|
||||
const pagination = panel.querySelector('[data-refund-month-pagination]');
|
||||
const current = Number(pagination.dataset.currentPage || 1);
|
||||
const next = Math.max(current + Number(button.dataset.refundMonthPage), 1);
|
||||
if (next !== current) {
|
||||
load(panel.dataset.refundMonthPanel, next, panel);
|
||||
}
|
||||
});
|
||||
root.querySelectorAll('[data-refund-summary-detail-page]').forEach((button) => {
|
||||
button.addEventListener('click', () => {
|
||||
const current = Number(pagination.dataset.currentPage || 1);
|
||||
const next = Math.max(current + Number(button.dataset.refundSummaryDetailPage), 1);
|
||||
if (next !== current) load(next);
|
||||
});
|
||||
});
|
||||
|
||||
root.querySelector('[data-refund-summary-detail-close]').addEventListener('click', () => {
|
||||
requestSerial += 1;
|
||||
detailPanel.hidden = true;
|
||||
triggers.forEach((item) => item.setAttribute('aria-pressed', 'false'));
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@else
|
||||
|
||||
@ -180,9 +180,15 @@ class NonJingxiRefundPageTest extends FeatureTestCase
|
||||
->assertSeeText('京东商城')
|
||||
->assertSeeText('天猫')
|
||||
->assertSeeText('2026年10月')
|
||||
->assertSeeText('5,641 条待确认')
|
||||
->assertSeeText('2026年11月')
|
||||
->assertSeeText('3,232 条待确认')
|
||||
->assertSeeText('点击汇总数字查看对应待确认明细')
|
||||
->assertSeeText('月度合计')
|
||||
->assertSee('data-refund-detail-trigger', false)
|
||||
->assertSee('data-refund-detail-month="2026-10"', false)
|
||||
->assertSee('data-refund-detail-platform="京东商城"', false)
|
||||
->assertSee('data-refund-summary-detail', false)
|
||||
->assertDontSee('data-refund-month-toggle', false)
|
||||
->assertDontSeeText('5,641 条待确认')
|
||||
->assertSeeText('导出全部明细')
|
||||
->assertSee('data-refund-all-export', false)
|
||||
->assertSee('from=2026-10-01&to=2026-11-04', false)
|
||||
@ -252,6 +258,7 @@ class NonJingxiRefundPageTest extends FeatureTestCase
|
||||
'waybill_no' => '',
|
||||
'shop_name' => '',
|
||||
'sku_id' => '',
|
||||
'platform' => '京东商城',
|
||||
] && $page === 2 && $limit === 25
|
||||
)->andReturn([
|
||||
'data' => [(object) [
|
||||
@ -278,9 +285,11 @@ class NonJingxiRefundPageTest extends FeatureTestCase
|
||||
$this->loginAs()->getJson(route('business.refunds.refund-pending.month-details', [
|
||||
'month' => '2026-10',
|
||||
'page' => 2,
|
||||
'platform' => '京东商城',
|
||||
]))
|
||||
->assertOk()
|
||||
->assertJsonPath('month', '2026-10')
|
||||
->assertJsonPath('platform', '京东商城')
|
||||
->assertJsonPath('page', 2)
|
||||
->assertJsonPath('has_more', true)
|
||||
->assertJsonPath('rows.0.after_sales_no', '743441809')
|
||||
@ -293,6 +302,38 @@ class NonJingxiRefundPageTest extends FeatureTestCase
|
||||
->assertJsonPath('rows.0.sku_id', 'SKU-PENDING-1');
|
||||
}
|
||||
|
||||
public function test_refund_pending_all_details_endpoint_uses_october_to_today(): void
|
||||
{
|
||||
$this->grantAdminPermission();
|
||||
$repository = \Mockery::mock(NonJingxiRefundMirrorRepository::class);
|
||||
$repository->shouldReceive('available')->once()->andReturnTrue();
|
||||
$repository->shouldReceive('ready')->once()->andReturnTrue();
|
||||
$repository->shouldReceive('queryReady')->once()->andReturnTrue();
|
||||
$repository->shouldReceive('pagePendingConfirmation')->once()->withArgs(
|
||||
static fn (array $filters, int $page, int $limit): bool =>
|
||||
$filters === [
|
||||
'from' => '2026-10-01',
|
||||
'to' => '2026-11-04',
|
||||
'order_no' => '',
|
||||
'waybill_no' => '',
|
||||
'shop_name' => '',
|
||||
'sku_id' => '',
|
||||
'platform' => '',
|
||||
] && $page === 1 && $limit === 25
|
||||
)->andReturn(['data' => [], 'has_more' => false]);
|
||||
$this->app->instance(NonJingxiRefundMirrorRepository::class, $repository);
|
||||
|
||||
$this->loginAs()->getJson(route('business.refunds.refund-pending.month-details', [
|
||||
'month' => 'all',
|
||||
]))
|
||||
->assertOk()
|
||||
->assertJsonPath('month', 'all')
|
||||
->assertJsonPath('platform', '')
|
||||
->assertJsonPath('page', 1)
|
||||
->assertJsonPath('has_more', false)
|
||||
->assertJsonCount(0, 'rows');
|
||||
}
|
||||
|
||||
public function test_refund_export_downloads_filtered_csv(): void
|
||||
{
|
||||
$this->grantAdminPermission();
|
||||
|
||||
@ -217,6 +217,53 @@ class NonJingxiRefundMirrorRepositoryTest extends TestCase
|
||||
], $platformSummary['platforms']);
|
||||
}
|
||||
|
||||
public function test_pending_confirmation_details_filter_by_platform_across_both_sources(): void
|
||||
{
|
||||
$db = DB::connection('business_mirror');
|
||||
$db->table('ops_jst_return_refund_info')->insert([
|
||||
array_replace($this->refundInfo(1, 101, '2026-10-02 10:00:00', '天猫', '天猫店'), [
|
||||
'status' => 'WaitConfirm',
|
||||
'shop_status' => 'SUCCESS',
|
||||
]),
|
||||
array_replace($this->refundInfo(2, 102, '2026-10-03 10:00:00', '京东商城', '京东店'), [
|
||||
'status' => 'WaitConfirm',
|
||||
'shop_status' => 'SUCCESS',
|
||||
]),
|
||||
]);
|
||||
$knownSupplement = $this->supplement(201, '2026-10-04 10:00:00', 'SHOP-JD');
|
||||
$knownSupplement['status'] = 'WaitConfirm';
|
||||
$knownSupplement['shop_status'] = 'SUCCESS';
|
||||
$unknownSupplement = $this->supplement(202, '2026-10-05 10:00:00', 'SHOP-UNKNOWN');
|
||||
$unknownSupplement['status'] = 'WaitConfirm';
|
||||
$unknownSupplement['shop_status'] = 'SUCCESS';
|
||||
$db->table('ops_jst_qmcust_refunds')->insert([$knownSupplement, $unknownSupplement]);
|
||||
$db->table('ops_jst_shop_info')->insert([
|
||||
['Id' => 1, 'shop_id' => 'SHOP-JD', 'shop_site' => '京东商城'],
|
||||
]);
|
||||
|
||||
$filters = [
|
||||
'from' => '2026-10-01',
|
||||
'to' => '2026-10-31',
|
||||
'order_no' => '',
|
||||
'waybill_no' => '',
|
||||
'shop_name' => '',
|
||||
'sku_id' => '',
|
||||
];
|
||||
$idsFor = function (string $platform) use ($filters): array {
|
||||
$result = $this->repository->pagePendingConfirmation(
|
||||
$filters + ['platform' => $platform],
|
||||
1,
|
||||
25,
|
||||
);
|
||||
|
||||
return array_map(static fn (object $row): int => (int) $row->as_id, $result['data']);
|
||||
};
|
||||
|
||||
$this->assertSame([101], $idsFor('天猫'));
|
||||
$this->assertEqualsCanonicalizing([102, 201], $idsFor('京东商城'));
|
||||
$this->assertSame([202], $idsFor('待确认'));
|
||||
}
|
||||
|
||||
public function test_supplement_and_header_only_refunds_survive_without_invented_details(): void
|
||||
{
|
||||
$db = DB::connection('business_mirror');
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user