WuYongJie 28a62434d2 整合并修正京东钱包账单 (#26)
接入京东充值与消耗账单,修复快照冷启动、刷新依赖、日期边界与标签局部切换。
2026-08-11 12:22:41 +08:00

737 lines
31 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
$isMarketingAccountBillsPage = ($section['slug'] ?? null) === 'billing-transactions'
&& ($page['slug'] ?? null) === 'marketing-account-bills';
$marketingAccountTabTitles = [
'taobao-wechat' => 'Alibaba',
'jd-wallet' => '京东',
];
$activeContentTitle = $activeTab['title'] ?? ($page['title'] ?? $section['title']);
if ($isMarketingAccountBillsPage && isset($marketingAccountTabTitles[$activeTab['slug'] ?? null])) {
$activeContentTitle = $marketingAccountTabTitles[$activeTab['slug']];
}
@endphp
<x-layouts.admin>
<x-slot name="title">
{{ $activeContentTitle }}
</x-slot>
<x-slot name="content">
<style>
#main-body > .container {
max-width: none;
width: auto;
margin-left: 1.5rem;
margin-right: 1.5rem;
}
.business-tab-bar {
display: flex;
align-items: center;
gap: 2.25rem;
min-height: 5rem;
padding: 0 2rem;
overflow-x: auto;
border: 1px solid #e5e7eb;
border-radius: 1rem;
background: #ffffff;
box-shadow: 0 8px 24px rgba(31, 41, 55, 0.06);
}
.business-tab-link {
position: relative;
display: inline-flex;
flex: 0 0 auto;
align-items: center;
gap: 0.65rem;
min-height: 5rem;
color: #4b5563;
font-size: 1rem;
font-weight: 600;
white-space: nowrap;
transition: color 150ms ease;
}
.business-tab-link:hover,
.business-tab-link.is-active {
color: var(--vip-content-accent);
}
.business-tab-link.is-active::after {
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 3px;
border-radius: 999px 999px 0 0;
background: var(--vip-content-accent);
content: '';
}
.business-page--marketing-account-bills .business-tab-bar {
gap: 2rem;
border-color: var(--vip-border);
border-radius: 0.75rem;
box-shadow: none;
}
.business-page--marketing-account-bills .business-tab-link {
gap: 0.5rem;
color: #595959;
font-size: 0.875rem;
}
.business-page--marketing-account-bills .business-tab-link:hover,
.business-page--marketing-account-bills .business-tab-link.is-active {
color: var(--vip-content-accent-dark);
}
.business-page--marketing-account-bills .business-tab-link.is-active::after {
background: var(--vip-content-accent);
}
.business-page--marketing-account-bills .business-tab-link:focus-visible {
outline: 2px solid var(--vip-content-accent-dark);
outline-offset: 3px;
}
.business-page--marketing-account-bills .business-content-section {
border-color: var(--vip-border);
border-radius: 0.75rem;
box-shadow: none;
}
.biz-card {
background: #ffffff;
border: 1px solid #eef0f4;
border-radius: 1rem;
box-shadow: 0 6px 18px rgba(31, 41, 55, 0.04);
}
.biz-alert {
margin-bottom: 1rem;
padding: 0.75rem 1rem;
border: 1px solid #bbf7d0;
border-radius: 0.5rem;
background: #f0fdf4;
color: #166534;
font-size: 0.875rem;
}
.biz-alert-error {
border-color: #fecaca;
background: #fef2f2;
color: #991b1b;
}
.biz-import-form {
display: flex;
align-items: center;
gap: 0.5rem;
}
.biz-import-input {
max-width: 15rem;
font-size: 0.75rem;
}
.biz-card + .biz-card {
margin-top: 1.25rem;
}
.biz-card-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1.5rem;
border-bottom: 1px solid #f3f4f6;
}
.biz-card-title {
font-size: 1rem;
font-weight: 700;
color: #111827;
display: flex;
align-items: center;
gap: 0.5rem;
}
.biz-card-actions {
display: flex;
align-items: center;
gap: 0.5rem;
}
.biz-table-wrap {
overflow-x: auto;
}
.biz-table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
font-size: 0.875rem;
}
.biz-table thead th {
position: sticky;
top: 0;
background: #f9fafb;
color: #4b5563;
font-weight: 600;
text-align: left;
padding: 0.75rem 1rem;
border-bottom: 1px solid #e5e7eb;
white-space: nowrap;
}
.biz-table tbody td {
padding: 0.65rem 1rem;
border-bottom: 1px solid #f3f4f6;
color: #1f2937;
white-space: nowrap;
}
.biz-table tbody tr:hover td {
background: #f9fafb;
}
.biz-table .num {
text-align: right;
font-variant-numeric: tabular-nums;
color: #111827;
}
.biz-table .actions-cell {
white-space: nowrap;
text-align: right;
}
.biz-empty {
padding: 4rem 1rem;
text-align: center;
color: #6b7280;
}
.biz-empty .material-icons-outlined {
font-size: 3rem;
color: #d1d5db;
}
.biz-btn {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.4rem 0.85rem;
border-radius: 0.5rem;
font-size: 0.8125rem;
font-weight: 600;
transition: background 150ms ease, color 150ms ease;
border: 1px solid transparent;
cursor: pointer;
}
.biz-btn-primary {
background: #4f46e5;
color: #ffffff;
}
.biz-btn-primary:hover {
background: #4338ca;
}
.biz-btn-ghost {
background: transparent;
color: #4b5563;
border-color: #e5e7eb;
}
.biz-btn-ghost:hover {
background: #f3f4f6;
}
.biz-btn-danger-ghost {
background: transparent;
color: #dc2626;
border-color: #fecaca;
}
.biz-btn-danger-ghost:hover {
background: #fef2f2;
}
.biz-badge {
display: inline-block;
padding: 0.15rem 0.55rem;
border-radius: 999px;
font-size: 0.75rem;
font-weight: 600;
}
.biz-badge-yes { background: #dcfce7; color: #15803d; }
.biz-badge-no { background: #f3f4f6; color: #6b7280; }
/* Modal */
.biz-modal {
position: fixed;
inset: 0;
background: rgba(15, 23, 42, 0.45);
display: none;
align-items: flex-start;
justify-content: center;
padding: 4rem 1rem;
z-index: 60;
overflow-y: auto;
}
.biz-modal.is-open {
display: flex;
}
.biz-modal-card {
background: #ffffff;
border-radius: 1rem;
box-shadow: 0 20px 60px rgba(15, 23, 42, 0.25);
width: 100%;
max-width: 720px;
padding: 1.5rem;
}
.biz-modal-title {
font-size: 1.125rem;
font-weight: 700;
color: #111827;
margin-bottom: 1rem;
}
.biz-form-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.85rem;
}
.biz-form-grid .full {
grid-column: span 2;
}
.biz-field-label {
display: block;
font-size: 0.75rem;
font-weight: 600;
color: #6b7280;
margin-bottom: 0.25rem;
}
.biz-field-input,
.biz-field-select,
.biz-field-textarea {
width: 100%;
border: 1px solid #e5e7eb;
border-radius: 0.5rem;
padding: 0.5rem 0.65rem;
font-size: 0.875rem;
color: #111827;
background: #ffffff;
}
.biz-field-input:focus,
.biz-field-select:focus,
.biz-field-textarea:focus {
border-color: #6366f1;
outline: 2px solid rgba(99, 102, 241, 0.25);
}
.biz-modal-foot {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
margin-top: 1.25rem;
}
.biz-pagination {
display: flex;
gap: 0.25rem;
padding: 0.75rem 1.5rem;
border-top: 1px solid #f3f4f6;
color: #4b5563;
font-size: 0.85rem;
align-items: center;
justify-content: space-between;
}
.biz-pagination a,
.biz-pagination span {
padding: 0.25rem 0.5rem;
border-radius: 0.375rem;
}
.biz-pagination a:hover {
background: #f3f4f6;
}
.business-content-section {
margin-top: 1.5rem;
padding: 1.5rem;
border: 1px solid #eef0f4;
border-radius: 1rem;
background: #ffffff;
}
[v-cloak] {
display: none;
}
@media (min-width: 1280px) {
#main-body > .container {
margin-left: 2.5rem;
margin-right: 2.5rem;
}
}
</style>
<div
@class([
'my-6',
'business-page--marketing-account-bills' => $isMarketingAccountBillsPage,
])
@if ($isMarketingAccountBillsPage) data-marketing-account-bills-workspace @endif
>
@if ($isMarketingAccountBillsPage)
<p class="sr-only" role="status" aria-live="polite" data-marketing-account-bills-status></p>
<div class="biz-alert biz-alert-error" role="alert" data-marketing-account-bills-navigation-error hidden>
<span>账单内容局部加载失败,当前页面内容已保留。</span>
<a href="{{ request()->fullUrl() }}" data-marketing-account-bills-retry data-marketing-account-bills-full-page>重新加载</a>
</div>
@endif
@if (is_string(session('success')))
<div class="biz-alert" role="status">{{ session('success') }}</div>
@endif
@if ($errors->has('import'))
<div class="biz-alert biz-alert-error" role="alert">{{ $errors->first('import') }}</div>
@endif
@if (!empty($upstreamError))
<div class="biz-alert biz-alert-error" role="alert">{{ $upstreamError }}</div>
@endif
@if ($tabs->isNotEmpty())
<nav class="business-tab-bar" aria-label="{{ $page['title'] }}">
@foreach ($tabs as $tab)
@php
$visibleTabTitle = $isMarketingAccountBillsPage
? ($marketingAccountTabTitles[$tab['slug']] ?? $tab['title'])
: $tab['title'];
@endphp
<a
href="{{ route('business.pages.show', [
'section' => $section['slug'],
'page' => $page['slug'],
'tab' => $tab['slug'],
]) }}"
@class([
'business-tab-link',
'is-active' => $activeTab['slug'] === $tab['slug'],
])
@if ($activeTab['slug'] === $tab['slug']) aria-current="page" @endif
>
<span class="material-icons-outlined text-xl" aria-hidden="true">
{{ $tab['icon'] ?? 'description' }}
</span>
<span>{{ $visibleTabTitle }}</span>
</a>
@endforeach
</nav>
@endif
@php
$pageView = null;
if ($page) {
$viewName = 'business.pages.' . $section['slug'] . '.' . $page['slug'];
if ($tabs->isNotEmpty()) {
$viewName .= '.' . ($activeTab['slug'] ?? '');
}
if (view()->exists($viewName)) {
$pageView = $viewName;
}
}
@endphp
@if (!empty($content) && !empty($content['view']))
<section class="business-content-section" aria-label="{{ $activeContentTitle }}">
@include($content['view'], $content)
</section>
@elseif ($pageView)
@include($pageView)
@elseif ($descriptor)
<div class="biz-card mt-6">
<div class="biz-card-head">
<div class="biz-card-title">
<span class="material-icons-outlined" aria-hidden="true">table_view</span>
<span>{{ $activeTab['title'] ?? $page['title'] }} · 数据列表</span>
@if ($descriptor->readOnly)
<span class="biz-badge biz-badge-yes">工作平台数据库</span>
@endif
</div>
@unless ($descriptor->readOnly)
<div class="biz-card-actions">
<form
class="biz-import-form"
method="POST"
enctype="multipart/form-data"
action="{{ route('business.pages.import', [
'section' => $section['slug'],
'page' => $page['slug'],
'tab' => $activeTab['slug'] ?? null,
]) }}"
>
@csrf
<input class="biz-import-input" type="file" name="import" accept=".xlsx,.xls,.csv" required>
<button type="submit" class="biz-btn biz-btn-ghost" title="导入 Excel 或 CSV">
<span class="material-icons-outlined" style="font-size:1.1rem">upload_file</span>
<span>导入</span>
</button>
</form>
<button type="button" class="biz-btn biz-btn-primary" onclick="bizOpenCreateModal()">
<span class="material-icons-outlined" style="font-size:1.1rem">add</span>
<span>新增</span>
</button>
</div>
@endunless
</div>
<div class="biz-table-wrap">
<table class="biz-table">
<thead>
<tr>
<th>#</th>
@foreach ($descriptor->fields as $field)
<th class="@if ($field['currency']) num @endif">{{ $field['label'] }}</th>
@endforeach
@unless ($descriptor->readOnly)
<th class="actions-cell">操作</th>
@endunless
</tr>
</thead>
<tbody>
@forelse ($rows as $row)
<tr>
<td>{{ $row->id }}</td>
@foreach ($descriptor->fields as $field)
@php
$val = $row->{$field['name']};
@endphp
<td class="@if ($field['currency']) num @endif">
@if ($field['type'] === 'boolean')
@if ($val)
<span class="biz-badge biz-badge-yes">是</span>
@else
<span class="biz-badge biz-badge-no">否</span>
@endif
@elseif ($field['type'] === 'date' && $val)
{{ \Illuminate\Support\Carbon::parse($val)->format('Y-m-d') }}
@elseif ($field['currency'])
{{ number_format((float) $val, 2) }}
@else
{{ $val }}
@endif
</td>
@endforeach
@unless ($descriptor->readOnly)
<td class="actions-cell">
<button type="button" class="biz-btn biz-btn-ghost" title="编辑" onclick='bizOpenEditModal({{ json_encode($row->only(array_merge(['id'], array_map(fn($f) => $f['name'], $descriptor->fields)))) }})'>
<span class="material-icons-outlined" style="font-size:1.1rem">edit</span>
</button>
<form method="POST" action="{{ route('business.pages.destroy', [
'section' => $section['slug'],
'page' => $page['slug'],
'id' => $row->id,
'tab' => $activeTab['slug'] ?? null,
]) }}" style="display:inline" onsubmit="return confirm('确认删除这条记录?')">
@csrf
@method('DELETE')
<button type="submit" class="biz-btn biz-btn-danger-ghost">
<span class="material-icons-outlined" style="font-size:1.1rem">delete</span>
</button>
</form>
</td>
@endunless
</tr>
@empty
<tr>
<td colspan="{{ count($descriptor->fields) + ($descriptor->readOnly ? 1 : 2) }}">
<div class="biz-empty">
<span class="material-icons-outlined" aria-hidden="true">inbox</span>
<div>{{ $descriptor->readOnly ? '暂无工作平台数据' : '暂无数据,点击右上角"新增"开始录入' }}</div>
</div>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<div class="biz-pagination">
<span>共 {{ $rows->total() }} 条 · 第 {{ $rows->currentPage() }} / {{ $rows->lastPage() }} 页</span>
<span>
@if ($rows->onFirstPage())
<span>‹ 上一页</span>
@else
<a href="{{ $rows->previousPageUrl() }}">‹ 上一页</a>
@endif
@if ($rows->hasMorePages())
<a href="{{ $rows->nextPageUrl() }}">下一页 ›</a>
@else
<span>下一页 ›</span>
@endif
</span>
</div>
</div>
@unless ($descriptor->readOnly)
{{-- Generic create/edit modal --}}
<div id="biz-modal" class="biz-modal" onclick="if(event.target===this)bizCloseModal()">
<div class="biz-modal-card">
<div class="biz-modal-title" id="biz-modal-title">新增记录</div>
<form id="biz-form" method="POST" action="">
@csrf
<input type="hidden" name="_method" value="POST" id="biz-form-method">
<div class="biz-form-grid">
@foreach ($descriptor->fields as $field)
<div class="{{ $field['type'] === 'textarea' ? 'full' : '' }}">
<label class="biz-field-label">{{ $field['label'] }}</label>
@switch($field['type'])
@case('boolean')
<select class="biz-field-select" name="{{ $field['name'] }}" data-default="">
<option value="0">否</option>
<option value="1">是</option>
</select>
@break
@case('select')
<select class="biz-field-select" name="{{ $field['name'] }}" data-default="">
<option value="">— 请选择 —</option>
@foreach ($field['options'] ?? [] as $opt)
<option value="{{ $opt }}">{{ $opt }}</option>
@endforeach
</select>
@break
@case('date')
<input type="date" class="biz-field-input" name="{{ $field['name'] }}" data-default="">
@break
@case('currency')
@case('number')
<input type="number" step="0.01" class="biz-field-input" name="{{ $field['name'] }}" data-default="">
@break
@case('textarea')
<textarea class="biz-field-textarea" name="{{ $field['name'] }}" rows="3" data-default=""></textarea>
@break
@default
<input type="text" class="biz-field-input" name="{{ $field['name'] }}" data-default="">
@endswitch
</div>
@endforeach
</div>
<div class="biz-modal-foot">
<button type="button" class="biz-btn biz-btn-ghost" onclick="bizCloseModal()">取消</button>
<button type="submit" class="biz-btn biz-btn-primary">
<span class="material-icons-outlined" style="font-size:1.1rem">save</span>
<span>保存</span>
</button>
</div>
</form>
</div>
</div>
<script>
window.__bizDescriptor = {
section: {{ json_encode($section['slug']) }},
page: {{ json_encode($page['slug']) }},
tab: {{ json_encode($activeTab['slug'] ?? null) }},
storeUrl: "{{ route('business.pages.store', ['section' => $section['slug'], 'page' => $page['slug'], 'tab' => $activeTab['slug'] ?? null]) }}",
updateBase: "{{ route('business.pages.update', ['section' => $section['slug'], 'page' => $page['slug'], 'id' => '__ID__', 'tab' => $activeTab['slug'] ?? null]) }}".replace('__ID__', ''),
fields: {{ json_encode($descriptor->fields) }}
};
function bizOpenModal() {
var m = document.getElementById('biz-modal');
m.classList.add('is-open');
document.body.style.overflow = 'hidden';
}
function bizCloseModal() {
var m = document.getElementById('biz-modal');
m.classList.remove('is-open');
document.body.style.overflow = '';
}
function bizResetForm() {
var form = document.getElementById('biz-form');
form.querySelectorAll('[data-default]').forEach(function (el) {
el.value = el.getAttribute('data-default') || '';
});
}
function bizOpenCreateModal() {
document.getElementById('biz-modal-title').textContent = '新增记录';
var form = document.getElementById('biz-form');
form.action = window.__bizDescriptor.storeUrl;
document.getElementById('biz-form-method').value = 'POST';
bizResetForm();
bizOpenModal();
}
function bizOpenEditModal(rowData) {
document.getElementById('biz-modal-title').textContent = '编辑记录 # ' + rowData.id;
var form = document.getElementById('biz-form');
form.action = window.__bizDescriptor.updateBase + rowData.id;
document.getElementById('biz-form-method').value = 'PATCH';
form.querySelectorAll('[data-default]').forEach(function (el) {
var name = el.getAttribute('name');
var val = rowData[name] === null ? '' : rowData[name];
// Format dates to YYYY-MM-DD for the date input
if (el.type === 'date' && val) {
val = String(val).substr(0, 10);
}
// Booleans -> "1" / "0"
if (typeof val === 'boolean') {
val = val ? '1' : '0';
}
el.value = val;
});
bizOpenModal();
}
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') bizCloseModal();
});
</script>
@endunless
@else
<section class="biz-card mt-6" aria-label="{{ $activeTab['title'] ?? ($page['title'] ?? $section['title']) }}">
<div class="biz-empty">
<span class="material-icons-outlined" aria-hidden="true">description</span>
<div>该页面暂未配置数据表,敬请期待。</div>
</div>
</section>
@endif
</div>
@if ($isMarketingAccountBillsPage)
@push('scripts_start')
<script src="{{ asset('public/js/business/marketing-account-bills.min.js') }}?id={{ md5_file(public_path('js/business/marketing-account-bills.min.js')) }}"></script>
@endpush
@elseif (($page['slug'] ?? null) === 'store-receiving-bills')
@push('scripts_start')
<script src="{{ asset('public/js/business/pages.min.js') }}?id={{ md5_file(public_path('js/business/pages.min.js')) }}"></script>
@endpush
@endif
</x-slot>
</x-layouts.admin>