CaiWuXiTong/resources/views/business/pages/fund-management/withdrawal-management.blade.php

555 lines
33 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@php
$withdrawalImportPreview = session('withdrawalImportPreview');
$withdrawalOpenImport = is_array($withdrawalImportPreview) || $errors->hasAny(['import', 'preview_token']);
$withdrawalFilterQuery = array_filter(
request()->only(['platform', 'withdrawal_path', 'store_entity', 'keyword', 'balance_status', 'balance_date']),
static fn ($value): bool => filled($value),
);
$withdrawalQuery = array_filter(
request()->only(['platform', 'withdrawal_path', 'store_entity', 'keyword', 'balance_status', 'balance_date', '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="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 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-balance-date">余额日期</label>
<input
id="withdrawal-balance-date"
class="biz-filter-input"
type="date"
name="balance_date"
value="{{ $withdrawalFilters['balanceDate'] }}"
@if ($withdrawalEarliestBalanceDate) min="{{ $withdrawalEarliestBalanceDate }}" @endif
@if ($withdrawalLatestBalanceDate) max="{{ $withdrawalLatestBalanceDate }}" @endif
title="不选时显示最新余额"
>
</div>
<div class="biz-filter-field">
<label for="withdrawal-platform">店铺平台</label>
<select id="withdrawal-platform" class="biz-filter-select" name="platform">
<option value="">全部</option>
@foreach ($withdrawalPlatforms as $option)
<option value="{{ $option }}" @selected($withdrawalFilters['platform'] === $option)>{{ $option }}</option>
@endforeach
</select>
</div>
<div class="biz-filter-field">
<label for="withdrawal-path">提现路径</label>
<select id="withdrawal-path" class="biz-filter-select" name="withdrawal_path">
<option value="">全部</option>
@foreach ($withdrawalPaths as $option)
<option value="{{ $option }}" @selected($withdrawalFilters['withdrawalPath'] === $option)>{{ $option }}</option>
@endforeach
</select>
</div>
<div class="biz-filter-field">
<label for="withdrawal-store-entity">店铺主体</label>
<select id="withdrawal-store-entity" class="biz-filter-select" name="store_entity">
<option value="">全部</option>
@foreach ($withdrawalStoreEntities as $option)
<option value="{{ $option }}" @selected($withdrawalFilters['storeEntity'] === $option)>{{ $option }}</option>
@endforeach
</select>
</div>
<div class="biz-filter-field">
<label for="withdrawal-keyword">ERP店铺名称</label>
<input id="withdrawal-keyword" class="biz-filter-input" type="search" name="keyword" value="{{ $withdrawalFilters['keyword'] }}" placeholder="输入店铺名称">
</div>
<div class="biz-filter-field">
<label for="withdrawal-balance-status">余额状态</label>
<select id="withdrawal-balance-status" class="biz-filter-select" name="balance_status">
<option value="">全部</option>
<option value="matched" @selected($withdrawalFilters['balanceStatus'] === 'matched')>已接入</option>
<option value="unmatched" @selected($withdrawalFilters['balanceStatus'] === 'unmatched')>未匹配</option>
<option value="ambiguous" @selected($withdrawalFilters['balanceStatus'] === 'ambiguous')>多条匹配</option>
<option value="not_ready" @selected($withdrawalFilters['balanceStatus'] === 'not_ready')>待同步</option>
</select>
</div>
<button type="submit" class="biz-btn biz-btn-primary">筛选</button>
@if (request()->hasAny(['platform', 'withdrawal_path', 'store_entity', 'keyword', 'balance_status', 'balance_date']))
<a class="biz-btn biz-btn-ghost" href="{{ request()->url() }}">重置</a>
@endif
</form>
<div class="biz-table-wrap">
<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>
@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>
@forelse ($withdrawalRows as $row)
@php
$bankCard = filled($row->bank_card_last_four)
? '**** ' . $row->bank_card_last_four
: '未配置';
$balanceMatch = $withdrawalBalances[(int) $row->id] ?? ['status' => 'not_ready', 'balance' => null];
$balanceText = match ($balanceMatch['status']) {
'matched' => number_format((float) $balanceMatch['balance'], 2),
'ambiguous' => '多条匹配',
'unmatched' => '未匹配',
default => '待同步',
};
$balanceTitle = $balanceMatch['status'] === 'matched'
? '聚水潭店铺:' . $balanceMatch['source_store_name'] . ';余额日期:' . $withdrawalBalanceDate
: '余额日期:' . $withdrawalBalanceDate;
$reserveStandard = $withdrawalReserveStandards[(int) $row->id] ?? null;
$withdrawalAmount = $withdrawalAmounts[(int) $row->id] ?? null;
$compliance = $withdrawalCompliance[(int) $row->id] ?? null;
@endphp
<tr>
<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">{{ $withdrawalAmount === null ? '待接入' : number_format($withdrawalAmount, 2) }}</td>
<td data-withdrawal-column="compliance">
@if ($compliance === 'yes')
<span class="biz-badge biz-badge-yes">是</span>
@elseif ($compliance === 'no')
<span class="biz-badge biz-badge-no">否</span>
@else
<span class="biz-badge biz-badge-no">待核验</span>
@endif
</td>
</tr>
@empty
<tr>
<td colspan="11">
<div class="biz-empty">
<span class="material-icons-outlined" aria-hidden="true">inbox</span>
<div>暂无店铺收款账户,请先在店铺收款账户中维护。</div>
</div>
</td>
</tr>
@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">
<span>共 {{ $withdrawalRows->total() }} 条 · 第 {{ $withdrawalRows->currentPage() }} / {{ $withdrawalRows->lastPage() }} 页</span>
<span>
@if ($withdrawalRows->onFirstPage())
<span>‹ 上一页</span>
@else
<a href="{{ $withdrawalRows->previousPageUrl() }}">‹ 上一页</a>
@endif
@if ($withdrawalRows->hasMorePages())
<a href="{{ $withdrawalRows->nextPageUrl() }}">下一页 ›</a>
@else
<span>下一页 ›</span>
@endif
</span>
</div>
<div
class="biz-modal withdrawal-import-modal {{ $withdrawalOpenImport ? 'is-open' : '' }}"
role="dialog"
aria-modal="true"
aria-labelledby="withdrawal-import-title"
aria-hidden="{{ $withdrawalOpenImport ? 'false' : 'true' }}"
data-withdrawal-import-modal
data-auto-open="{{ $withdrawalOpenImport ? 'true' : 'false' }}"
>
<div class="biz-modal-card">
<div class="withdrawal-import__head">
<div class="biz-modal-title" id="withdrawal-import-title">导入提现配置</div>
<button type="button" class="withdrawal-import__close" title="关闭" aria-label="关闭" data-withdrawal-import-close>
<span class="material-icons-outlined" aria-hidden="true">close</span>
</button>
</div>
@if (is_array($withdrawalImportPreview))
<div class="withdrawal-import__stats" aria-label="导入预览汇总">
<div><span>文件数据</span><strong>{{ number_format($withdrawalImportPreview['total']) }}</strong></div>
<div><span>拟更新</span><strong>{{ number_format($withdrawalImportPreview['changed']) }}</strong></div>
<div><span>无变化</span><strong>{{ number_format($withdrawalImportPreview['unchanged']) }}</strong></div>
</div>
@if ($withdrawalImportPreview['preview'] !== [])
<div class="withdrawal-import__preview">
<table class="biz-table">
<thead><tr><th>店铺平台</th><th>ERP店铺名称</th><th>平台店铺ID</th><th>变更字段</th></tr></thead>
<tbody>
@foreach ($withdrawalImportPreview['preview'] as $previewRow)
<tr>
<td>{{ $previewRow['platform'] }}</td>
<td>{{ $previewRow['erp_store_name'] }}</td>
<td>{{ $previewRow['store_id'] ?: '-' }}</td>
<td>{{ $previewRow['changes'] }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@if ($withdrawalImportPreview['changed'] > count($withdrawalImportPreview['preview']))
<p class="withdrawal-import__status">已显示 20 / {{ number_format($withdrawalImportPreview['changed']) }} 条变更</p>
@endif
@else
<p class="withdrawal-import__empty">文件内容与当前配置一致,没有需要更新的字段。</p>
@endif
<form method="POST" action="{{ route('business.withdrawal-management.import.commit', $withdrawalQuery) }}">
@csrf
<input type="hidden" name="preview_token" value="{{ $withdrawalImportPreview['token'] }}">
<div class="biz-modal-foot">
<button type="button" class="biz-btn biz-btn-ghost" data-withdrawal-import-close>取消</button>
<button type="submit" class="biz-btn biz-btn-primary" @disabled($withdrawalImportPreview['changed'] === 0)>
<span class="material-icons-outlined" aria-hidden="true">save</span>
<span>确认更新 {{ number_format($withdrawalImportPreview['changed']) }} 家店铺</span>
</button>
</div>
</form>
@else
@if ($errors->has('import'))
<div class="withdrawal-import__error" role="alert">{{ $errors->first('import') }}</div>
@endif
<form method="POST" enctype="multipart/form-data" action="{{ route('business.withdrawal-management.import.preview', $withdrawalQuery) }}">
@csrf
<label class="biz-field-label" for="withdrawal-import-file">Excel 文件</label>
<input id="withdrawal-import-file" class="biz-field-input" type="file" name="import" accept=".xlsx,.xls" required>
<div class="withdrawal-import__template">
<a class="biz-btn biz-btn-ghost" href="{{ route('business.withdrawal-management.template') }}">
<span class="material-icons-outlined" aria-hidden="true">download</span>
<span>下载导入模板</span>
</a>
</div>
<div class="biz-modal-foot">
<button type="button" class="biz-btn biz-btn-ghost" data-withdrawal-import-close>取消</button>
<button type="submit" class="biz-btn biz-btn-primary">
<span class="material-icons-outlined" aria-hidden="true">preview</span>
<span>上传并预览</span>
</button>
</div>
</form>
@endif
</div>
</div>
</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; }
.withdrawal-import__close { display:inline-flex; width:2rem; height:2rem; flex:0 0 2rem; align-items:center; justify-content:center; border:1px solid #d8d2c7; border-radius:6px; color:#34312d; }
.withdrawal-import__close:hover { background:#f3efe6; }
.withdrawal-import__stats { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); margin:1rem 0; border:1px solid #d8d2c7; }
.withdrawal-import__stats > div { display:flex; align-items:center; justify-content:space-between; gap:.75rem; padding:.75rem 1rem; }
.withdrawal-import__stats > div + div { border-left:1px solid #d8d2c7; }
.withdrawal-import__stats span { color:#6b665e; font-size:.8125rem; }
.withdrawal-import__stats strong { color:#171717; font-variant-numeric:tabular-nums; }
.withdrawal-import__preview { max-height:360px; overflow:auto; border:1px solid #e5e0d7; }
.withdrawal-import__preview .biz-table { min-width:700px; }
.withdrawal-import__status { margin:.75rem 0 0; color:#6b665e; font-size:.8125rem; }
.withdrawal-import__template { margin-top:1rem; }
.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; }
.withdrawal-import-modal .biz-modal-foot { align-items:stretch; flex-direction:column-reverse; }
.withdrawal-import-modal .biz-btn { justify-content:center; }
}
</style>
<script>
(() => {
const modal = document.querySelector('[data-withdrawal-import-modal]');
if (!modal) return;
const open = () => {
modal.classList.add('is-open');
modal.setAttribute('aria-hidden', 'false');
modal.querySelector('input[type="file"], button:not([disabled])')?.focus();
};
const close = () => {
modal.classList.remove('is-open');
modal.setAttribute('aria-hidden', 'true');
};
document.querySelectorAll('[data-withdrawal-import-open]').forEach((button) => button.addEventListener('click', open));
modal.querySelectorAll('[data-withdrawal-import-close]').forEach((button) => button.addEventListener('click', close));
modal.addEventListener('click', (event) => { if (event.target === modal) close(); });
document.addEventListener('keydown', (event) => { if (event.key === 'Escape' && modal.classList.contains('is-open')) close(); });
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>