退货取件管理:整行点击异步加载详情,本地 opcache 提速

This commit is contained in:
zhoushengchen 2026-10-12 13:47:01 +08:00
parent 3e24bc5111
commit 9f6a5f6fde
6 changed files with 315 additions and 119 deletions

View File

@ -6247,6 +6247,30 @@ class Pages extends Controller
);
}
/** 详情局部刷新:点行后只返回详情面板,避免整页重载。 */
public function returnPickupManagementDetail(Request $request): \Illuminate\Contracts\View\View
{
$pickupId = max((int) $request->query('pickup_id', 0), 0);
abort_if($pickupId <= 0, 404);
$repository = app(\Modules\EcommerceFinance\Repositories\ReturnPickupManagementMirrorRepository::class);
$detail = $this->enrichReturnPickupDetail(
$repository->detail($pickupId),
$repository->warehouseDirectory(),
);
abort_if($detail === null, 404);
return view('business.pages.courier-payables.partials.return-pickup-detail', [
'detail' => $detail,
'closeUrl' => route('business.pages.show', [
'section' => 'courier-payables',
'page' => 'return-pickup-management',
]),
]);
}
protected function contentForReturnPickupManagement(): array
{
$filters = $this->returnPickupFilters(request());

View File

@ -1,6 +1,10 @@
; Local development serves bind-mounted PHP/Blade source directly. Disable
; opcode caching so a browser refresh always executes the current source.
opcache.enable=0
; Local development serves bind-mounted PHP/Blade source directly. Keep opcode
; caching on with per-request timestamp validation so a browser refresh still
; executes the current source, without recompiling the whole framework each time.
opcache.enable=1
opcache.enable_cli=0
opcache.validate_timestamps=1
opcache.revalidate_freq=0
opcache.memory_consumption=256
opcache.max_accelerated_files=40000
opcache.interned_strings_buffer=16

View File

@ -0,0 +1,115 @@
@if ($detail !== null)
@php
$main = $detail['main'];
@endphp
<section class="return-pickup-panel" id="return-pickup-detail" aria-label="退货取件单详情">
<div class="return-pickup-panel__head">
<span>退货取件单详情 · {{ $main->orderId ?? '-' }} · {{ $main->shopName ?: '-' }} · {{ $main->express_company ?: '-' }}</span>
<a class="return-pickup-link" href="{{ $closeUrl }}">关闭详情</a>
</div>
<div class="return-pickup-table-wrap return-pickup-table-wrap--scroll">
<table class="return-pickup-table">
<thead>
<tr>
<th scope="col">应退货号</th>
<th scope="col">实退货号</th>
<th scope="col">平台商品id</th>
<th scope="col">平台skuid</th>
<th scope="col">条形码</th>
<th scope="col">品名</th>
<th scope="col">应退数量</th>
<th scope="col">实退数量</th>
<th scope="col">待核销数量</th>
<th scope="col">明细退货进度</th>
<th scope="col">发货仓</th>
<th scope="col">收件人</th>
<th scope="col">收件地址</th>
<th scope="col">长</th>
<th scope="col">宽</th>
<th scope="col">高</th>
<th scope="col">毛重(kg)</th>
<th scope="col">体积重(kg)</th>
<th scope="col">重量取大(kg)</th>
<th scope="col">计费重量(kg)</th>
<th scope="col">面单(元)</th>
<th scope="col">费率(元/kg)</th>
<th scope="col">预估运费(元)</th>
<th scope="col">实际运费(元)</th>
<th scope="col">未算出原因</th>
<th scope="col">加收(元)</th>
<th scope="col">差异(元)</th>
<th scope="col">实退单号</th>
</tr>
</thead>
<tbody>
@forelse ($detail['items'] as $item)
@php
$volumeWeight = ($item->length && $item->width && $item->height)
? round(((float) $item->length * (float) $item->width * (float) $item->height) / 6000, 2)
: null;
@endphp
<tr>
<td>{{ $item->returnProcode ?: '-' }}</td>
<td class="is-code">{{ $item->actualReturnProcode ?: '-' }}</td>
<td class="is-code">{{ $item->platformProductId ?: '-' }}</td>
<td class="is-code">{{ $item->platformSkuId ?: '-' }}</td>
<td class="is-code">{{ $item->barcode ?: '-' }}</td>
<td class="is-clamped" title="{{ $item->productName }}">{{ $item->productName ?: '-' }}</td>
<td>{{ $item->returnQuantity ?? '-' }}</td>
<td>{{ $item->actualReturnQuantity ?? '-' }}</td>
<td>{{ $item->unverifiedQuantity ?? '-' }}</td>
<td>{{ $item->returnProgress ?: '-' }}</td>
<td title="{{ $item->warehouse }}">{{ ($item->warehouse_name ?? null) ?: ($item->warehouse ?: '-') }}</td>
<td>{{ $item->receiverName ?: '-' }} {{ $item->receiverPhone ?: '' }}</td>
<td class="is-clamped" title="{{ $item->receiverAddress }}">{{ $item->receiverAddress ?: '-' }}</td>
<td class="is-code">{{ $item->length !== null ? number_format((float) $item->length, 2) : '-' }}</td>
<td class="is-code">{{ $item->width !== null ? number_format((float) $item->width, 2) : '-' }}</td>
<td class="is-code">{{ $item->height !== null ? number_format((float) $item->height, 2) : '-' }}</td>
<td class="is-code">{{ $item->grossWeight !== null ? number_format((float) $item->grossWeight, 2) : '-' }}</td>
<td class="is-code">{{ $volumeWeight !== null ? number_format($volumeWeight, 2) : '-' }}</td>
<td class="is-code">{{ $item->freight_settlement !== null ? number_format((float) $item->freight_settlement, 2) : '—' }}</td>
<td class="is-code" title="{{ $item->freight_process }}">{{ $item->freight_billable !== null ? number_format((float) $item->freight_billable, 2) : '—' }}</td>
<td class="is-code">{{ $item->freight_base_amount !== null ? number_format((float) $item->freight_base_amount, 2) : '—' }}</td>
<td class="is-code">{{ $item->freight_increment_amount !== null ? number_format((float) $item->freight_increment_amount, 2) : '—' }}</td>
<td class="is-code">{{ $item->estimatedFreight !== null ? number_format((float) $item->estimatedFreight, 2) : '-' }}</td>
<td class="is-code" title="{{ $item->freight_note }}">
{{ $item->actual_freight !== null ? number_format((float) $item->actual_freight, 2) : '—' }}
</td>
<td class="is-clamped" title="{{ $item->freight_note }}">
{{ $item->actual_freight === null ? ($item->freight_note ?: '未匹配到报价') : '—' }}
</td>
<td class="is-code" title="{{ ($item->freight_surcharge ?? 0) > 0 ? $item->freight_note : '' }}">
{{ ($item->freight_surcharge ?? 0) > 0 ? number_format((float) $item->freight_surcharge, 2) : '—' }}
</td>
<td class="is-code">
@if ($item->freight_diff !== null)
<span class="{{ (float) $item->freight_diff < 0 ? 'is-negative' : 'is-positive' }}">{{ number_format((float) $item->freight_diff, 2) }}</span>
@else
—
@endif
</td>
<td class="is-code">{{ $item->actualReturnOrderNo ?: '-' }}</td>
</tr>
@empty
<tr><td class="return-pickup-empty" colspan="28">该取件单暂无明细分录。</td></tr>
@endforelse
</tbody>
@if (($detail['totals'] ?? null) !== null)
<tbody>
<tr class="return-pickup-total-row">
<td colspan="22">合计({{ count($detail['items']) }} 条明细,差异 = 预估 − 实际运费,加收单列)</td>
<td class="is-code">{{ number_format((float) $detail['totals']['estimated'], 2) }}</td>
<td class="is-code">{{ $detail['totals']['actual'] !== null ? number_format((float) $detail['totals']['actual'], 2) : '—' }}</td>
<td>—</td>
<td class="is-code">{{ ($detail['totals']['surcharge'] ?? 0) > 0 ? number_format((float) $detail['totals']['surcharge'], 2) : '—' }}</td>
<td class="is-code">
{{ $detail['totals']['diff'] !== null ? number_format((float) $detail['totals']['diff'], 2) : '—' }}
</td>
<td>—</td>
</tr>
</tbody>
@endif
</table>
</div>
</section>
@endif

View File

@ -252,6 +252,7 @@
.return-pickup-table tbody tr.is-active { background: var(--vip-content-accent-soft, #f7f0de); }
.return-pickup-table tbody tr.is-active td { border-bottom-color: #e4d7ba; }
.return-pickup-table tbody tr.is-active td:first-child { box-shadow: inset 3px 0 0 var(--vip-content-accent-dark, #856632); }
.return-pickup-table tbody tr.is-clickable { cursor: pointer; }
.return-pickup-table .is-code { font-variant-numeric: tabular-nums; }
.return-pickup-table .is-clamped { max-width: 220px; overflow: hidden; text-overflow: ellipsis; }
.return-pickup-table .is-negative { color: var(--vip-danger, #b80000); font-weight: 600; }
@ -433,7 +434,7 @@
</thead>
<tbody>
@forelse (($rows ?? collect()) as $row)
<tr class="{{ $detail !== null && (int) ($detail['main']->Id ?? 0) === (int) $row->Id ? 'is-active' : '' }}">
<tr class="is-clickable {{ $detail !== null && (int) ($detail['main']->Id ?? 0) === (int) $row->Id ? 'is-active' : '' }}" data-href="{{ $detailUrl($row->Id) }}" data-detail-url="{{ route('business.courier-payables.return-pickup-management.detail', ['pickup_id' => $row->Id]) }}" tabindex="0">
<td class="is-code">{{ $row->orderId ?: '-' }}</td>
<td>{{ $row->express_company ?: '-' }}</td>
<td>
@ -485,121 +486,7 @@
@endif
</section>
@if ($detail !== null)
@php
$main = $detail['main'];
@endphp
<section class="return-pickup-panel" id="return-pickup-detail" aria-label="退货取件单详情">
<div class="return-pickup-panel__head">
<span>退货取件单详情 · {{ $main->orderId ?? '-' }} · {{ $main->shopName ?: '-' }} · {{ $main->express_company ?: '-' }}</span>
<a class="return-pickup-link" href="{{ $pageUrl($rows?->currentPage() ?? 1) }}">关闭详情</a>
</div>
<div class="return-pickup-table-wrap return-pickup-table-wrap--scroll">
<table class="return-pickup-table">
<thead>
<tr>
<th scope="col">应退货号</th>
<th scope="col">实退货号</th>
<th scope="col">平台商品id</th>
<th scope="col">平台skuid</th>
<th scope="col">条形码</th>
<th scope="col">品名</th>
<th scope="col">应退数量</th>
<th scope="col">实退数量</th>
<th scope="col">待核销数量</th>
<th scope="col">明细退货进度</th>
<th scope="col">发货仓</th>
<th scope="col">收件人</th>
<th scope="col">收件地址</th>
<th scope="col">长</th>
<th scope="col">宽</th>
<th scope="col">高</th>
<th scope="col">毛重(kg)</th>
<th scope="col">体积重(kg)</th>
<th scope="col">重量取大(kg)</th>
<th scope="col">计费重量(kg)</th>
<th scope="col">面单(元)</th>
<th scope="col">费率(元/kg)</th>
<th scope="col">预估运费(元)</th>
<th scope="col">实际运费(元)</th>
<th scope="col">未算出原因</th>
<th scope="col">加收(元)</th>
<th scope="col">差异(元)</th>
<th scope="col">实退单号</th>
</tr>
</thead>
<tbody>
@forelse ($detail['items'] as $item)
@php
$volumeWeight = ($item->length && $item->width && $item->height)
? round(((float) $item->length * (float) $item->width * (float) $item->height) / 6000, 2)
: null;
@endphp
<tr>
<td>{{ $item->returnProcode ?: '-' }}</td>
<td class="is-code">{{ $item->actualReturnProcode ?: '-' }}</td>
<td class="is-code">{{ $item->platformProductId ?: '-' }}</td>
<td class="is-code">{{ $item->platformSkuId ?: '-' }}</td>
<td class="is-code">{{ $item->barcode ?: '-' }}</td>
<td class="is-clamped" title="{{ $item->productName }}">{{ $item->productName ?: '-' }}</td>
<td>{{ $item->returnQuantity ?? '-' }}</td>
<td>{{ $item->actualReturnQuantity ?? '-' }}</td>
<td>{{ $item->unverifiedQuantity ?? '-' }}</td>
<td>{{ $item->returnProgress ?: '-' }}</td>
<td title="{{ $item->warehouse }}">{{ ($item->warehouse_name ?? null) ?: ($item->warehouse ?: '-') }}</td>
<td>{{ $item->receiverName ?: '-' }} {{ $item->receiverPhone ?: '' }}</td>
<td class="is-clamped" title="{{ $item->receiverAddress }}">{{ $item->receiverAddress ?: '-' }}</td>
<td class="is-code">{{ $item->length !== null ? number_format((float) $item->length, 2) : '-' }}</td>
<td class="is-code">{{ $item->width !== null ? number_format((float) $item->width, 2) : '-' }}</td>
<td class="is-code">{{ $item->height !== null ? number_format((float) $item->height, 2) : '-' }}</td>
<td class="is-code">{{ $item->grossWeight !== null ? number_format((float) $item->grossWeight, 2) : '-' }}</td>
<td class="is-code">{{ $volumeWeight !== null ? number_format($volumeWeight, 2) : '-' }}</td>
<td class="is-code">{{ $item->freight_settlement !== null ? number_format((float) $item->freight_settlement, 2) : '—' }}</td>
<td class="is-code" title="{{ $item->freight_process }}">{{ $item->freight_billable !== null ? number_format((float) $item->freight_billable, 2) : '—' }}</td>
<td class="is-code">{{ $item->freight_base_amount !== null ? number_format((float) $item->freight_base_amount, 2) : '—' }}</td>
<td class="is-code">{{ $item->freight_increment_amount !== null ? number_format((float) $item->freight_increment_amount, 2) : '—' }}</td>
<td class="is-code">{{ $item->estimatedFreight !== null ? number_format((float) $item->estimatedFreight, 2) : '-' }}</td>
<td class="is-code" title="{{ $item->freight_note }}">
{{ $item->actual_freight !== null ? number_format((float) $item->actual_freight, 2) : '—' }}
</td>
<td class="is-clamped" title="{{ $item->freight_note }}">
{{ $item->actual_freight === null ? ($item->freight_note ?: '未匹配到报价') : '—' }}
</td>
<td class="is-code" title="{{ ($item->freight_surcharge ?? 0) > 0 ? $item->freight_note : '' }}">
{{ ($item->freight_surcharge ?? 0) > 0 ? number_format((float) $item->freight_surcharge, 2) : '—' }}
</td>
<td class="is-code">
@if ($item->freight_diff !== null)
<span class="{{ (float) $item->freight_diff < 0 ? 'is-negative' : 'is-positive' }}">{{ number_format((float) $item->freight_diff, 2) }}</span>
@else
—
@endif
</td>
<td class="is-code">{{ $item->actualReturnOrderNo ?: '-' }}</td>
</tr>
@empty
<tr><td class="return-pickup-empty" colspan="28">该取件单暂无明细分录。</td></tr>
@endforelse
</tbody>
@if (($detail['totals'] ?? null) !== null)
<tbody>
<tr class="return-pickup-total-row">
<td colspan="22">合计({{ count($detail['items']) }} 条明细,差异 = 预估 − 实际运费,加收单列)</td>
<td class="is-code">{{ number_format((float) $detail['totals']['estimated'], 2) }}</td>
<td class="is-code">{{ $detail['totals']['actual'] !== null ? number_format((float) $detail['totals']['actual'], 2) : '—' }}</td>
<td>—</td>
<td class="is-code">{{ ($detail['totals']['surcharge'] ?? 0) > 0 ? number_format((float) $detail['totals']['surcharge'], 2) : '—' }}</td>
<td class="is-code">
{{ $detail['totals']['diff'] !== null ? number_format((float) $detail['totals']['diff'], 2) : '—' }}
</td>
<td>—</td>
</tr>
</tbody>
@endif
</table>
</div>
</section>
@endif
@include('business.pages.courier-payables.partials.return-pickup-detail', ['detail' => $detail, 'closeUrl' => $pageUrl($rows?->currentPage() ?? 1)])
</div>
<script>
@ -613,5 +500,87 @@
searchPlaceholder: '打字找快递公司(可多选)'
});
}
function openReturnPickupDetail(row) {
var detailUrl = row.getAttribute('data-detail-url');
var pageUrl = row.getAttribute('data-href');
if (!detailUrl || !window.fetch) {
if (pageUrl) {
window.location.href = pageUrl;
}
return;
}
fetch(detailUrl, { headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'text/html' } })
.then(function (response) {
if (!response.ok) {
throw new Error('detail');
}
return response.text();
})
.then(function (html) {
var holder = document.createElement('div');
holder.innerHTML = html;
var next = holder.querySelector('#return-pickup-detail');
if (!next) {
throw new Error('empty');
}
var current = document.getElementById('return-pickup-detail');
if (current) {
current.replaceWith(next);
} else {
var list = document.querySelector('[aria-label="退货取件单列表"]');
if (list && list.parentNode) {
list.parentNode.insertBefore(next, list.nextSibling);
}
}
document.querySelectorAll('tr.is-active').forEach(function (tr) { tr.classList.remove('is-active'); });
row.classList.add('is-active');
if (pageUrl && window.history && window.history.pushState) {
window.history.pushState({}, '', pageUrl);
}
next.scrollIntoView({ behavior: 'smooth', block: 'start' });
})
.catch(function () {
if (pageUrl) {
window.location.href = pageUrl;
}
});
}
document.addEventListener('click', function (event) {
if (!(event.target instanceof Element) || event.target.closest('a, button, input, label, select')) {
return;
}
var row = event.target.closest('tr[data-detail-url]');
if (row) {
event.preventDefault();
openReturnPickupDetail(row);
}
});
document.addEventListener('keydown', function (event) {
if (event.key !== 'Enter' || !(event.target instanceof Element)) {
return;
}
var row = event.target.closest('tr[data-detail-url]');
if (row) {
openReturnPickupDetail(row);
}
});
})();
</script>

View File

@ -352,6 +352,10 @@ Route::get('workspace/courier-payables/return-pickup-management/export', 'Busine
->middleware('business.page:courier-payables,return-pickup-management')
->name('business.courier-payables.return-pickup-management.export');
Route::get('workspace/courier-payables/return-pickup-management/detail', 'Business\Pages@returnPickupManagementDetail')
->middleware('business.page:courier-payables,return-pickup-management')
->name('business.courier-payables.return-pickup-management.detail');
// 必须注册在通用 {section}/{page}/{tab}/export 之前,否则会被通用导出路由截获。
Route::get('workspace/courier-payables/courier-surcharge/settlement/export', 'Business\Pages@exportCourierSurchargeSettlement')
->middleware('business.page:courier-payables,courier-surcharge')

View File

@ -216,6 +216,9 @@ class ReturnPickupManagementPageTest extends FeatureTestCase
->assertOk()
->assertSee('id="pickupCarrierFilter"', false)
->assertSee('data-name="carrier"', false)
->assertSee('is-clickable', false)
->assertSee('data-href=', false)
->assertSee('data-detail-url=', false)
->assertSeeText('同步正常')
->assertSeeText('待快递下单')
->assertSeeText('E202610020090')
@ -349,6 +352,83 @@ class ReturnPickupManagementPageTest extends FeatureTestCase
->assertSeeText('该快递公司不在逆向物流报价范围内');
}
public function test_detail_endpoint_returns_partial_for_inline_loading(): void
{
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
$repository = Mockery::mock(ReturnPickupManagementMirrorRepository::class);
$repository->shouldReceive('detail')->once()->withArgs(
static fn (int $id): bool => $id === 900002
)->andReturn([
'main' => (object) [
'Id' => 900002,
'orderId' => 'E202609130089',
'express_company' => '百世',
'orderStatus' => 'WaitExpOrder',
'urgencyLevel' => '正常',
'shopName' => '天猫-欧意朗旗舰店',
'transactionNo' => '3316448031005001998',
'returnExpressNo' => '',
'paymentMethod' => '月结',
'pickupType' => '有主取件',
'expectedPickupTime' => null,
'expressOrderTime' => null,
'expressPickupTime' => null,
'senderName' => '张三',
'senderPhone' => '13800000000',
'province' => '福建省',
'city' => '泉州市',
'district' => '',
'street' => '',
'address' => '某处',
'issueType' => '',
'issueDescription' => '',
'expressFollowStatus' => '',
'expressFollowResult' => '',
'expressFollowDescription' => '',
'note' => '',
'totalReturnQuantity' => 1,
'createTime' => '2026-09-13 10:00:00',
'updateTime' => '2026-09-13 10:00:00',
'creator' => '白沙',
'canceler' => '',
],
'items' => [(object) [
'id' => 2,
'returnProcode' => 'CJ002319',
'actualReturnProcode' => '',
'barcode' => 'CJ002319N1',
'productName' => '住宅家具 - 床',
'platformProductId' => '3316448031005001998',
'platformSkuId' => '4200000001002',
'returnQuantity' => 1,
'actualReturnQuantity' => null,
'unverifiedQuantity' => null,
'returnProgress' => '在途',
'warehouse' => '',
'receiverName' => '逆向物流萧十一郎',
'receiverPhone' => '15370596225',
'receiverAddress' => '江苏省常州市某处',
'grossWeight' => 3.0,
'length' => 87,
'width' => 46,
'height' => 6,
'estimatedFreight' => null,
'actualReturnOrderNo' => '',
]],
'truth_numbers' => [],
]);
$repository->shouldReceive('warehouseDirectory')->once()->andReturn([]);
$this->app->instance(ReturnPickupManagementMirrorRepository::class, $repository);
$this->loginAs()
->get(route('business.courier-payables.return-pickup-management.detail', ['pickup_id' => 900002]))
->assertOk()
->assertSee('id="return-pickup-detail"', false)
->assertSeeText('退货取件单详情')
->assertSeeText('实际运费(元)');
}
public function test_export_streams_filtered_rows_with_actual_freight_and_difference(): void
{
$this->user->attachPermission(