退货取件管理:详情精简置底、当前行高亮、内部滚动与快递公司多选 #308

Merged
zhoushengchen merged 1 commits from feature/zhoushengchen-return-pickup-detail into main 2026-10-12 13:14:54 +08:00
4 changed files with 144 additions and 135 deletions

View File

@ -6315,7 +6315,10 @@ class Pages extends Controller
$filters = [
'created_from' => $this->dateFilter($request->query('created_from')),
'created_to' => $this->dateFilter($request->query('created_to')),
'carrier' => mb_substr(trim((string) $request->query('carrier', '')), 0, 64),
'carrier' => array_values(array_filter(array_map(
static fn ($carrier): string => mb_substr(trim((string) $carrier), 0, 64),
(array) $request->query('carrier', []),
), static fn (string $carrier): bool => $carrier !== '')),
'status' => mb_substr(trim((string) $request->query('status', '')), 0, 64),
'shop' => mb_substr(trim((string) $request->query('shop', '')), 0, 191),
'keyword' => mb_substr(trim((string) $request->query('keyword', '')), 0, 100),

View File

@ -330,7 +330,7 @@ class ReturnPickupManagementMirrorRepository
}
}
/** @param array<string,string> $filters */
/** @param array<string,mixed> $filters */
private function filteredQuery(array $filters): Builder
{
$query = $this->connection->table($this->dataset()->targetTable());
@ -343,8 +343,13 @@ class ReturnPickupManagementMirrorRepository
$query->where('createTime', '<', Carbon::parse($filters['created_to'])->addDay()->toDateString());
}
if (($filters['carrier'] ?? '') !== '') {
$query->where('express_company', $filters['carrier']);
$carriers = array_values(array_filter(array_map(
static fn ($carrier): string => trim((string) $carrier),
(array) ($filters['carrier'] ?? []),
), static fn (string $carrier): bool => $carrier !== ''));
if ($carriers !== []) {
$query->whereIn('express_company', $carriers);
}
if (($filters['status'] ?? '') !== '') {

View File

@ -1,3 +1,5 @@
@include('business.pages.partials.multi-select')
@php
$filters = $returnPickupFilters ?? [];
$rows = $returnPickupRows ?? null;
@ -50,7 +52,7 @@
return route('business.pages.show', array_merge(
['section' => 'courier-payables', 'page' => 'return-pickup-management', 'pickup_id' => $id],
array_filter($filters, static fn ($value): bool => $value !== '' && $value !== null),
));
)) . '#return-pickup-detail';
};
$exportUrl = route(
@ -207,6 +209,18 @@
max-height: max(12rem, calc(100vh - 30rem));
}
.return-pickup-table-wrap--scroll {
max-height: max(16rem, min(56vh, 40rem));
overflow-y: auto;
scrollbar-gutter: stable;
}
.return-pickup-table-wrap--scroll thead th {
position: sticky;
top: 0;
z-index: 1;
}
.return-pickup-table {
width: 100%;
min-width: 1880px;
@ -235,6 +249,9 @@
}
.return-pickup-table tbody tr:hover { background: #fcfaf5; }
.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 .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; }
@ -300,21 +317,8 @@
.return-pickup-page-link:hover { border-color: var(--vip-content-accent-dark, #856632); background: var(--vip-content-accent-soft, #f7f0de); }
.return-pickup-page-link.is-disabled { opacity: .4; pointer-events: none; }
.return-pickup-detail-meta {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 8px 16px;
padding: 12px 14px;
border-bottom: 1px solid #eeeae3;
}
.return-pickup-detail-meta div { display: grid; gap: 2px; }
.return-pickup-detail-meta dt { color: var(--vip-content-muted, #595959); font-size: 0.72rem; }
.return-pickup-detail-meta dd { margin: 0; word-break: break-word; white-space: normal; }
@media (max-width: 1200px) {
.return-pickup-filters { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.return-pickup-detail-meta { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 720px) {
.return-pickup-filters { grid-template-columns: 1fr; }
@ -355,13 +359,16 @@
<input id="pickup-created-to" type="date" name="created_to" value="{{ $filters['created_to'] ?? '' }}">
</div>
<div class="return-pickup-field">
<label for="pickup-carrier">快递公司</label>
<select id="pickup-carrier" name="carrier">
<option value="">全部</option>
@foreach ($carriers as $carrier)
<option value="{{ $carrier }}" @selected(($filters['carrier'] ?? '') === $carrier)>{{ $carrier }}</option>
@endforeach
</select>
<label>快递公司</label>
<span class="cw-ms" id="pickupCarrierFilter" data-name="carrier" data-initial="{{ json_encode(array_values($filters['carrier'] ?? []), JSON_UNESCAPED_UNICODE) }}">
<span class="cw-ms__box">
<button type="button" class="cw-ms__btn" data-role="btn" aria-expanded="false">
<span class="cw-ms__label">全部</span><span class="cw-ms__caret">▾</span>
</button>
<span class="cw-ms__panel" data-role="panel" hidden></span>
<span class="cw-ms__values" data-role="values"></span>
</span>
</span>
</div>
<div class="return-pickup-field">
<label for="pickup-status">订单状态</label>
@ -400,39 +407,94 @@
<div class="return-pickup-notice {{ $pending ? '' : 'is-error' }}">{{ $error }}</div>
@endif
<section class="return-pickup-panel" aria-label="退货取件单列表">
<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">紧急程度</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">操作</th>
</tr>
</thead>
<tbody>
@forelse (($rows ?? collect()) as $row)
<tr class="{{ $detail !== null && (int) ($detail['main']->Id ?? 0) === (int) $row->Id ? 'is-active' : '' }}">
<td class="is-code">{{ $row->orderId ?: '-' }}</td>
<td>{{ $row->express_company ?: '-' }}</td>
<td>
<span class="return-pickup-badge {{ $row->urgencyLevel === '紧急' ? 'is-danger' : '' }}">{{ $row->urgencyLevel ?: '-' }}</span>
</td>
<td>{{ $datetime($row->expectedPickupTime ?? null) }}</td>
<td>
<span class="return-pickup-badge {{ $row->orderStatus === 'ExpCanceled' ? 'is-danger' : (in_array($row->orderStatus, ['WaitExpOrder', 'WaitExpAccept'], true) ? 'is-warning' : 'is-success') }}">
{{ $orderStatusLabels[$row->orderStatus] ?? ($row->orderStatus ?: '-') }}
</span>
</td>
<td>{{ $row->expressFollowStatus ?: '-' }}{{ $row->expressFollowResult ? '(' . $row->expressFollowResult . ')' : '' }}</td>
<td>{{ $row->paymentMethod ?: '-' }}</td>
<td>{{ $row->pickupType ?: '-' }}</td>
<td class="is-clamped" title="{{ $row->shopName }}">{{ $row->shopName ?: '-' }}</td>
<td class="is-code">{{ $row->transactionNo ?: '-' }}</td>
<td>{{ $row->totalReturnQuantity ?? '-' }}</td>
<td class="is-clamped" title="{{ $row->senderName }}">{{ $row->senderName ?: '-' }}</td>
<td>{{ $row->province ?: '-' }}</td>
<td class="is-code">{{ $row->returnExpressNo ?: '-' }}</td>
<td>{{ $datetime($row->createTime ?? null) }}</td>
<td>
<a class="return-pickup-link" href="{{ $detailUrl((int) $row->Id) }}">查看</a>
</td>
</tr>
@empty
<tr>
<td class="return-pickup-empty" colspan="16">
{{ $error !== null ? '退货取件管理数据暂时无法读取。' : '没有符合筛选条件的退货取件单。' }}
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@if ($rows !== null)
<div class="return-pickup-footer">
<span>第 {{ $rows->currentPage() }} / {{ $rows->lastPage() }} 页,本页 {{ $rows->count() }} 条,共 {{ number_format($rows->total()) }} 条</span>
<div class="return-pickup-pagination">
<a class="return-pickup-page-link {{ $rows->onFirstPage() ? 'is-disabled' : '' }}" href="{{ $rows->onFirstPage() ? '#' : $pageUrl($rows->currentPage() - 1) }}" aria-label="上一页">
<span class="material-icons-outlined" aria-hidden="true">chevron_left</span>
</a>
<a class="return-pickup-page-link {{ $rows->hasMorePages() ? '' : 'is-disabled' }}" href="{{ $rows->hasMorePages() ? $pageUrl($rows->currentPage() + 1) : '#' }}" aria-label="下一页">
<span class="material-icons-outlined" aria-hidden="true">chevron_right</span>
</a>
</div>
</div>
@endif
</section>
@if ($detail !== null)
@php
$main = $detail['main'];
@endphp
<section class="return-pickup-panel" aria-label="退货取件单详情">
<section class="return-pickup-panel" id="return-pickup-detail" aria-label="退货取件单详情">
<div class="return-pickup-panel__head">
<span>退货取件单详情 · {{ $main->orderId ?? '-' }}</span>
<span>退货取件单详情 · {{ $main->orderId ?? '-' }} · {{ $main->shopName ?: '-' }} · {{ $main->express_company ?: '-' }}</span>
<a class="return-pickup-link" href="{{ $pageUrl($rows?->currentPage() ?? 1) }}">关闭详情</a>
</div>
<dl class="return-pickup-detail-meta">
<div><dt>订单号</dt><dd>{{ $main->orderId ?? '-' }}</dd></div>
<div><dt>快递公司</dt><dd>{{ $main->express_company ?: '-' }}</dd></div>
<div><dt>订单状态</dt><dd>{{ $orderStatusLabels[$main->orderStatus] ?? ($main->orderStatus ?: '-') }}</dd></div>
<div><dt>紧急程度</dt><dd>{{ $main->urgencyLevel ?: '-' }}</dd></div>
<div><dt>店铺</dt><dd>{{ $main->shopName ?: '-' }}</dd></div>
<div><dt>交易单号</dt><dd>{{ $main->transactionNo ?: '-' }}</dd></div>
<div><dt>总件数</dt><dd>{{ $main->totalReturnQuantity ?? '-' }}</dd></div>
<div><dt>实退快递单号</dt><dd>{{ $main->returnExpressNo ?: ($detail['truth_numbers'][0] ?? '-') }}</dd></div>
<div><dt>实退单号映射</dt><dd>{{ $detail['truth_numbers'] === [] ? '-' : implode('、', $detail['truth_numbers']) }}</dd></div>
<div><dt>付款方式</dt><dd>{{ $main->paymentMethod ?: '-' }}</dd></div>
<div><dt>取件类型</dt><dd>{{ $main->pickupType ?: '-' }}</dd></div>
<div><dt>预计取件时间</dt><dd>{{ $datetime($main->expectedPickupTime ?? null) }}</dd></div>
<div><dt>快递下单 / 取件时间</dt><dd>{{ $datetime($main->expressOrderTime ?? null) }} / {{ $datetime($main->expressPickupTime ?? null) }}</dd></div>
<div><dt>发件人</dt><dd>{{ $main->senderName ?: '-' }} {{ $main->senderPhone ?: '' }}</dd></div>
<div><dt>发件地址</dt><dd>{{ trim(($main->province ?? '') . ($main->city ?? '') . ($main->district ?? '') . ($main->street ?? '') . ($main->address ?? '')) ?: '-' }}</dd></div>
<div><dt>问题类型 / 描述</dt><dd>{{ $main->issueType ?: '-' }} {{ $main->issueDescription ?: '' }}</dd></div>
<div><dt>问题跟进</dt><dd>{{ $main->expressFollowStatus ?: '-' }} / {{ $main->expressFollowResult ?: '-' }}</dd></div>
<div><dt>问题跟进说明</dt><dd>{{ $main->expressFollowDescription ?: '-' }}</dd></div>
<div><dt>备注</dt><dd>{{ $main->note ?: '-' }}</dd></div>
<div><dt>创建 / 更新</dt><dd>{{ $datetime($main->createTime ?? null) }} / {{ $datetime($main->updateTime ?? null) }}</dd></div>
<div><dt>创建人 / 取消人</dt><dd>{{ $main->creator ?: '-' }} / {{ $main->canceler ?: '-' }}</dd></div>
</dl>
<div class="return-pickup-table-wrap">
<div class="return-pickup-table-wrap return-pickup-table-wrap--scroll">
<table class="return-pickup-table">
<thead>
<tr>
@ -538,81 +600,18 @@
</div>
</section>
@endif
<section class="return-pickup-panel" aria-label="退货取件单列表">
<div class="return-pickup-table-wrap">
<table class="return-pickup-table">
<thead>
<tr>
<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">发件省</th>
<th scope="col">实退快递单号</th>
<th scope="col">创建时间</th>
<th scope="col">操作</th>
</tr>
</thead>
<tbody>
@forelse (($rows ?? collect()) as $row)
<tr>
<td class="is-code">{{ $row->orderId ?: '-' }}</td>
<td>{{ $row->express_company ?: '-' }}</td>
<td>
<span class="return-pickup-badge {{ $row->urgencyLevel === '紧急' ? 'is-danger' : '' }}">{{ $row->urgencyLevel ?: '-' }}</span>
</td>
<td>{{ $datetime($row->expectedPickupTime ?? null) }}</td>
<td>
<span class="return-pickup-badge {{ $row->orderStatus === 'ExpCanceled' ? 'is-danger' : (in_array($row->orderStatus, ['WaitExpOrder', 'WaitExpAccept'], true) ? 'is-warning' : 'is-success') }}">
{{ $orderStatusLabels[$row->orderStatus] ?? ($row->orderStatus ?: '-') }}
</span>
</td>
<td>{{ $row->expressFollowStatus ?: '-' }}{{ $row->expressFollowResult ? '(' . $row->expressFollowResult . ')' : '' }}</td>
<td>{{ $row->paymentMethod ?: '-' }}</td>
<td>{{ $row->pickupType ?: '-' }}</td>
<td class="is-clamped" title="{{ $row->shopName }}">{{ $row->shopName ?: '-' }}</td>
<td class="is-code">{{ $row->transactionNo ?: '-' }}</td>
<td>{{ $row->totalReturnQuantity ?? '-' }}</td>
<td class="is-clamped" title="{{ $row->senderName }}">{{ $row->senderName ?: '-' }}</td>
<td>{{ $row->province ?: '-' }}</td>
<td class="is-code">{{ $row->returnExpressNo ?: '-' }}</td>
<td>{{ $datetime($row->createTime ?? null) }}</td>
<td>
<a class="return-pickup-link" href="{{ $detailUrl((int) $row->Id) }}">查看</a>
</td>
</tr>
@empty
<tr>
<td class="return-pickup-empty" colspan="16">
{{ $error !== null ? '退货取件管理数据暂时无法读取。' : '没有符合筛选条件的退货取件单。' }}
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@if ($rows !== null)
<div class="return-pickup-footer">
<span>第 {{ $rows->currentPage() }} / {{ $rows->lastPage() }} 页,本页 {{ $rows->count() }} 条,共 {{ number_format($rows->total()) }} 条</span>
<div class="return-pickup-pagination">
<a class="return-pickup-page-link {{ $rows->onFirstPage() ? 'is-disabled' : '' }}" href="{{ $rows->onFirstPage() ? '#' : $pageUrl($rows->currentPage() - 1) }}" aria-label="上一页">
<span class="material-icons-outlined" aria-hidden="true">chevron_left</span>
</a>
<a class="return-pickup-page-link {{ $rows->hasMorePages() ? '' : 'is-disabled' }}" href="{{ $rows->hasMorePages() ? $pageUrl($rows->currentPage() + 1) : '#' }}" aria-label="下一页">
<span class="material-icons-outlined" aria-hidden="true">chevron_right</span>
</a>
</div>
</div>
@endif
</section>
</div>
<script>
(function () {
var api = window.CaiWuMultiSelect;
var el = document.getElementById('pickupCarrierFilter');
if (api && el) {
api.create(el, {
options: @json(array_values($carriers)),
searchable: true,
searchPlaceholder: '打字找快递公司(可多选)'
});
}
})();
</script>

View File

@ -110,7 +110,7 @@ class ReturnPickupManagementPageTest extends FeatureTestCase
$repository->shouldReceive('paginate')
->once()
->withArgs(function (array $filters, int $page, int $limit): bool {
return $filters['carrier'] === '中通快运'
return $filters['carrier'] === ['中通快运', '速寄递']
&& $filters['keyword'] === 'E202610020090'
&& $filters['created_from'] === '2026-10-01'
&& $page === 1
@ -208,18 +208,23 @@ class ReturnPickupManagementPageTest extends FeatureTestCase
->get(route('business.pages.show', [
'section' => 'courier-payables',
'page' => 'return-pickup-management',
'carrier' => '中通快运',
'carrier' => ['中通快运', '速寄递'],
'keyword' => 'E202610020090',
'created_from' => '2026-10-01',
'pickup_id' => 307390,
]))
->assertOk()
->assertSee('id="pickupCarrierFilter"', false)
->assertSee('data-name="carrier"', false)
->assertSeeText('同步正常')
->assertSeeText('待快递下单')
->assertSeeText('E202610020090')
->assertSeeText('天猫-欧意朗旗舰店')
->assertSeeText('第 1 / 1 页,本页 1 条,共 1 条')
->assertSeeText('退货取件单详情')
->assertSee('id="return-pickup-detail"', false)
->assertSee('is-active', false)
->assertSee('return-pickup-table-wrap--scroll', false)
->assertSeeText('SG007027')
->assertSeeText('SF-RMA-0001')
->assertSeeText('3316448031005001855')
@ -229,8 +234,6 @@ class ReturnPickupManagementPageTest extends FeatureTestCase
->assertSeeText('实退货号')
->assertSeeText('简尚-POP仓')
->assertSeeText('总件数')
->assertSeeText('已与客户确认上门时间')
->assertSeeText('客户要求下午上门')
->assertSeeText('长')
->assertSeeText('93.00')
->assertSeeText('13.00')
@ -251,8 +254,7 @@ class ReturnPickupManagementPageTest extends FeatureTestCase
->assertSeeText('19.00')
->assertSeeText('9.00')
->assertSeeText('1.70')
->assertSeeText('SF1234567890')
->assertSee('value="中通快运" selected', false)
->assertSee('data-initial="[&quot;中通快运&quot;,&quot;速寄递&quot;]"', false)
->assertSee('value="2026-10-01"', false);
}
@ -357,7 +359,7 @@ class ReturnPickupManagementPageTest extends FeatureTestCase
$repository->shouldReceive('exportRows')
->once()
->withArgs(static function (array $filters): bool {
return ($filters['carrier'] ?? '') === '中通快运';
return ($filters['carrier'] ?? []) === ['中通快运'];
})
->andReturn((function (): \Generator {
yield (object) [