feat: 退款月度汇总支持点击查看明细

月度合计移入汇总表最后一行,汇总数字支持按平台和月份查看明细。
This commit is contained in:
renshen 2026-11-04 17:33:22 +08:00
parent 026733e919
commit c7654fe74d
5 changed files with 396 additions and 140 deletions

View File

@ -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'],

View File

@ -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

View File

@ -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

View File

@ -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&amp;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();

View File

@ -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');