fix: 删除月度期末余额汇总卡片

删除账户余额总览中的月度汇总卡片、切换、接口及关联代码。
This commit is contained in:
qiuxiang 2026-11-11 16:09:08 +08:00
parent dde4531ff4
commit e5faacd2bf
7 changed files with 5 additions and 765 deletions

View File

@ -25,21 +25,6 @@ class StoreReceivingBalanceOverview extends Controller
}
}
public function monthlyClosing(Request $request, StoreReceivingBalanceOverviewService $service)
{
$this->authorizeStoreReceivingBills();
try {
return response()->json($service->monthlyClosing(...$this->requestedPeriod($request)));
} catch (Throwable $exception) {
report($exception);
return response()->json([
'message' => '月度期末余额汇总暂时无法读取,请稍后重试。',
], 503);
}
}
public function incomeDetail(Request $request, StoreReceivingBalanceOverviewService $service)
{
$this->authorizeStoreReceivingBills();

View File

@ -151,98 +151,6 @@ class StoreReceivingBalanceOverviewService
];
}
/**
* 选定月份各平台期末余额。只读取当前发生时间对应的一个月。
*
* @return array<string,mixed>
*/
public function monthlyClosing(string $periodStart, ?string $periodEnd = null): array
{
[$periodStart, $periodEnd, $periodMonth] = $this->periodForRange($periodStart, $periodEnd);
$periodOverview = $this->overview($periodStart, $periodEnd);
$rows = [];
$availableRows = 0;
foreach (self::PLATFORMS as $definition) {
$platform = $this->platformFromOverview($periodOverview, $definition['slug']);
$balance = $platform !== null
&& $platform['available']
&& is_numeric($platform['balance'] ?? null)
? round((float) $platform['balance'], 2)
: null;
$available = $balance !== null;
if ($available) {
$availableRows++;
}
$rows[] = $this->monthlyClosingRow(
$definition['slug'],
$definition['name'],
[$periodMonth => $balance],
$available,
);
}
$totalBalance = $periodOverview['summary']['complete']
&& is_numeric($periodOverview['summary']['balance'] ?? null)
? round((float) $periodOverview['summary']['balance'], 2)
: null;
return [
'month' => $periodMonth,
'period' => [
'start' => $periodStart,
'end' => $periodEnd,
],
'months' => [[
'key' => $periodMonth,
'label' => (int) CarbonImmutable::parse($periodStart, 'Asia/Shanghai')->format('n') . '月',
]],
'summary' => [
'platform_count' => count(self::PLATFORMS),
'available_platform_count' => $availableRows,
'complete' => $availableRows === count(self::PLATFORMS) && $totalBalance !== null,
],
'total' => $this->monthlyClosingRow(
'total',
'合计',
[$periodMonth => $totalBalance],
$totalBalance !== null,
),
'platforms' => $rows,
];
}
/** @param array<string,mixed> $overview */
private function platformFromOverview(array $overview, string $slug): ?array
{
foreach ($overview['platforms'] ?? [] as $platform) {
if (($platform['slug'] ?? null) === $slug) {
return $platform;
}
}
return null;
}
/**
* @param array<string,float|null> $balances
* @return array<string,mixed>
*/
private function monthlyClosingRow(
string $slug,
string $name,
array $balances,
bool $available,
): array
{
return [
'slug' => $slug,
'name' => $name,
'available' => $available,
'balances' => $balances,
];
}
/**
* 收入合计的平台、店铺、账单项目明细。
*

View File

@ -125,126 +125,6 @@
</template>
</section>
<section class="monthly-closing-card" aria-labelledby="monthly-closing-title" :aria-busy="monthlyClosingLoading ? 'true' : 'false'">
<header class="monthly-closing-head">
<div>
<div class="monthly-closing-title-row">
<h3 id="monthly-closing-title">月度期末余额汇总</h3>
<span class="monthly-closing-badge">新增</span>
</div>
<p>
{{ monthlyClosingMonthLabel }}{{ monthlyClosingView === 'store' ? '店铺' : '各平台' }}期末余额
· 金额单位:万元
</p>
</div>
<div v-if="!monthlyClosingLoading && !monthlyClosingError" class="monthly-closing-head-actions">
<span class="monthly-closing-count">{{ monthlyClosingCountText }}</span>
<div class="monthly-closing-view-switch" role="tablist" aria-label="月度期末余额汇总方式">
<button
type="button"
role="tab"
:aria-selected="monthlyClosingView === 'platform' ? 'true' : 'false'"
:class="{ 'is-active': monthlyClosingView === 'platform' }"
@click="selectMonthlyClosingView('platform')"
>平台账户</button>
<button
type="button"
role="tab"
:aria-selected="monthlyClosingView === 'store' ? 'true' : 'false'"
:class="{ 'is-active': monthlyClosingView === 'store' }"
@click="selectMonthlyClosingView('store')"
>店铺汇总</button>
</div>
</div>
</header>
<div v-if="monthlyClosingLoading" class="monthly-closing-loading" role="status">
<span v-for="index in 4" :key="index"></span>
<p>正在汇总 {{ monthlyClosingMonthLabel }}期末余额…</p>
</div>
<div v-else-if="monthlyClosingError" class="monthly-closing-error" role="alert">
<span>{{ monthlyClosingError }}</span>
<button type="button" @click="fetchMonthlyClosing">重新加载</button>
</div>
<div
v-else-if="monthlyClosingView === 'store' && monthlyClosingStoreLoading"
class="monthly-closing-loading"
role="status"
>
<span v-for="index in 4" :key="index"></span>
<p>正在汇总 {{ monthlyClosingMonthLabel }}店铺期末余额…</p>
</div>
<div
v-else-if="monthlyClosingView === 'store' && monthlyClosingStoreError"
class="monthly-closing-error"
role="alert"
>
<span>{{ monthlyClosingStoreError }}</span>
<button type="button" @click="fetchMonthlyClosingStores">重新加载</button>
</div>
<div v-else-if="monthlyClosingView === 'platform'" class="monthly-closing-scroll">
<table class="monthly-closing-table">
<thead>
<tr>
<th scope="col">平台账户</th>
<th v-for="month in monthlyClosing.months" :key="month.key" scope="col" class="is-number monthly-closing-month-head">
{{ month.label }}
</th>
</tr>
</thead>
<tbody>
<tr
v-for="row in monthlyClosingRows"
:key="row.slug"
:class="{ 'is-total': row.slug === 'total', 'is-unavailable': !row.available }"
:data-platform-slug="row.slug"
>
<th scope="row">{{ row.name }}</th>
<td v-for="month in monthlyClosing.months" :key="month.key" class="is-number">
{{ monthlyBalanceWan(row, month.key) }}
</td>
</tr>
</tbody>
</table>
</div>
<template v-else>
<div class="monthly-closing-scroll is-store">
<table class="monthly-closing-table monthly-closing-store-table">
<thead>
<tr>
<th scope="col">平台账户</th>
<th scope="col">店铺名称</th>
<th v-for="month in monthlyClosing.months" :key="month.key" scope="col" class="is-number monthly-closing-month-head">
{{ month.label }}
</th>
</tr>
</thead>
<tbody>
<tr
v-for="row in monthlyStoreRows"
:key="row.key"
:class="{ 'is-total': row.isTotal, 'is-unavailable': !row.available }"
>
<th scope="row">{{ row.platform }}</th>
<td class="monthly-store-name">{{ row.store_name }}</td>
<td v-for="month in monthlyClosing.months" :key="month.key" class="is-number">
{{ monthlyBalanceWan(row, month.key) }}
</td>
</tr>
</tbody>
</table>
</div>
</template>
<footer class="monthly-closing-foot">
<span>统计月份:{{ monthlyClosingMonthLabel }};金额单位为万元。</span>
<span v-if="!monthlyClosingLoading && !monthlyClosingError">
{{ monthlyClosingView === 'store'
? '店铺期末余额 = 期初余额 + 收入 - 支出;缺少期初时显示“—”。'
: '8 个平台沿用各自账单页的期末余额口径。' }}
</span>
</footer>
</section>
<div
v-if="detailOpen"
class="flow-detail-modal"
@ -410,7 +290,6 @@ export default {
},
data() {
const range = currentMonthRange();
const closingMonth = range.end.slice(0, 7);
return {
dateRange: { ...range },
@ -433,33 +312,6 @@ export default {
detailStore: '',
detailBillItem: '',
bodyOverflowBeforeDetail: '',
monthlyClosingLoading: true,
monthlyClosingError: '',
monthlyClosingRequestSerial: 0,
monthlyClosingView: 'platform',
monthlyClosingStoreLoading: false,
monthlyClosingStoreError: '',
monthlyClosingStoreLoaded: false,
monthlyClosingStoreRequestSerial: 0,
monthlyStoreMonthDetails: [],
monthlyClosing: {
month: closingMonth,
period: {
start: range.start,
end: range.end,
},
months: [{
key: closingMonth,
label: `${Number(closingMonth.slice(5))}月`,
}],
summary: {
platform_count: 8,
available_platform_count: 0,
complete: false,
},
total: null,
platforms: [],
},
};
},
computed: {
@ -674,99 +526,9 @@ export default {
const tab = this.detailTabs.find(item => item.key === this.detailTab);
return tab ? tab.label : '';
},
monthlyClosingRows() {
return [
...(this.monthlyClosing.total ? [this.monthlyClosing.total] : []),
...(Array.isArray(this.monthlyClosing.platforms) ? this.monthlyClosing.platforms : []),
];
},
monthlyClosingCountText() {
if (this.monthlyClosingView === 'platform') {
return `${this.monthlyClosing.summary.available_platform_count} / ${this.monthlyClosing.summary.platform_count} 个平台`;
}
if (this.monthlyClosingStoreLoading) return '正在读取店铺…';
if (this.monthlyClosingStoreError) return '店铺数据暂不可用';
const total = this.monthlyStoreRows.filter(row => !row.isTotal).length;
const incomplete = this.monthlyStoreRows.filter(row => !row.isTotal && !row.available).length;
return incomplete > 0
? `${total} 个店铺 · ${incomplete} 个期初待补`
: `${total} 个店铺`;
},
monthlyClosingMonth() {
const selectedDate = this.queryRange.end || this.queryRange.start;
return selectedDate ? selectedDate.slice(0, 7) : '';
},
monthlyClosingMonthLabel() {
const monthKey = this.monthlyClosingMonth;
if (!/^\d{4}-\d{2}$/.test(monthKey)) return '';
return `${monthKey.slice(0, 4)}年${Number(monthKey.slice(5))}月`;
},
monthlyStoreRows() {
const months = Array.isArray(this.monthlyClosing.months)
? this.monthlyClosing.months.map(month => month.key)
: [];
const platformOrder = new Map(
this.monthlyClosing.platforms.map((platform, index) => [platform.slug, index]),
);
const grouped = new Map();
this.monthlyStoreMonthDetails.forEach((result) => {
(Array.isArray(result.details) ? result.details : []).forEach((detail) => {
const key = [detail.platform_slug, detail.store_name].join('|');
const row = grouped.get(key) || {
key,
platform_slug: detail.platform_slug,
platform: detail.platform,
store_name: detail.store_name,
balances: Object.fromEntries(months.map(month => [month, null])),
};
row.balances[result.month] = detail.amount === null || detail.amount === undefined
? null
: (Number.isFinite(Number(detail.amount))
? Math.round(Number(detail.amount) * 100) / 100
: null);
grouped.set(key, row);
});
});
const rows = Array.from(grouped.values()).map(row => ({
...row,
available: months.some(month => row.balances[month] !== null),
}));
const selectedMonth = months[0];
rows.sort((left, right) => {
const leftBalance = left.balances[selectedMonth];
const rightBalance = right.balances[selectedMonth];
return (
(leftBalance === null ? 1 : 0) - (rightBalance === null ? 1 : 0)
|| (leftBalance === null ? 0 : rightBalance)
- (leftBalance === null ? 0 : leftBalance)
|| (platformOrder.get(left.platform_slug) ?? 99)
- (platformOrder.get(right.platform_slug) ?? 99)
|| left.store_name.localeCompare(right.store_name, 'zh-CN')
);
});
if (!this.monthlyClosing.total) return rows;
return [{
...this.monthlyClosing.total,
key: 'monthly-store-total',
platform: '合计',
store_name: `合计 · ${rows.length} 个店铺`,
isTotal: true,
available: true,
}, ...rows];
},
},
mounted() {
this.fetchOverview();
this.fetchMonthlyClosing();
},
beforeDestroy() {
if (this.detailOpen) this.unlockDetailScroll();
@ -805,96 +567,6 @@ export default {
this.queryRange = { ...this.dateRange };
if (this.detailOpen) this.closeDetails();
this.fetchOverview();
this.fetchMonthlyClosing();
},
async fetchMonthlyClosing() {
const requestId = ++this.monthlyClosingRequestSerial;
this.monthlyClosingLoading = true;
this.monthlyClosingError = '';
++this.monthlyClosingStoreRequestSerial;
this.monthlyClosingStoreLoaded = false;
this.monthlyStoreMonthDetails = [];
try {
const response = await window.axios.get(`${this.apiBase}/monthly-closing`, {
params: {
month: this.monthlyClosingMonth,
},
});
if (requestId !== this.monthlyClosingRequestSerial) return;
const body = response.data || {};
this.monthlyClosing = {
month: body.month || this.monthlyClosingMonth,
period: body.period || {
start: this.queryRange.start,
end: this.queryRange.end,
},
months: Array.isArray(body.months) ? body.months : [],
summary: {
platform_count: 8,
available_platform_count: 0,
complete: false,
...(body.summary || {}),
},
total: body.total || null,
platforms: Array.isArray(body.platforms) ? body.platforms : [],
};
if (this.monthlyClosingView === 'store') {
this.fetchMonthlyClosingStores();
}
} catch (error) {
if (requestId !== this.monthlyClosingRequestSerial) return;
this.monthlyClosingError = (error.response && error.response.data && error.response.data.message)
|| '月度期末余额汇总暂时无法读取,请稍后重试。';
} finally {
if (requestId === this.monthlyClosingRequestSerial) {
this.monthlyClosingLoading = false;
}
}
},
selectMonthlyClosingView(view) {
if (!['platform', 'store'].includes(view) || this.monthlyClosingView === view) return;
this.monthlyClosingView = view;
if (view === 'store' && !this.monthlyClosingStoreLoaded && !this.monthlyClosingStoreLoading) {
this.fetchMonthlyClosingStores();
}
},
async fetchMonthlyClosingStores() {
const month = Array.isArray(this.monthlyClosing.months)
? this.monthlyClosing.months[0]
: null;
if (!month) return;
const requestId = ++this.monthlyClosingStoreRequestSerial;
this.monthlyClosingStoreLoading = true;
this.monthlyClosingStoreError = '';
this.monthlyStoreMonthDetails = [];
try {
const response = await window.axios.get(`${this.apiBase}/balance-store-detail`, {
params: {
month: month.key,
},
});
if (requestId !== this.monthlyClosingStoreRequestSerial) return;
this.monthlyStoreMonthDetails = [{
month: month.key,
details: Array.isArray(response.data && response.data.details)
? response.data.details
: [],
}];
this.monthlyClosingStoreLoaded = true;
} catch (error) {
if (requestId !== this.monthlyClosingStoreRequestSerial) return;
this.monthlyClosingStoreLoaded = false;
this.monthlyClosingStoreError = (error.response && error.response.data && error.response.data.message)
|| '店铺月度期末余额暂时无法读取,请稍后重试。';
} finally {
if (requestId === this.monthlyClosingStoreRequestSerial) {
this.monthlyClosingStoreLoading = false;
}
}
},
changeDate(field, value) {
const date = value || '';
@ -1044,15 +716,6 @@ export default {
return amount === '—' ? '' : `原值:${amount}`;
},
monthlyBalanceWan(row, month) {
const value = row && row.balances ? row.balances[month] : null;
if (value === null || value === undefined || !Number.isFinite(Number(value))) return '—';
return `¥${(Number(value) / 10000).toLocaleString('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}万`;
},
},
};
</script>
@ -1404,218 +1067,6 @@ export default {
border-radius: 50%;
background: currentColor;
}
.monthly-closing-card {
margin-top: .75rem;
overflow: hidden;
border: 1px solid #ded6c8;
border-radius: .65rem;
background: rgba(255, 255, 255, .92);
box-shadow: 0 10px 28px rgba(63, 50, 29, .05);
}
.monthly-closing-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: .85rem 1rem;
border-bottom: 1px solid #eee7da;
background: linear-gradient(90deg, #fbf5e9, rgba(255, 255, 255, .9));
}
.monthly-closing-title-row {
display: flex;
align-items: center;
gap: .5rem;
}
.monthly-closing-head h3 {
margin: 0;
color: var(--overview-ink);
font-size: .9375rem;
}
.monthly-closing-head p {
margin: .2rem 0 0;
color: var(--overview-muted);
font-size: .6875rem;
}
.monthly-closing-badge {
padding: .1rem .4rem;
border: 1px solid #c9b68f;
border-radius: 999px;
background: #fffaf0;
color: #775725;
font-size: .625rem;
font-weight: 700;
}
.monthly-closing-count {
flex: 0 0 auto;
color: #55743f;
font-size: .6875rem;
font-weight: 600;
}
.monthly-closing-head-actions {
display: flex;
flex: 0 0 auto;
align-items: center;
gap: .75rem;
}
.monthly-closing-view-switch {
display: inline-flex;
padding: 2px;
border: 1px solid #d8cdbc;
border-radius: .5rem;
background: #f7f1e5;
}
.monthly-closing-view-switch button {
height: 1.9rem;
padding: 0 .75rem;
border: 0;
border-radius: .38rem;
background: transparent;
color: var(--overview-muted);
font: 600 .6875rem/1.2 inherit;
white-space: nowrap;
cursor: pointer;
}
.monthly-closing-view-switch button:hover {
color: var(--overview-gold);
}
.monthly-closing-view-switch button.is-active {
background: #fffdf8;
color: var(--overview-gold);
box-shadow: 0 1px 2px rgba(63, 50, 29, .10);
}
.monthly-closing-view-switch button:focus-visible {
box-shadow: 0 0 0 3px rgba(138, 104, 47, .18);
outline: none;
}
.monthly-closing-scroll {
overflow-x: auto;
overscroll-behavior-inline: contain;
}
.monthly-closing-scroll.is-store {
max-height: 30rem;
overflow: auto;
}
.monthly-closing-scroll.is-store .monthly-closing-table thead th {
position: sticky;
z-index: 2;
top: 0;
}
.monthly-closing-table {
width: 100%;
min-width: 34rem;
border-collapse: separate;
border-spacing: 0;
font-size: .75rem;
font-variant-numeric: tabular-nums;
}
.monthly-closing-store-table {
min-width: 46rem;
}
.monthly-closing-table th,
.monthly-closing-table td {
padding: .65rem .8rem;
border-right: 1px solid #eeeae2;
border-bottom: 1px solid #eeeae2;
white-space: nowrap;
}
.monthly-closing-table thead th {
background: #f8f2e6;
color: #5f574b;
font-size: .6875rem;
font-weight: 700;
text-align: left;
}
.monthly-closing-table thead th.is-number,
.monthly-closing-table td.is-number {
text-align: right;
}
.monthly-closing-table thead th:last-child,
.monthly-closing-table td:last-child {
border-right: 0;
}
.monthly-closing-table thead th.monthly-closing-month-head {
background: #f3e5c9;
color: #6e5025;
}
.monthly-closing-table tbody th {
position: sticky;
left: 0;
z-index: 1;
background: #fff;
color: #3e3a34;
font-weight: 600;
text-align: left;
}
.monthly-closing-table tbody tr:hover th,
.monthly-closing-table tbody tr:hover td {
background: #fbf7ef;
}
.monthly-closing-table tbody tr.is-total th,
.monthly-closing-table tbody tr.is-total td {
background: #f8f2e6;
color: #171717;
font-weight: 700;
}
.monthly-closing-table tbody tr.is-unavailable td {
color: #937b57;
}
.monthly-closing-store-table .monthly-store-name {
min-width: 15rem;
}
.monthly-closing-foot {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: .35rem 1rem;
padding: .55rem .8rem;
border-top: 1px solid #eee7da;
background: #fcfaf5;
color: var(--overview-muted);
font-size: .625rem;
}
.monthly-closing-loading,
.monthly-closing-error {
min-height: 6rem;
}
.monthly-closing-loading,
.monthly-closing-error {
padding: 1rem;
}
.monthly-closing-loading {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: .5rem;
}
.monthly-closing-loading span {
height: .75rem;
border-radius: .25rem;
background: linear-gradient(90deg, #eee8dd 25%, #f8f4ed 50%, #eee8dd 75%);
background-size: 200% 100%;
animation: overview-shimmer 1.25s infinite linear;
}
.monthly-closing-loading p {
grid-column: 1 / -1;
margin: 0;
color: var(--overview-muted);
font-size: .6875rem;
}
.monthly-closing-error {
display: flex;
align-items: center;
justify-content: space-between;
gap: .75rem;
color: #8a3a27;
font-size: .75rem;
}
.monthly-closing-error button {
padding: .25rem .55rem;
border: 1px solid currentColor;
border-radius: .35rem;
background: transparent;
color: inherit;
font: inherit;
cursor: pointer;
}
.flow-detail-modal {
position: fixed;
z-index: 10000;
@ -1907,19 +1358,6 @@ export default {
.overview-cards {
grid-template-columns: 1fr;
}
.monthly-closing-head {
align-items: flex-start;
flex-direction: column;
}
.monthly-closing-head-actions {
width: 100%;
flex-wrap: wrap;
justify-content: space-between;
}
.monthly-closing-foot {
align-items: flex-start;
flex-direction: column;
}
.flow-detail-modal {
align-items: stretch;
padding: .5rem;

View File

@ -39,8 +39,6 @@ Route::post('store-account-passwords/{account}/reveal', 'Business\StoreAccountPa
Route::get('ecommerce-finance/account-bills/overview', [StoreReceivingBalanceOverview::class, 'index'])
->name('ecommerce-finance.account-bills.overview');
Route::get('ecommerce-finance/account-bills/overview/monthly-closing', [StoreReceivingBalanceOverview::class, 'monthlyClosing'])
->name('ecommerce-finance.account-bills.overview.monthly-closing');
Route::get('ecommerce-finance/account-bills/overview/income-detail', [StoreReceivingBalanceOverview::class, 'incomeDetail'])
->name('ecommerce-finance.account-bills.overview.income-detail');
Route::get('ecommerce-finance/account-bills/overview/expense-detail', [StoreReceivingBalanceOverview::class, 'expenseDetail'])

View File

@ -79,42 +79,6 @@ class StoreReceivingBalanceOverviewEndpointTest extends FeatureTestCase
->assertJsonPath('summary.complete', true);
}
public function test_monthly_closing_endpoint_returns_selected_month_and_eight_platforms(): void
{
$this->user->attachPermission(
Permission::where('name', 'read-admin-panel')->firstOrFail()
);
$payload = [
'month' => '2026-10',
'months' => [['key' => '2026-10', 'label' => '10月']],
'summary' => [
'platform_count' => 8,
'available_platform_count' => 8,
'complete' => true,
],
'total' => [
'slug' => 'total',
'name' => '合计',
'balances' => ['2026-10' => 1040.0],
],
'platforms' => [],
];
$service = Mockery::mock(StoreReceivingBalanceOverviewService::class);
$service->shouldReceive('monthlyClosing')->once()->with('2026-10')->andReturn($payload);
$this->app->instance(StoreReceivingBalanceOverviewService::class, $service);
$this->loginAs()
->getJson(route('ecommerce-finance.account-bills.overview.monthly-closing', [
'month' => '2026-10',
]))
->assertOk()
->assertJsonCount(1, 'months')
->assertJsonPath('months.0.label', '10月')
->assertJsonPath('summary.complete', true)
->assertJsonMissingPath('total.change');
}
public function test_overview_page_uses_the_vue_mount_point(): void
{
$this->user->attachPermission(

View File

@ -320,40 +320,6 @@ class StoreReceivingBalanceOverviewServiceTest extends TestCase
$this->assertNull($rows[0]['amount']);
}
public function test_monthly_closing_returns_only_the_selected_month_for_all_platforms(): void
{
$seenPeriods = [];
$service = $this->service(function (string $platform, string $start, string $end) use (&$seenPeriods): array {
$seenPeriods[] = [$platform, $start, $end];
$index = array_search($platform, array_column(StoreReceivingBalanceOverviewService::PLATFORMS, 'slug'), true) + 1;
return [
'opening_balance' => 1000.00 * $index,
'income' => 10.00,
'expense' => 0.0,
];
});
$result = $service->monthlyClosing('2026-10');
$this->assertCount(8, $seenPeriods);
$this->assertSame(
['2026-10-01|2026-10-31'],
array_values(array_unique(array_map(
static fn (array $period): string => $period[1] . '|' . $period[2],
$seenPeriods,
))),
);
$this->assertSame(['2026-10'], array_column($result['months'], 'key'));
$this->assertSame('10月', $result['months'][0]['label']);
$this->assertCount(8, $result['platforms']);
$this->assertSame(['2026-10' => 1010.00], $result['platforms'][0]['balances']);
$this->assertArrayNotHasKey('change', $result['platforms'][0]);
$this->assertSame('合计', $result['total']['name']);
$this->assertTrue($result['summary']['complete']);
$this->assertSame(8, $result['summary']['available_platform_count']);
}
private function service(callable $summary): StoreReceivingBalanceOverviewService
{
return new class($summary) extends StoreReceivingBalanceOverviewService {

View File

@ -65,35 +65,16 @@ class StoreReceivingBalanceOverviewTest extends TestCase
);
}
public function test_overview_contains_selected_month_closing_summary_without_comparison(): void
public function test_overview_does_not_render_monthly_closing_summary(): void
{
$source = file_get_contents(
__DIR__ . '/../../../resources/assets/js/components/business/StoreReceivingBalanceOverview.vue'
);
$this->assertStringContainsString('月度期末余额汇总', $source);
$this->assertStringContainsString('平台账户', $source);
$this->assertStringContainsString('monthly-closing-table', $source);
$this->assertStringContainsString('monthlyClosingMonthLabel', $source);
$this->assertStringContainsString('const selectedDate = this.queryRange.end || this.queryRange.start', $source);
$this->assertStringContainsString('month: this.monthlyClosingMonth', $source);
$this->assertStringContainsString('month: month.key', $source);
$this->assertStringContainsString("'各平台'", $source);
$this->assertStringContainsString('金额单位:万元', $source);
$this->assertStringContainsString('monthlyBalanceWan', $source);
$this->assertStringContainsString('Number(value) / 10000', $source);
$this->assertStringContainsString('monthlyClosingRows', $source);
$this->assertStringContainsString('fetchMonthlyClosing', $source);
$this->assertStringContainsString('店铺汇总', $source);
$this->assertStringContainsString('monthly-closing-view-switch', $source);
$this->assertStringContainsString('selectMonthlyClosingView', $source);
$this->assertStringContainsString('fetchMonthlyClosingStores', $source);
$this->assertStringContainsString('monthlyStoreRows', $source);
$this->assertStringContainsString('monthly-closing-store-table', $source);
$this->assertStringContainsString('店铺期末余额 = 期初余额 + 收入 - 支出', $source);
$this->assertStringContainsString('缺少期初时显示“—”', $source);
$this->assertStringNotContainsString('月度期末余额汇总', $source);
$this->assertStringNotContainsString('monthly-closing', $source);
$this->assertStringNotContainsString('fetchMonthlyClosing', $source);
$this->assertStringNotContainsString('monthlyClosing', $source);
$this->assertStringNotContainsString('环比', $source);
$this->assertStringNotContainsString('2026 年 7-10 月', $source);
$this->assertStringNotContainsString('body.comparison', $source);
}
}