feat: add withdrawal table controls #258
@ -9,6 +9,20 @@ use Modules\EcommerceFinance\Repositories\JushuitanShopBalanceRepository;
|
||||
|
||||
class WithdrawalManagementReport
|
||||
{
|
||||
private const SORT_FIELDS = [
|
||||
'platform',
|
||||
'erp_store_name',
|
||||
'store_id',
|
||||
'store_entity',
|
||||
'withdrawal_path',
|
||||
'withdrawal_account',
|
||||
'bank_card',
|
||||
'balance',
|
||||
'reserve_standard',
|
||||
'withdrawal_amount',
|
||||
'compliance',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private readonly JushuitanShopBalanceRepository $shopBalances,
|
||||
) {
|
||||
@ -20,12 +34,13 @@ class WithdrawalManagementReport
|
||||
* platforms:list<string>,paths:list<string>,storeEntities:list<string>,rows:Collection<int,StoreReceivingAccount>,
|
||||
* balances:array<int,array{status:string,balance:?float,source_store_name:?string,source_platform:?string}>,
|
||||
* reserveStandards:array<int,int|null>,summary:array{source:int,matched:int,ambiguous:int,unmatched:int,not_ready:int},
|
||||
* balanceColumnLabel:string,balanceDate:string
|
||||
* balanceColumnLabel:string,balanceDate:string,sort:array{field:string,direction:string}
|
||||
* }
|
||||
*/
|
||||
public function results(Request $request): array
|
||||
{
|
||||
$filters = $this->filters($request);
|
||||
$sort = $this->sorting($request);
|
||||
$platforms = $this->options('store_platform');
|
||||
$paths = $this->options('receiving_method');
|
||||
$storeEntities = $this->options('store_entity_name');
|
||||
@ -58,18 +73,6 @@ class WithdrawalManagementReport
|
||||
);
|
||||
}
|
||||
|
||||
$statusOrder = ['matched' => 0, 'ambiguous' => 1, 'unmatched' => 2, 'not_ready' => 3];
|
||||
$rows = $rows->sort(
|
||||
static function (StoreReceivingAccount $left, StoreReceivingAccount $right) use ($balances, $statusOrder): int {
|
||||
$leftStatus = $balances[(int) $left->id]['status'] ?? 'not_ready';
|
||||
$rightStatus = $balances[(int) $right->id]['status'] ?? 'not_ready';
|
||||
$statusComparison = ($statusOrder[$leftStatus] ?? 99) <=> ($statusOrder[$rightStatus] ?? 99);
|
||||
|
||||
return $statusComparison !== 0
|
||||
? $statusComparison
|
||||
: ((int) $right->id <=> (int) $left->id);
|
||||
},
|
||||
)->values();
|
||||
$reserveStandards = $rows->mapWithKeys(
|
||||
static fn (StoreReceivingAccount $row): array => [
|
||||
(int) $row->id => WithdrawalReserveStandard::amountFor(
|
||||
@ -78,6 +81,9 @@ class WithdrawalManagementReport
|
||||
),
|
||||
],
|
||||
)->all();
|
||||
$rows = $sort['field'] === ''
|
||||
? $this->sortByBalanceStatus($rows, $balances)
|
||||
: $this->sortRows($rows, $sort, $balances, $reserveStandards);
|
||||
|
||||
return [
|
||||
'filters' => $filters,
|
||||
@ -96,6 +102,7 @@ class WithdrawalManagementReport
|
||||
],
|
||||
'balanceColumnLabel' => $today->format('n月j日') . '店铺余额',
|
||||
'balanceDate' => $balanceDate,
|
||||
'sort' => $sort,
|
||||
];
|
||||
}
|
||||
|
||||
@ -131,4 +138,95 @@ class WithdrawalManagementReport
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
/** @return array{field:string,direction:string} */
|
||||
private function sorting(Request $request): array
|
||||
{
|
||||
$field = trim((string) $request->query('sort'));
|
||||
$direction = strtolower(trim((string) $request->query('direction')));
|
||||
|
||||
return [
|
||||
'field' => in_array($field, self::SORT_FIELDS, true) ? $field : '',
|
||||
'direction' => in_array($direction, ['asc', 'desc'], true) ? $direction : 'asc',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection<int,StoreReceivingAccount> $rows
|
||||
* @param array<int,array{status:string,balance:?float,source_store_name:?string,source_platform:?string}> $balances
|
||||
* @return Collection<int,StoreReceivingAccount>
|
||||
*/
|
||||
private function sortByBalanceStatus(Collection $rows, array $balances): Collection
|
||||
{
|
||||
$statusOrder = ['matched' => 0, 'ambiguous' => 1, 'unmatched' => 2, 'not_ready' => 3];
|
||||
|
||||
return $rows->sort(
|
||||
static function (StoreReceivingAccount $left, StoreReceivingAccount $right) use ($balances, $statusOrder): int {
|
||||
$leftStatus = $balances[(int) $left->id]['status'] ?? 'not_ready';
|
||||
$rightStatus = $balances[(int) $right->id]['status'] ?? 'not_ready';
|
||||
$statusComparison = ($statusOrder[$leftStatus] ?? 99) <=> ($statusOrder[$rightStatus] ?? 99);
|
||||
|
||||
return $statusComparison !== 0
|
||||
? $statusComparison
|
||||
: ((int) $right->id <=> (int) $left->id);
|
||||
},
|
||||
)->values();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection<int,StoreReceivingAccount> $rows
|
||||
* @param array{field:string,direction:string} $sort
|
||||
* @param array<int,array{status:string,balance:?float,source_store_name:?string,source_platform:?string}> $balances
|
||||
* @param array<int,int|null> $reserveStandards
|
||||
* @return Collection<int,StoreReceivingAccount>
|
||||
*/
|
||||
private function sortRows(Collection $rows, array $sort, array $balances, array $reserveStandards): Collection
|
||||
{
|
||||
return $rows->sort(function (StoreReceivingAccount $left, StoreReceivingAccount $right) use ($sort, $balances, $reserveStandards): int {
|
||||
$leftValue = $this->sortValue($left, $sort['field'], $balances, $reserveStandards);
|
||||
$rightValue = $this->sortValue($right, $sort['field'], $balances, $reserveStandards);
|
||||
$comparison = $this->compareSortValues($leftValue, $rightValue, $sort['direction']);
|
||||
|
||||
return $comparison !== 0
|
||||
? $comparison
|
||||
: ((int) $right->id <=> (int) $left->id);
|
||||
})->values();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int,array{status:string,balance:?float,source_store_name:?string,source_platform:?string}> $balances
|
||||
* @param array<int,int|null> $reserveStandards
|
||||
*/
|
||||
private function sortValue(StoreReceivingAccount $row, string $field, array $balances, array $reserveStandards): mixed
|
||||
{
|
||||
return match ($field) {
|
||||
'platform' => $row->store_platform,
|
||||
'erp_store_name' => $row->erp_store_name,
|
||||
'store_id' => $row->store_id,
|
||||
'store_entity' => $row->store_entity_name,
|
||||
'withdrawal_path' => $row->withdrawal_path,
|
||||
'withdrawal_account' => $row->withdrawal_account_name,
|
||||
'bank_card' => $row->bank_card_last_four,
|
||||
'balance' => ($balances[(int) $row->id]['status'] ?? null) === 'matched'
|
||||
? $balances[(int) $row->id]['balance']
|
||||
: null,
|
||||
'reserve_standard' => $reserveStandards[(int) $row->id] ?? null,
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
private function compareSortValues(mixed $left, mixed $right, string $direction): int
|
||||
{
|
||||
$leftEmpty = $left === null || $left === '';
|
||||
$rightEmpty = $right === null || $right === '';
|
||||
if ($leftEmpty || $rightEmpty) {
|
||||
return $leftEmpty === $rightEmpty ? 0 : ($leftEmpty ? 1 : -1);
|
||||
}
|
||||
|
||||
$comparison = is_numeric($left) && is_numeric($right)
|
||||
? ((float) $left <=> (float) $right)
|
||||
: strnatcasecmp((string) $left, (string) $right);
|
||||
|
||||
return $direction === 'desc' ? -$comparison : $comparison;
|
||||
}
|
||||
}
|
||||
|
||||
@ -894,6 +894,7 @@ class Pages extends Controller
|
||||
'withdrawalBalanceSummary' => $report['summary'],
|
||||
'withdrawalBalanceColumnLabel' => $report['balanceColumnLabel'],
|
||||
'withdrawalBalanceDate' => $report['balanceDate'],
|
||||
'withdrawalSort' => $report['sort'],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@ -1,28 +1,69 @@
|
||||
@php
|
||||
$withdrawalImportPreview = session('withdrawalImportPreview');
|
||||
$withdrawalOpenImport = is_array($withdrawalImportPreview) || $errors->hasAny(['import', 'preview_token']);
|
||||
$withdrawalQuery = array_filter(
|
||||
$withdrawalFilterQuery = array_filter(
|
||||
request()->only(['platform', 'withdrawal_path', 'store_entity', 'keyword', 'balance_status']),
|
||||
static fn ($value): bool => filled($value),
|
||||
);
|
||||
$withdrawalQuery = array_filter(
|
||||
request()->only(['platform', 'withdrawal_path', 'store_entity', 'keyword', 'balance_status', 'sort', 'direction']),
|
||||
static fn ($value): bool => filled($value),
|
||||
);
|
||||
$withdrawalSortUrl = static function (string $field, string $direction) use ($withdrawalFilterQuery): string {
|
||||
$query = http_build_query(array_merge($withdrawalFilterQuery, [
|
||||
'sort' => $field,
|
||||
'direction' => $direction,
|
||||
]));
|
||||
|
||||
return request()->url() . '?' . $query;
|
||||
};
|
||||
$withdrawalColumns = [
|
||||
['key' => 'platform', 'label' => '店铺平台', 'numeric' => false, 'title' => null],
|
||||
['key' => 'erp_store_name', 'label' => 'ERP店铺名称', 'numeric' => false, 'title' => null],
|
||||
['key' => 'store_id', 'label' => '平台店铺ID', 'numeric' => false, 'title' => null],
|
||||
['key' => 'store_entity', 'label' => '店铺主体', 'numeric' => false, 'title' => null],
|
||||
['key' => 'withdrawal_path', 'label' => '提现路径', 'numeric' => false, 'title' => null],
|
||||
['key' => 'withdrawal_account', 'label' => '提现账户名称', 'numeric' => false, 'title' => null],
|
||||
['key' => 'bank_card', 'label' => '银行卡号', 'numeric' => false, 'title' => null],
|
||||
['key' => 'balance', 'label' => $withdrawalBalanceColumnLabel, 'numeric' => true, 'title' => '余额数据日期:' . $withdrawalBalanceDate],
|
||||
['key' => 'reserve_standard', 'label' => '预留标准', 'numeric' => true, 'title' => null],
|
||||
['key' => 'withdrawal_amount', 'label' => '提现金额', 'numeric' => true, 'title' => null],
|
||||
['key' => 'compliance', 'label' => '是否达标', 'numeric' => false, 'title' => null],
|
||||
];
|
||||
@endphp
|
||||
|
||||
<section class="biz-card mt-6 withdrawal-management" aria-label="提现配置列表">
|
||||
<div class="biz-card-head">
|
||||
<div class="biz-card-title">
|
||||
<span class="material-icons-outlined" aria-hidden="true">account_balance_wallet</span>
|
||||
<span>提现配置</span>
|
||||
<div class="withdrawal-management__heading">
|
||||
<div class="biz-card-title">
|
||||
<span class="material-icons-outlined" aria-hidden="true">account_balance_wallet</span>
|
||||
<span>提现配置</span>
|
||||
</div>
|
||||
<div class="withdrawal-management__summary">
|
||||
<span class="biz-badge">余额源 {{ number_format($withdrawalBalanceSummary['source']) }} 条</span>
|
||||
<span class="biz-badge biz-badge-yes">已显示余额 {{ number_format($withdrawalBalanceSummary['matched']) }} 条</span>
|
||||
@if ($withdrawalBalanceSummary['ambiguous'] > 0)
|
||||
<span class="biz-badge biz-badge-no">多条匹配 {{ number_format($withdrawalBalanceSummary['ambiguous']) }} 条</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="biz-badge">余额源 {{ number_format($withdrawalBalanceSummary['source']) }} 条</span>
|
||||
<span class="biz-badge biz-badge-yes">已显示余额 {{ number_format($withdrawalBalanceSummary['matched']) }} 条</span>
|
||||
@if ($withdrawalBalanceSummary['ambiguous'] > 0)
|
||||
<span class="biz-badge biz-badge-no">多条匹配 {{ number_format($withdrawalBalanceSummary['ambiguous']) }} 条</span>
|
||||
@endif
|
||||
<div class="withdrawal-management__actions" aria-label="提现管理导入导出">
|
||||
<button type="button" class="biz-btn biz-btn-ghost" data-withdrawal-import-open>
|
||||
<span class="material-icons-outlined" aria-hidden="true">upload_file</span>
|
||||
<span>导入</span>
|
||||
</button>
|
||||
<a class="biz-btn biz-btn-primary" href="{{ route('business.withdrawal-management.export', $withdrawalQuery) }}">
|
||||
<span class="material-icons-outlined" aria-hidden="true">download</span>
|
||||
<span>导出</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form class="biz-filter-bar" method="GET" action="{{ request()->url() }}" aria-label="提现管理筛选">
|
||||
@if ($withdrawalSort['field'] !== '')
|
||||
<input type="hidden" name="sort" value="{{ $withdrawalSort['field'] }}">
|
||||
<input type="hidden" name="direction" value="{{ $withdrawalSort['direction'] }}">
|
||||
@endif
|
||||
<div class="biz-filter-field">
|
||||
<label for="withdrawal-platform">店铺平台</label>
|
||||
<select id="withdrawal-platform" class="biz-filter-select" name="platform">
|
||||
@ -65,34 +106,49 @@
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="biz-btn biz-btn-primary">筛选</button>
|
||||
<button type="button" class="biz-btn biz-btn-ghost" data-withdrawal-import-open>
|
||||
<span class="material-icons-outlined" aria-hidden="true">upload_file</span>
|
||||
<span>导入</span>
|
||||
</button>
|
||||
<a class="biz-btn biz-btn-ghost" href="{{ route('business.withdrawal-management.export', $withdrawalQuery) }}">
|
||||
<span class="material-icons-outlined" aria-hidden="true">download</span>
|
||||
<span>导出</span>
|
||||
</a>
|
||||
@if (request()->hasAny(['platform', 'withdrawal_path', 'store_entity', 'keyword', 'balance_status']))
|
||||
<a class="biz-btn biz-btn-ghost" href="{{ request()->url() }}">重置</a>
|
||||
@endif
|
||||
</form>
|
||||
|
||||
<div class="biz-table-wrap">
|
||||
<table class="biz-table" data-finance-table-name="withdrawal-management-v3">
|
||||
<table
|
||||
class="biz-table"
|
||||
aria-label="提现管理表格"
|
||||
data-finance-table-name="withdrawal-management-v4"
|
||||
data-finance-table-native
|
||||
data-withdrawal-table
|
||||
data-column-storage-key="caiwu.withdrawal.columns.v1.{{ auth()->id() ?? 'guest' }}"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>店铺平台</th>
|
||||
<th>ERP店铺名称</th>
|
||||
<th>平台店铺ID</th>
|
||||
<th>店铺主体</th>
|
||||
<th>提现路径</th>
|
||||
<th>提现账户名称</th>
|
||||
<th>银行卡号</th>
|
||||
<th class="num" title="余额数据日期:{{ $withdrawalBalanceDate }}">{{ $withdrawalBalanceColumnLabel }}</th>
|
||||
<th class="num">预留标准</th>
|
||||
<th class="num">提现金额</th>
|
||||
<th>是否达标</th>
|
||||
@foreach ($withdrawalColumns as $column)
|
||||
<th
|
||||
@class(['num' => $column['numeric']])
|
||||
data-withdrawal-column="{{ $column['key'] }}"
|
||||
@if ($column['title']) title="{{ $column['title'] }}" @endif
|
||||
@if ($withdrawalSort['field'] === $column['key']) aria-sort="{{ $withdrawalSort['direction'] === 'asc' ? 'ascending' : 'descending' }}" @endif
|
||||
>
|
||||
<div class="withdrawal-table-head">
|
||||
<span>{{ $column['label'] }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="withdrawal-table-head__menu"
|
||||
data-withdrawal-column-trigger
|
||||
data-sort-field="{{ $column['key'] }}"
|
||||
data-sort-label="{{ $column['label'] }}"
|
||||
data-sort-asc="{{ $withdrawalSortUrl($column['key'], 'asc') }}"
|
||||
data-sort-desc="{{ $withdrawalSortUrl($column['key'], 'desc') }}"
|
||||
aria-label="设置{{ $column['label'] }}排序和显示字段"
|
||||
aria-haspopup="dialog"
|
||||
aria-expanded="false"
|
||||
title="排序和显示字段"
|
||||
>
|
||||
<span class="material-icons-outlined" aria-hidden="true">{{ $withdrawalSort['field'] === $column['key'] ? ($withdrawalSort['direction'] === 'asc' ? 'arrow_upward' : 'arrow_downward') : 'unfold_more' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</th>
|
||||
@endforeach
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -114,17 +170,17 @@
|
||||
$reserveStandard = $withdrawalReserveStandards[(int) $row->id] ?? null;
|
||||
@endphp
|
||||
<tr>
|
||||
<td>{{ $row->store_platform ?: '未配置' }}</td>
|
||||
<td>{{ $row->erp_store_name ?: '-' }}</td>
|
||||
<td>{{ $row->store_id ?: '-' }}</td>
|
||||
<td>{{ $row->store_entity_name ?: '-' }}</td>
|
||||
<td>{{ $row->withdrawal_path ?: '未配置' }}</td>
|
||||
<td>{{ $row->withdrawal_account_name ?: '未配置' }}</td>
|
||||
<td>{{ $bankCard }}</td>
|
||||
<td class="num" title="{{ $balanceTitle }}">{{ $balanceText }}</td>
|
||||
<td class="num">{{ $reserveStandard === null ? '待配置' : number_format($reserveStandard, 2) }}</td>
|
||||
<td class="num">待接入</td>
|
||||
<td><span class="biz-badge biz-badge-no">待核验</span></td>
|
||||
<td data-withdrawal-column="platform">{{ $row->store_platform ?: '未配置' }}</td>
|
||||
<td data-withdrawal-column="erp_store_name">{{ $row->erp_store_name ?: '-' }}</td>
|
||||
<td data-withdrawal-column="store_id">{{ $row->store_id ?: '-' }}</td>
|
||||
<td data-withdrawal-column="store_entity">{{ $row->store_entity_name ?: '-' }}</td>
|
||||
<td data-withdrawal-column="withdrawal_path">{{ $row->withdrawal_path ?: '未配置' }}</td>
|
||||
<td data-withdrawal-column="withdrawal_account">{{ $row->withdrawal_account_name ?: '未配置' }}</td>
|
||||
<td data-withdrawal-column="bank_card">{{ $bankCard }}</td>
|
||||
<td class="num" data-withdrawal-column="balance" title="{{ $balanceTitle }}">{{ $balanceText }}</td>
|
||||
<td class="num" data-withdrawal-column="reserve_standard">{{ $reserveStandard === null ? '待配置' : number_format($reserveStandard, 2) }}</td>
|
||||
<td class="num" data-withdrawal-column="withdrawal_amount">待接入</td>
|
||||
<td data-withdrawal-column="compliance"><span class="biz-badge biz-badge-no">待核验</span></td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
@ -138,6 +194,42 @@
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div
|
||||
class="withdrawal-column-menu"
|
||||
data-withdrawal-column-menu
|
||||
data-current-sort="{{ $withdrawalSort['field'] }}"
|
||||
data-current-direction="{{ $withdrawalSort['direction'] }}"
|
||||
role="dialog"
|
||||
aria-label="表格排序和显示字段"
|
||||
hidden
|
||||
>
|
||||
<div class="withdrawal-column-menu__title" data-withdrawal-menu-title>表格设置</div>
|
||||
<div class="withdrawal-column-menu__sort" aria-label="排序方式">
|
||||
<a href="#" data-withdrawal-sort-asc>
|
||||
<span class="material-icons-outlined" aria-hidden="true">arrow_upward</span>
|
||||
<span>升序</span>
|
||||
</a>
|
||||
<a href="#" data-withdrawal-sort-desc>
|
||||
<span class="material-icons-outlined" aria-hidden="true">arrow_downward</span>
|
||||
<span>降序</span>
|
||||
</a>
|
||||
</div>
|
||||
<fieldset class="withdrawal-column-menu__fields">
|
||||
<legend>显示字段</legend>
|
||||
@foreach ($withdrawalColumns as $column)
|
||||
<label>
|
||||
<input type="checkbox" value="{{ $column['key'] }}" data-withdrawal-column-toggle checked>
|
||||
<span>{{ $column['label'] }}</span>
|
||||
</label>
|
||||
@endforeach
|
||||
</fieldset>
|
||||
<div class="withdrawal-column-menu__actions">
|
||||
<button type="button" data-withdrawal-columns-all>全选</button>
|
||||
<button type="button" data-withdrawal-columns-reset>恢复默认</button>
|
||||
</div>
|
||||
<p class="withdrawal-column-menu__status" data-withdrawal-column-status aria-live="polite"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="biz-pagination">
|
||||
@ -242,9 +334,36 @@
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.withdrawal-management .biz-card-head { align-items:flex-start; gap:1rem; }
|
||||
.withdrawal-management .biz-btn .material-icons-outlined { font-size: 1.1rem; }
|
||||
.withdrawal-management .biz-btn-primary { border-color: #856632; background: #856632; color: #fff; }
|
||||
.withdrawal-management .biz-btn-primary:hover { background: #6f5429; }
|
||||
.withdrawal-management__heading { display:flex; min-width:0; flex-direction:column; gap:.55rem; }
|
||||
.withdrawal-management__summary { display:flex; flex-wrap:wrap; gap:.35rem; }
|
||||
.withdrawal-management__actions { display:flex; flex:0 0 auto; align-items:center; gap:.5rem; margin-left:auto; }
|
||||
.withdrawal-management__actions .biz-btn { min-width:5.5rem; justify-content:center; }
|
||||
.withdrawal-table-head { display:flex; min-width:max-content; align-items:center; justify-content:space-between; gap:.35rem; }
|
||||
.withdrawal-management th.num .withdrawal-table-head { justify-content:flex-end; }
|
||||
.withdrawal-table-head__menu { display:inline-flex; width:1.65rem; height:1.65rem; flex:0 0 1.65rem; align-items:center; justify-content:center; border:1px solid transparent; border-radius:4px; color:#856632; }
|
||||
.withdrawal-table-head__menu:hover, .withdrawal-table-head__menu:focus-visible { border-color:#b9954f; background:#fff; outline:none; box-shadow:0 0 0 3px rgba(133,102,50,.2); }
|
||||
.withdrawal-table-head__menu .material-icons-outlined { font-size:1rem; }
|
||||
.withdrawal-column-menu { position:fixed; z-index:80; width:min(18rem,calc(100vw - 1.5rem)); max-height:min(32rem,calc(100vh - 1.5rem)); overflow:auto; padding:.75rem; border:1px solid #d8d2c7; border-radius:8px; background:#fff; color:#171717; box-shadow:0 4px 8px rgba(23,23,23,.1); }
|
||||
.withdrawal-column-menu[hidden] { display:none; }
|
||||
.withdrawal-column-menu__title { margin:0 0 .5rem; font-size:.8125rem; font-weight:600; }
|
||||
.withdrawal-column-menu__sort { display:grid; grid-template-columns:1fr 1fr; gap:.35rem; padding-bottom:.65rem; border-bottom:1px solid #d8d2c7; }
|
||||
.withdrawal-column-menu__sort a { display:flex; min-height:2rem; align-items:center; gap:.35rem; padding:0 .55rem; border:1px solid #d8d2c7; border-radius:6px; color:#171717; font-size:.75rem; text-decoration:none; }
|
||||
.withdrawal-column-menu__sort a:hover, .withdrawal-column-menu__sort a:focus-visible, .withdrawal-column-menu__sort a.is-active { border-color:#856632; background:#f7f0de; color:#856632; outline:none; }
|
||||
.withdrawal-column-menu__sort .material-icons-outlined { font-size:1rem; }
|
||||
.withdrawal-column-menu__fields { display:grid; grid-template-columns:1fr 1fr; gap:.25rem .6rem; margin:.65rem 0 0; padding:0; border:0; }
|
||||
.withdrawal-column-menu__fields legend { grid-column:1 / -1; width:100%; margin-bottom:.25rem; color:#595959; font-size:.75rem; font-weight:600; }
|
||||
.withdrawal-column-menu__fields label { display:flex; min-width:0; min-height:1.75rem; align-items:center; gap:.4rem; font-size:.75rem; cursor:pointer; }
|
||||
.withdrawal-column-menu__fields input { flex:0 0 auto; accent-color:#856632; }
|
||||
.withdrawal-column-menu__fields span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||
.withdrawal-column-menu__actions { display:flex; gap:.35rem; margin-top:.6rem; padding-top:.6rem; border-top:1px solid #d8d2c7; }
|
||||
.withdrawal-column-menu__actions button { min-height:1.8rem; padding:0 .55rem; border:1px solid #d8d2c7; border-radius:5px; background:#fff; color:#856632; font-size:.75rem; }
|
||||
.withdrawal-column-menu__actions button:hover, .withdrawal-column-menu__actions button:focus-visible { border-color:#856632; background:#f7f0de; outline:none; }
|
||||
.withdrawal-column-menu__status { min-height:1rem; margin:.45rem 0 0; color:#595959; font-size:.7rem; }
|
||||
.withdrawal-management [data-withdrawal-column].is-column-hidden { display:none !important; }
|
||||
.withdrawal-import-modal .biz-modal-card { max-width: 820px; border: 1px solid #d8d2c7; border-radius: 8px; }
|
||||
.withdrawal-import__head { display:flex; align-items:flex-start; justify-content:space-between; gap:1rem; margin-bottom:1rem; }
|
||||
.withdrawal-import__head .biz-modal-title { margin:0; }
|
||||
@ -262,6 +381,10 @@
|
||||
.withdrawal-import__empty, .withdrawal-import__error { padding:.85rem 1rem; border:1px solid #d8d2c7; color:#595959; }
|
||||
.withdrawal-import__error { margin-bottom:1rem; border-color:#d6a4a4; background:#fff7f7; color:#8a1f1f; }
|
||||
@media (max-width: 640px) {
|
||||
.withdrawal-management .biz-card-head { align-items:stretch; }
|
||||
.withdrawal-management__actions { width:100%; margin-left:0; }
|
||||
.withdrawal-management__actions .biz-btn { flex:1 1 0; }
|
||||
.withdrawal-column-menu__fields { grid-template-columns:1fr; }
|
||||
.withdrawal-import-modal { padding:1rem; }
|
||||
.withdrawal-import__stats { grid-template-columns:1fr; }
|
||||
.withdrawal-import__stats > div + div { border-left:0; border-top:1px solid #d8d2c7; }
|
||||
@ -290,3 +413,119 @@
|
||||
if (modal.dataset.autoOpen === 'true') open();
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
const table = document.querySelector('[data-withdrawal-table]');
|
||||
const menu = document.querySelector('[data-withdrawal-column-menu]');
|
||||
if (!table || !menu) return;
|
||||
|
||||
const triggers = [...table.querySelectorAll('[data-withdrawal-column-trigger]')];
|
||||
const checks = [...menu.querySelectorAll('[data-withdrawal-column-toggle]')];
|
||||
const ascending = menu.querySelector('[data-withdrawal-sort-asc]');
|
||||
const descending = menu.querySelector('[data-withdrawal-sort-desc]');
|
||||
const title = menu.querySelector('[data-withdrawal-menu-title]');
|
||||
const status = menu.querySelector('[data-withdrawal-column-status]');
|
||||
const storageKey = table.dataset.columnStorageKey;
|
||||
let activeTrigger = null;
|
||||
|
||||
const checkedKeys = () => checks.filter((check) => check.checked).map((check) => check.value);
|
||||
const close = () => {
|
||||
menu.hidden = true;
|
||||
activeTrigger?.setAttribute('aria-expanded', 'false');
|
||||
activeTrigger = null;
|
||||
};
|
||||
const applyVisibility = (save = true, message = null) => {
|
||||
const visible = new Set(checkedKeys());
|
||||
table.querySelectorAll('[data-withdrawal-column]').forEach((cell) => {
|
||||
cell.classList.toggle('is-column-hidden', !visible.has(cell.dataset.withdrawalColumn));
|
||||
});
|
||||
table.querySelectorAll('tbody td[colspan]').forEach((cell) => {
|
||||
cell.colSpan = Math.max(visible.size, 1);
|
||||
});
|
||||
status.textContent = message || `已显示 ${visible.size}/${checks.length} 个字段`;
|
||||
if (save && storageKey) {
|
||||
try { localStorage.setItem(storageKey, JSON.stringify([...visible])); } catch (_) {}
|
||||
}
|
||||
};
|
||||
const position = (trigger) => {
|
||||
const triggerRect = trigger.getBoundingClientRect();
|
||||
const menuRect = menu.getBoundingClientRect();
|
||||
const gap = 6;
|
||||
const edge = 12;
|
||||
let left = triggerRect.right - menuRect.width;
|
||||
let top = triggerRect.bottom + gap;
|
||||
left = Math.max(edge, Math.min(left, window.innerWidth - menuRect.width - edge));
|
||||
if (top + menuRect.height > window.innerHeight - edge) {
|
||||
top = Math.max(edge, triggerRect.top - menuRect.height - gap);
|
||||
}
|
||||
menu.style.left = `${left}px`;
|
||||
menu.style.top = `${top}px`;
|
||||
};
|
||||
const open = (trigger) => {
|
||||
if (activeTrigger === trigger && !menu.hidden) {
|
||||
close();
|
||||
return;
|
||||
}
|
||||
activeTrigger?.setAttribute('aria-expanded', 'false');
|
||||
activeTrigger = trigger;
|
||||
trigger.setAttribute('aria-expanded', 'true');
|
||||
title.textContent = `${trigger.dataset.sortLabel} · 排序与字段`;
|
||||
ascending.href = trigger.dataset.sortAsc;
|
||||
descending.href = trigger.dataset.sortDesc;
|
||||
const current = menu.dataset.currentSort === trigger.dataset.sortField;
|
||||
ascending.classList.toggle('is-active', current && menu.dataset.currentDirection === 'asc');
|
||||
descending.classList.toggle('is-active', current && menu.dataset.currentDirection === 'desc');
|
||||
menu.hidden = false;
|
||||
position(trigger);
|
||||
ascending.focus({ preventScroll: true });
|
||||
};
|
||||
|
||||
if (storageKey) {
|
||||
try {
|
||||
const saved = JSON.parse(localStorage.getItem(storageKey) || 'null');
|
||||
if (Array.isArray(saved) && saved.length > 0) {
|
||||
const available = new Set(checks.map((check) => check.value));
|
||||
checks.forEach((check) => { check.checked = saved.includes(check.value) || !available.has(check.value); });
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
if (checkedKeys().length === 0) checks.forEach((check) => { check.checked = true; });
|
||||
applyVisibility(false);
|
||||
|
||||
triggers.forEach((trigger) => trigger.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
open(trigger);
|
||||
}));
|
||||
checks.forEach((check) => check.addEventListener('change', () => {
|
||||
if (checkedKeys().length === 0) {
|
||||
check.checked = true;
|
||||
applyVisibility(false, '至少保留一个显示字段');
|
||||
return;
|
||||
}
|
||||
applyVisibility();
|
||||
}));
|
||||
menu.querySelector('[data-withdrawal-columns-all]')?.addEventListener('click', () => {
|
||||
checks.forEach((check) => { check.checked = true; });
|
||||
applyVisibility();
|
||||
});
|
||||
menu.querySelector('[data-withdrawal-columns-reset]')?.addEventListener('click', () => {
|
||||
checks.forEach((check) => { check.checked = true; });
|
||||
if (storageKey) {
|
||||
try { localStorage.removeItem(storageKey); } catch (_) {}
|
||||
}
|
||||
applyVisibility(false, '已恢复默认字段');
|
||||
});
|
||||
menu.addEventListener('click', (event) => event.stopPropagation());
|
||||
document.addEventListener('click', close);
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape' && !menu.hidden) {
|
||||
const trigger = activeTrigger;
|
||||
close();
|
||||
trigger?.focus();
|
||||
}
|
||||
});
|
||||
window.addEventListener('resize', close, { passive: true });
|
||||
table.closest('.biz-table-wrap')?.addEventListener('scroll', close, { passive: true });
|
||||
})();
|
||||
</script>
|
||||
|
||||
@ -142,19 +142,22 @@ class ManagementCrudTest extends FeatureTestCase
|
||||
->assertSeeText('提现金额')
|
||||
->assertSeeText('是否达标')
|
||||
->assertSeeText('待核验')
|
||||
->assertSee('data-finance-table-name="withdrawal-management-v3"', false)
|
||||
->assertSee('data-finance-table-name="withdrawal-management-v4"', false)
|
||||
->assertSee('data-finance-table-native', false)
|
||||
->assertSee('data-withdrawal-column-menu', false)
|
||||
->assertSee('data-withdrawal-column-trigger', false)
|
||||
->assertSeeInOrder([
|
||||
'<th>店铺平台</th>',
|
||||
'<th>ERP店铺名称</th>',
|
||||
'<th>平台店铺ID</th>',
|
||||
'<th>店铺主体</th>',
|
||||
'<th>提现路径</th>',
|
||||
'<th>提现账户名称</th>',
|
||||
'<th>银行卡号</th>',
|
||||
'<th class="num" title="余额数据日期:2026-09-12">9月13日店铺余额</th>',
|
||||
'<th class="num">预留标准</th>',
|
||||
'<th class="num">提现金额</th>',
|
||||
'<th>是否达标</th>',
|
||||
'data-withdrawal-column="platform"',
|
||||
'data-withdrawal-column="erp_store_name"',
|
||||
'data-withdrawal-column="store_id"',
|
||||
'data-withdrawal-column="store_entity"',
|
||||
'data-withdrawal-column="withdrawal_path"',
|
||||
'data-withdrawal-column="withdrawal_account"',
|
||||
'data-withdrawal-column="bank_card"',
|
||||
'data-withdrawal-column="balance"',
|
||||
'data-withdrawal-column="reserve_standard"',
|
||||
'data-withdrawal-column="withdrawal_amount"',
|
||||
'data-withdrawal-column="compliance"',
|
||||
], false)
|
||||
->assertSee('aria-label="提现管理筛选"', false)
|
||||
->assertSee('name="store_entity"', false)
|
||||
|
||||
@ -56,10 +56,53 @@ class WithdrawalManagementImportExportTest extends FeatureTestCase
|
||||
->assertSeeText('导入')
|
||||
->assertSeeText('导出')
|
||||
->assertSee('data-withdrawal-import-open', false)
|
||||
->assertSee('class="withdrawal-management__actions"', false)
|
||||
->assertSee('data-withdrawal-column-trigger', false)
|
||||
->assertSee('data-withdrawal-column-toggle', false)
|
||||
->assertSee('data-withdrawal-sort-asc', false)
|
||||
->assertSee('data-withdrawal-sort-desc', false)
|
||||
->assertSee(route('business.withdrawal-management.export', ['platform' => '天猫']), false)
|
||||
->assertSee(route('business.withdrawal-management.template'), false);
|
||||
}
|
||||
|
||||
public function test_page_sorts_all_filtered_rows_before_pagination(): void
|
||||
{
|
||||
foreach (['B排序测试店', 'A排序测试店'] as $name) {
|
||||
StoreReceivingAccount::query()->create([
|
||||
'company_id' => $this->company->id,
|
||||
'store_platform' => '排序测试平台',
|
||||
'erp_store_name' => $name,
|
||||
'store_entity_name' => '排序测试主体',
|
||||
]);
|
||||
}
|
||||
|
||||
$ascending = $this->loginAs()->get(route('business.pages.show', [
|
||||
'section' => 'fund-management',
|
||||
'page' => 'withdrawal-management',
|
||||
'platform' => '排序测试平台',
|
||||
'sort' => 'erp_store_name',
|
||||
'direction' => 'asc',
|
||||
]));
|
||||
$descending = $this->get(route('business.pages.show', [
|
||||
'section' => 'fund-management',
|
||||
'page' => 'withdrawal-management',
|
||||
'platform' => '排序测试平台',
|
||||
'sort' => 'erp_store_name',
|
||||
'direction' => 'desc',
|
||||
]));
|
||||
|
||||
$ascending
|
||||
->assertOk()
|
||||
->assertSeeInOrder(['A排序测试店', 'B排序测试店'])
|
||||
->assertSee('name="sort" value="erp_store_name"', false)
|
||||
->assertSee('name="direction" value="asc"', false)
|
||||
->assertSee('aria-sort="ascending"', false);
|
||||
$descending
|
||||
->assertOk()
|
||||
->assertSeeInOrder(['B排序测试店', 'A排序测试店'])
|
||||
->assertSee('aria-sort="descending"', false);
|
||||
}
|
||||
|
||||
public function test_export_contains_all_filtered_rows_and_excludes_passwords(): void
|
||||
{
|
||||
StoreReceivingAccount::query()->create([
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user