feat: 账户基本信息类型卡片改为独立页面 (#1146)

将五类账户基本信息的详情入口改为独立页面,复用现有账户筛选和编辑能力。
This commit is contained in:
shanshi 2026-11-13 10:12:52 +08:00
parent b4783ae528
commit 45dade3c26
7 changed files with 169 additions and 65 deletions

View File

@ -16,6 +16,7 @@ use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
use Illuminate\View\View;
use Modules\EcommerceFinance\Services\FinanceLedgerAccountCreator;
use Modules\EcommerceFinance\Services\FinanceLedgerAccountEditor;
use Modules\EcommerceFinance\Services\FinanceLedgerAccountTemplate;
@ -45,6 +46,26 @@ class FinanceLedger extends Controller
return $template->download();
}
public function accountBasicPage(FinanceLedgerPageData $pageData, string $view): View
{
$definition = FinanceLedgerPageData::ACCOUNT_TYPE_VIEWS[$view] ?? null;
abort_if($definition === null, 404);
$ledgerData = $pageData->page('accounts', $view);
$ledgerData['ledgerStandaloneAccountView'] = $view;
$ledgerData['ledgerStandaloneAccountTitle'] = (string) $definition['title'];
$ledgerData['ledgerStandaloneAccountDescription'] = (string) $definition['description'];
$ledgerData['ledgerStandaloneAccountBackUrl'] = route('business.pages.show', [
'section' => 'financial-operations',
'page' => 'ledger',
]) . '?tab=flows&flows_view=basic';
return view('business.pages.financial-ledger.account-basic-page', [
'ledgerData' => $ledgerData,
'pageTitle' => (string) $definition['title'],
]);
}
public function storeAccount(Request $request, FinanceLedgerAccountCreator $creator): JsonResponse
{
abort_unless(

View File

@ -37,6 +37,34 @@ class FinanceLedgerPageData
'sources' => ['title' => '数据来源', 'icon' => 'database'],
];
public const ACCOUNT_TYPE_VIEWS = [
'bank' => [
'title' => '银行账户',
'description' => '银行开户、网点和限额等基本资料',
'icon' => 'account_balance',
],
'non_store' => [
'title' => '支付宝',
'description' => '支付宝账号、商户和实名主体资料',
'icon' => 'account_balance_wallet',
],
'wangshang' => [
'title' => '网商银行',
'description' => '网商银行账户及企业资料',
'icon' => 'business_center',
],
'wallet' => [
'title' => '网银钱包',
'description' => '网银钱包账户及关联资料',
'icon' => 'wallet',
],
'other' => [
'title' => '其他账户',
'description' => '尚未归入固定类型的账户资料',
'icon' => 'category',
],
];
private const HIDDEN_TABS = [
'accounts' => true,
];
@ -85,7 +113,7 @@ class FinanceLedgerPageData
) {
}
public function page(?string $tab = null): array
public function page(?string $tab = null, ?string $accountView = null): array
{
$tab = (string) (request('tab') ?: $tab);
$tab = array_key_exists($tab, self::TABS) ? $tab : 'flows';
@ -95,7 +123,7 @@ class FinanceLedgerPageData
$flowView = in_array($requestedFlowView, ['summary', 'bank', 'non_store', 'wangshang', 'wallet', 'daily', 'basic'], true)
? $requestedFlowView
: 'summary';
$requestedAccountView = trim((string) request('accounts_view', 'all'));
$requestedAccountView = trim((string) ($accountView ?? request('accounts_view', 'all')));
$accountView = in_array($requestedAccountView, ['all', 'bank', 'non_store', 'wangshang', 'wallet', 'other'], true)
? $requestedAccountView
: 'all';
@ -307,36 +335,9 @@ class FinanceLedgerPageData
*/
private function accountTypeCards(): array
{
$definitions = [
'bank' => [
'title' => '银行账户',
'description' => '银行开户、网点和限额等基本资料',
'icon' => 'account_balance',
],
'non_store' => [
'title' => '支付宝',
'description' => '支付宝账号、商户和实名主体资料',
'icon' => 'account_balance_wallet',
],
'wangshang' => [
'title' => '网商银行',
'description' => '网商银行账户及企业资料',
'icon' => 'business_center',
],
'wallet' => [
'title' => '网银钱包',
'description' => '网银钱包账户及关联资料',
'icon' => 'wallet',
],
'other' => [
'title' => '其他账户',
'description' => '尚未归入固定类型的账户资料',
'icon' => 'category',
],
];
$cards = [];
foreach ($definitions as $view => $definition) {
foreach (self::ACCOUNT_TYPE_VIEWS as $view => $definition) {
$query = FinanceLedgerIndependentAccounts::exclude(
$this->db()->table('fin_accounts as a'),
'a.account_short',

View File

@ -0,0 +1,7 @@
<x-layouts.admin>
<x-slot name="title">账户基本信息 - {{ $pageTitle }}</x-slot>
<x-slot name="content">
@include($ledgerData['view'], $ledgerData)
</x-slot>
</x-layouts.admin>

View File

@ -14,6 +14,10 @@
$accountPlanOptions = $ledgerAccountPlanOptions ?? [];
$companyOptions = $ledgerCompanyOptions ?? [];
$accountKeepOptions = $ledgerAccountKeepOptions ?? \App\Utilities\FinanceLedgerAccountShouldKeep::OPTIONS;
$standaloneAccountView = $ledgerStandaloneAccountView ?? null;
$standaloneAccountTitle = $ledgerStandaloneAccountTitle ?? '';
$standaloneAccountDescription = $ledgerStandaloneAccountDescription ?? '';
$standaloneAccountBackUrl = $ledgerStandaloneAccountBackUrl ?? '';
$availableBreakdown = $ledgerAvailableBreakdown ?? [
'bank_available' => (float) ($ledgerAccountSummary['balance'] ?? 0),
'shop_available' => (float) ($ledgerShopSummary['balance'] ?? 0),
@ -67,6 +71,7 @@
$accountViewQuery = collect(request()->query())
->except([
'tab',
'view',
'accounts_page',
'accounts_filter_column',
'accounts_filter_value',
@ -77,14 +82,23 @@
])
->filter(static fn ($value): bool => is_scalar($value) || $value === null)
->all();
$accountViewUrl = static fn (string $view): string => $ledgerPageUrl . '?' . http_build_query(array_merge(
$accountViewQuery,
['tab' => 'accounts', 'accounts_view' => $view],
));
$accountViewResetUrl = $ledgerPageUrl . '?' . http_build_query([
'tab' => 'accounts',
'accounts_view' => $accountView,
]);
$standaloneAccountViewUrl = static fn (string $view): string => route(
'business.financial-ledger.account-basic',
array_merge($accountViewQuery, ['view' => $view]),
);
$accountViewUrl = $standaloneAccountView === null
? static fn (string $view): string => $ledgerPageUrl . '?' . http_build_query(array_merge(
$accountViewQuery,
['tab' => 'accounts', 'accounts_view' => $view],
))
: $standaloneAccountViewUrl;
$accountViewResetUrl = $standaloneAccountView === null
? $ledgerPageUrl . '?' . http_build_query([
'tab' => 'accounts',
'accounts_view' => $accountView,
])
: $standaloneAccountViewUrl($accountView);
$accountFormUrl = $standaloneAccountView === null ? $tabUrl('accounts') : $accountViewResetUrl;
$transactionView = $ledgerTransactionView ?? 'all';
$transactionViews = $ledgerTransactionViews ?? [];
$transactionViewAccount = $ledgerTransactionViewAccount ?? null;
@ -688,42 +702,66 @@
.ledger-flow-edit-actions [hidden] { display:none !important; }
.ledger-flow-edit-actions .ledger-btn { color:var(--vip-warning, #935F07); }
.ledger-mono { font-variant-numeric:tabular-nums; }
.ledger-standalone-account-header { display:flex; align-items:flex-start; justify-content:space-between; gap:1rem; padding:1rem 1.15rem; margin-bottom:.9rem; border:1px solid var(--vip-border, #D8D2C7); border-radius:.75rem; background:#fff; }
.ledger-standalone-account-header__copy { min-width:0; }
.ledger-standalone-account-header__eyebrow { display:block; margin-bottom:.22rem; color:var(--vip-content-accent-dark, #856632); font-size:.66rem; font-weight:700; letter-spacing:.04em; }
.ledger-standalone-account-header h2 { margin:0; color:var(--vip-content-ink, #171717); font-size:.95rem; font-weight:700; }
.ledger-standalone-account-header p { margin:.3rem 0 0; color:var(--vip-content-muted, #595959); font-size:.72rem; line-height:1.55; }
.ledger-standalone-account-back { display:inline-flex; flex:0 0 auto; align-items:center; gap:.35rem; color:var(--vip-content-accent-dark, #856632); font-size:.72rem; font-weight:600; text-decoration:none; }
.ledger-standalone-account-back:hover { color:#6A5129; }
@media (max-width: 900px) { .ledger-hero { align-items:flex-start; flex-direction:column; } .ledger-field { min-width:calc(50% - .5rem); } .ledger-edit-dialog .ledger-company-form__body { grid-template-columns:repeat(2, minmax(0, 1fr)); } }
@media (max-width: 700px) { .ledger-standalone-account-header { flex-direction:column; } }
@media (max-width: 760px) { .ledger-company-form__body { grid-template-columns:repeat(2, minmax(0, 1fr)); } .ledger-company-field--wide { grid-column:span 2; } }
@media (max-width: 520px) { .ledger-company-form__body { grid-template-columns:minmax(0, 1fr); } .ledger-company-field--wide { grid-column:auto; } }
</style>
<div class="ledger-hero">
<div>
<h2>财务台账</h2>
<p>账户、店铺、期初与动账明细统一归集;数据页默认本月 1 日至月底,可调整日期查看历史,流水口径:期末 = 期初 + 入账 − 出账。账户总表与店铺台账展示当前状态。</p>
@if ($standaloneAccountView !== null)
<header class="ledger-standalone-account-header" data-ledger-standalone-account="{{ $standaloneAccountView }}">
<div class="ledger-standalone-account-header__copy">
<span class="ledger-standalone-account-header__eyebrow">账户基本信息</span>
<h2>{{ $standaloneAccountTitle }}</h2>
<p>{{ $standaloneAccountDescription }}。当前为独立页面,筛选、列设置和账户编辑均只在本页进行。</p>
</div>
<a class="ledger-standalone-account-back" href="{{ $standaloneAccountBackUrl }}">
<span class="material-icons-outlined" aria-hidden="true">arrow_back</span>
返回账户基本信息
</a>
</header>
@else
<div class="ledger-hero">
<div>
<h2>财务台账</h2>
<p>账户、店铺、期初与动账明细统一归集;数据页默认本月 1 日至月底,可调整日期查看历史,流水口径:期末 = 期初 + 入账 − 出账。账户总表与店铺台账展示当前状态。</p>
</div>
<div class="ledger-hero__meta">
<span>账户 <b>{{ number_format((int) ($meta['accounts'] ?? 0)) }}</b></span>
<span>店铺 <b>{{ number_format((int) ($meta['shops'] ?? 0)) }}</b></span>
<span>公司 <b>{{ number_format((int) ($meta['companies'] ?? 0)) }}</b></span>
<span>动账 <b data-ledger-meta-transactions>{{ number_format((int) ($meta['transactions'] ?? 0)) }}</b> 笔</span>
<span>收入 <b>{{ $money($meta['income'] ?? 0) }}</b></span>
<span>支出 <b>{{ $money($meta['expense'] ?? 0) }}</b></span>
@if (!empty($meta['last_synced_at']))
<span>数据时间 <b>{{ $date($meta['last_synced_at']) }}</b></span>
@endif
</div>
</div>
<div class="ledger-hero__meta">
<span>账户 <b>{{ number_format((int) ($meta['accounts'] ?? 0)) }}</b></span>
<span>店铺 <b>{{ number_format((int) ($meta['shops'] ?? 0)) }}</b></span>
<span>公司 <b>{{ number_format((int) ($meta['companies'] ?? 0)) }}</b></span>
<span>动账 <b data-ledger-meta-transactions>{{ number_format((int) ($meta['transactions'] ?? 0)) }}</b> 笔</span>
<span>收入 <b>{{ $money($meta['income'] ?? 0) }}</b></span>
<span>支出 <b>{{ $money($meta['expense'] ?? 0) }}</b></span>
@if (!empty($meta['last_synced_at']))
<span>数据时间 <b>{{ $date($meta['last_synced_at']) }}</b></span>
@endif
</div>
</div>
<nav class="ledger-tabbar" role="tablist" aria-label="财务台账页签">
@foreach ($tabs as $key => $tab)
<button type="button" class="ledger-tab {{ $activeTab === $key ? 'is-active' : '' }}" role="tab" aria-selected="{{ $activeTab === $key ? 'true' : 'false' }}" aria-controls="ledger-panel-{{ $key }}" data-ledger-tab="{{ $key }}">
<span class="material-icons-outlined" aria-hidden="true">{{ $tab['icon'] }}</span>
<span>{{ $tab['title'] }}</span>
</button>
@endforeach
</nav>
<nav class="ledger-tabbar" role="tablist" aria-label="财务台账页签">
@foreach ($tabs as $key => $tab)
<button type="button" class="ledger-tab {{ $activeTab === $key ? 'is-active' : '' }}" role="tab" aria-selected="{{ $activeTab === $key ? 'true' : 'false' }}" aria-controls="ledger-panel-{{ $key }}" data-ledger-tab="{{ $key }}">
<span class="material-icons-outlined" aria-hidden="true">{{ $tab['icon'] }}</span>
<span>{{ $tab['title'] }}</span>
</button>
@endforeach
</nav>
@endif
{{-- 账户总表 --}}
<section class="ledger-panel" id="ledger-panel-accounts" role="tabpanel" data-ledger-panel="accounts" @if ($activeTab !== 'accounts') hidden @endif>
<form class="ledger-toolbar" method="GET" action="{{ $tabUrl('accounts') }}">
<input type="hidden" name="tab" value="accounts">
<form class="ledger-toolbar" method="GET" action="{{ $accountFormUrl }}">
@if ($standaloneAccountView === null)
<input type="hidden" name="tab" value="accounts">
@endif
<input type="hidden" name="accounts_view" value="{{ $accountView }}">
<div class="ledger-field"><label for="accounts-q">关键词</label><input id="accounts-q" name="accounts_q" value="{{ request('accounts_q') }}" placeholder="公司、账号、责任人"></div>
<div class="ledger-field" style="min-width:11rem"><label for="accounts-short">账户简称</label><input id="accounts-short" name="accounts_short" list="ledger-account-list" value="{{ request('accounts_short') }}" placeholder="选择或输入"></div>

View File

@ -8,7 +8,7 @@
</header>
<div class="ledger-account-type-grid">
@foreach ($ledgerAccountTypeCards as $card)
<a class="ledger-account-type-card" href="{{ $accountViewUrl($card['view']) }}" data-account-type-card="{{ $card['view'] }}">
<a class="ledger-account-type-card" href="{{ route('business.financial-ledger.account-basic', ['view' => $card['view']]) }}" data-account-type-card="{{ $card['view'] }}">
<span class="ledger-account-type-card__icon material-icons-outlined" aria-hidden="true">{{ $card['icon'] }}</span>
<div class="ledger-account-type-card__title">
<strong>{{ $card['title'] }}</strong>

View File

@ -1003,6 +1003,11 @@ Route::get('workspace/financial-operations/ledger/unretained-accounts', [\App\Ht
->middleware('business.page:financial-operations,ledger')
->name('business.financial-ledger.unretained-accounts');
Route::get('workspace/financial-operations/ledger/account-basic/{view}', [\App\Http\Controllers\Business\FinanceLedger::class, 'accountBasicPage'])
->whereIn('view', ['bank', 'non_store', 'wangshang', 'wallet', 'other'])
->middleware('business.page:financial-operations,ledger')
->name('business.financial-ledger.account-basic');
Route::post('workspace/financial-ledger/accounts/todo', [\App\Http\Controllers\Business\FinanceLedger::class, 'updateAccountTodo'])
->middleware('business.page:financial-operations,ledger')
->name('business.financial-ledger.accounts.todo');

View File

@ -8,6 +8,7 @@ use App\Models\Auth\User;
use App\Utilities\BusinessNavigation;
use App\Utilities\SidebarMenu;
use App\Utilities\SidebarPermissions;
use Modules\EcommerceFinance\View\FinanceLedgerPageData;
use Tests\Feature\FeatureTestCase;
class FinanceLedgerNavigationTest extends FeatureTestCase
@ -194,6 +195,37 @@ class FinanceLedgerNavigationTest extends FeatureTestCase
$this->assertStringContainsString('accounts_view=bank', $content);
$this->assertStringContainsString('accounts_view=wallet', $content);
$this->assertStringContainsString('accounts_view=other', $content);
foreach (array_keys(FinanceLedgerPageData::ACCOUNT_TYPE_VIEWS) as $view) {
$this->assertStringContainsString(
'href="' . e(route('business.financial-ledger.account-basic', ['view' => $view])) . '"',
$content,
);
}
}
public function test_account_type_cards_open_standalone_account_pages(): void
{
$this->loginAs();
$backUrl = route('business.pages.show', [
'section' => 'financial-operations',
'page' => 'ledger',
]) . '?tab=flows&flows_view=basic';
foreach (FinanceLedgerPageData::ACCOUNT_TYPE_VIEWS as $view => $definition) {
$url = route('business.financial-ledger.account-basic', ['view' => $view]);
$html = $this->get($url)
->assertOk()
->assertSeeText($definition['title'])
->assertSeeText('返回账户基本信息')
->getContent();
$this->assertStringContainsString('data-ledger-standalone-account="' . $view . '"', $html);
$this->assertStringNotContainsString('data-ledger-tab="flows"', $html);
$this->assertStringContainsString('action="' . e($url) . '"', $html);
$this->assertStringContainsString('href="' . e($backUrl) . '"', $html);
}
}
public function test_legacy_independent_company_reconciliation_permission_follows_the_renamed_page(): void