From 2f3852271aedcca1147aa0def78b36f9cb4d098f Mon Sep 17 00:00:00 2001 From: qiuxiang <394405259@qq.com> Date: Mon, 26 Oct 2026 15:47:50 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=E7=B2=BE=E7=AE=80=E5=AE=89?= =?UTF-8?q?=E8=A3=85=E6=98=8E=E7=BB=86=E7=AD=9B=E9=80=89=E5=8C=BA=E5=B9=B6?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=94=9F=E6=95=88=E6=9D=A1=E4=BB=B6=E6=A0=87?= =?UTF-8?q?=E7=AD=BE=20(#602)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 统一安装明细时间口径与日期区间,收敛常驻筛选,增加快捷区间、可删除生效条件和低频筛选折叠。 --- .../InstallationDetailMirrorRepository.php | 29 +- .../partials/date-range-picker.blade.php | 1 + .../installation-fee-summary.blade.php | 4 +- .../partials/installation-filters.blade.php | 320 ++++++++++++++ resources/views/business/pages/show.blade.php | 406 ++++++++++++------ tests/Feature/Business/ManagementCrudTest.php | 92 +++- ...InstallationDetailMirrorRepositoryTest.php | 28 +- 7 files changed, 729 insertions(+), 151 deletions(-) create mode 100644 resources/views/business/pages/partials/installation-filters.blade.php diff --git a/modules/EcommerceFinance/Repositories/InstallationDetailMirrorRepository.php b/modules/EcommerceFinance/Repositories/InstallationDetailMirrorRepository.php index a33cb19a..5356e0ac 100644 --- a/modules/EcommerceFinance/Repositories/InstallationDetailMirrorRepository.php +++ b/modules/EcommerceFinance/Repositories/InstallationDetailMirrorRepository.php @@ -212,16 +212,39 @@ class InstallationDetailMirrorRepository public static function requestFilters(): array { $rules = []; + $rules['time_scope'] = ['nullable', 'in:created,customer_review,finance_review']; + $rules['date_from'] = ['nullable', 'date_format:Y-m-d']; + $rules['date_to'] = ['nullable', 'date_format:Y-m-d']; + if (request()->filled('date_from')) $rules['date_to'][] = 'after_or_equal:date_from'; foreach (['created', 'customer_review', 'finance_review'] as $prefix) { $rules[$prefix . '_from'] = ['nullable', 'date_format:Y-m-d']; $rules[$prefix . '_to'] = ['nullable', 'date_format:Y-m-d']; if (request()->filled($prefix . '_from')) $rules[$prefix . '_to'][] = 'after_or_equal:' . $prefix . '_from'; } - foreach (['push_status','order_no','customer_review','finance_review','quote_status','arranged_status','fee_payer','completed_status','shop_name','delete_status','installation_company','installation_no','order_type','product_id'] as $key) { + foreach (['push_status','order_no','customer_review','finance_review','quote_status','arranged_status','fee_payer','completed_status','shop_name','shop_code','delete_status','installation_company','installation_no','order_type','product_id'] as $key) { $rules[$key] = ['nullable', 'string', 'max:191']; } foreach (['customer_review', 'finance_review', 'arranged_status', 'completed_status', 'delete_status'] as $key) $rules[$key][] = 'in:0,1'; - return validator(request()->only(array_keys($rules)), $rules)->validate(); + $filters = validator(request()->only(array_keys($rules)), $rules)->validate(); + if (! array_key_exists('time_scope', $filters) + && ! array_key_exists('date_from', $filters) + && ! array_key_exists('date_to', $filters)) { + return $filters; + } + + $scope = (string) ($filters['time_scope'] ?? 'customer_review'); + $prefix = match ($scope) { + 'created' => 'created', + 'finance_review' => 'finance_review', + default => 'customer_review', + }; + foreach (['created', 'customer_review', 'finance_review'] as $legacyPrefix) { + unset($filters[$legacyPrefix . '_from'], $filters[$legacyPrefix . '_to']); + } + $filters[$prefix . '_from'] = $filters['date_from'] ?? null; + $filters[$prefix . '_to'] = $filters['date_to'] ?? null; + + return $filters; } private function applyFilters($query, array $filters, string $itemTable): void @@ -230,7 +253,7 @@ class InstallationDetailMirrorRepository if (!empty($filters[$prefix.'_from'])) $query->where('v.'.$column, '>=', $filters[$prefix.'_from']); if (!empty($filters[$prefix.'_to'])) $query->where('v.'.$column, '<', \Illuminate\Support\Carbon::parse($filters[$prefix.'_to'])->addDay()->toDateString()); } - foreach (['push_status'=>'apistatus','order_no'=>'OrderNum','quote_status'=>'BidStatus','fee_payer'=>'costtaker','shop_name'=>'shop_name','installation_company'=>'installCompanyName','installation_no'=>'ordernum_id','order_type'=>'TypeDto'] as $key=>$column) { + foreach (['push_status'=>'apistatus','order_no'=>'OrderNum','quote_status'=>'BidStatus','fee_payer'=>'costtaker','shop_name'=>'shop_name','shop_code'=>'shopcode','installation_company'=>'installCompanyName','installation_no'=>'ordernum_id','order_type'=>'TypeDto'] as $key=>$column) { if (isset($filters[$key]) && $filters[$key] !== '') $query->where('v.'.$column, $filters[$key]); } foreach (['customer_review'=>'kfsh','finance_review'=>'cwsh','completed_status'=>'installComplete','delete_status'=>'Delete'] as $key=>$column) { diff --git a/resources/views/business/pages/partials/date-range-picker.blade.php b/resources/views/business/pages/partials/date-range-picker.blade.php index 0de248c4..8dd024b2 100644 --- a/resources/views/business/pages/partials/date-range-picker.blade.php +++ b/resources/views/business/pages/partials/date-range-picker.blade.php @@ -286,6 +286,7 @@ }); input.addEventListener('click', open); + input.addEventListener('input', syncClear); input.addEventListener('keydown', function (event) { if (event.key === 'Enter' || event.key === ' ' || event.key === 'ArrowDown') { event.preventDefault(); diff --git a/resources/views/business/pages/partials/installation-fee-summary.blade.php b/resources/views/business/pages/partials/installation-fee-summary.blade.php index 583b3afe..65440a1a 100644 --- a/resources/views/business/pages/partials/installation-fee-summary.blade.php +++ b/resources/views/business/pages/partials/installation-fee-summary.blade.php @@ -381,8 +381,8 @@ var dialog = document.getElementById('installation-fee-dialog'); if (!form || !card || !dialog) { return; } - var fromInput = form.elements.namedItem('customer_review_from'); - var toInput = form.elements.namedItem('customer_review_to'); + var fromInput = form.elements.namedItem('date_from'); + var toInput = form.elements.namedItem('date_to'); var cardTotal = card.querySelector('[data-installation-fee-total]'); var status = document.querySelector('[data-installation-fee-status]'); var subtitle = dialog.querySelector('[data-installation-fee-subtitle]'); diff --git a/resources/views/business/pages/partials/installation-filters.blade.php b/resources/views/business/pages/partials/installation-filters.blade.php new file mode 100644 index 00000000..8263978d --- /dev/null +++ b/resources/views/business/pages/partials/installation-filters.blade.php @@ -0,0 +1,320 @@ +@php + $installationTimeScopes = [ + 'created' => '创建时间', + 'customer_review' => '客服审核时间', + 'finance_review' => '财务审核时间', + ]; + $installationTimePrefixes = [ + 'created' => 'created', + 'customer_review' => 'customer_review', + 'finance_review' => 'finance_review', + ]; + $usesUnifiedTime = request()->hasAny(['time_scope', 'date_from', 'date_to']); + $installationTimeScope = (string) request('time_scope', ''); + if (! array_key_exists($installationTimeScope, $installationTimeScopes)) { + $installationTimeScope = request()->filled('created_from') || request()->filled('created_to') + ? 'created' + : (request()->filled('finance_review_from') || request()->filled('finance_review_to') + ? 'finance_review' + : 'customer_review'); + } + $installationTimePrefix = $installationTimePrefixes[$installationTimeScope]; + $installationDateFrom = $usesUnifiedTime + ? (string) request('date_from', '') + : (string) request($installationTimePrefix . '_from', ''); + $installationDateTo = $usesUnifiedTime + ? (string) request('date_to', '') + : (string) request($installationTimePrefix . '_to', ''); + $installationStore = (string) request('shop_name', ''); + $installationCompleted = (string) request('completed_status', ''); + $installationMoreFields = [ + 'shop_code' => ['label' => '店铺 ID', 'placeholder' => '按店铺编码精确查询'], + 'order_no' => ['label' => '订单编号', 'placeholder' => '按原值精确查询'], + 'installation_no' => ['label' => '安装单号', 'placeholder' => '按原值精确查询'], + 'installation_company' => ['label' => '安装公司', 'placeholder' => '按原值精确查询'], + 'push_status' => ['label' => '推送状态', 'placeholder' => '按原值精确查询'], + 'quote_status' => ['label' => '报价状态', 'placeholder' => '按原值精确查询'], + 'fee_payer' => ['label' => '安装费承担方', 'placeholder' => '按原值精确查询'], + 'order_type' => ['label' => '订单类型', 'placeholder' => '按原值精确查询'], + 'product_id' => ['label' => '商品 ID', 'placeholder' => '按原值精确查询'], + 'customer_review' => ['label' => '客服审核', 'options' => ['0' => '未审核', '1' => '已审核']], + 'finance_review' => ['label' => '财务审核', 'options' => ['0' => '未审核', '1' => '已审核']], + 'arranged_status' => ['label' => '是否安排', 'options' => ['0' => '未安排', '1' => '已安排']], + 'delete_status' => ['label' => '删除状态', 'options' => ['0' => '未删除', '1' => '已删除']], + ]; + $installationActiveMore = collect($installationMoreFields) + ->filter(static fn (array $field, string $name): bool => request()->filled($name)) + ->map(static fn (array $field, string $name): array => $field + [ + 'name' => $name, + 'value' => (string) request($name, ''), + ]) + ->all(); + $installationMoreCount = count($installationActiveMore); + $installationQuery = request()->query(); + $installationRemoveUrl = static function (array $keys) use ($installationQuery): string { + unset($installationQuery['page']); + foreach ($keys as $key) { + unset($installationQuery[$key]); + } + + $query = \Illuminate\Support\Arr::query($installationQuery); + + return request()->url() . ($query !== '' ? '?' . $query : ''); + }; + $installationDateChip = trim($installationDateFrom . ' 至 ' . $installationDateTo, ' 至'); +@endphp + +
+
+
+ +
+ 日期区间 +
+ + + + + + + 至 + + + + + + +
+
+
+ + + + + +
+ + + 清除 + + + 导出 + +
+
+ +
+
+ 快捷区间 + @foreach (['today' => '今天', 'this_week' => '本周', 'this_month' => '本月', 'last_month' => '上月', 'last_30_days' => '近30天'] as $value => $label) + + @endforeach +
+ +
+ @if ($installationDateFrom !== '' || $installationDateTo !== '') + + {{ $installationTimeScopes[$installationTimeScope] }} + {{ $installationDateChip }} + + + @endif + @if ($installationStore !== '') + + 店铺{{ $installationStore }} + + + @endif + @if ($installationCompleted !== '') + + 安装状态{{ $installationCompleted === '1' ? '已完工' : '未完工' }} + + + @endif + @foreach ($installationActiveMore as $field) + @php + $displayValue = isset($field['options']) + ? ($field['options'][$field['value']] ?? $field['value']) + : $field['value']; + @endphp + + {{ $field['label'] }}{{ $displayValue }} + + + @endforeach +
+
+ + + + +
+ + diff --git a/resources/views/business/pages/show.blade.php b/resources/views/business/pages/show.blade.php index 2d13204f..561471eb 100644 --- a/resources/views/business/pages/show.blade.php +++ b/resources/views/business/pages/show.blade.php @@ -203,12 +203,10 @@ } .installation-filter-bar { - display: grid; - grid-template-columns: repeat(4, minmax(12rem, 1fr)); - gap: 0.65rem 1rem; - padding: 1rem 1.5rem; - border-bottom: 1px solid #f3f4f6; - background: #fcfbf8; + padding: 0.7rem 1.25rem 0.75rem; + border-bottom: 1px solid #e8e2d8; + background: #fffdf8; + color: #595959; } .installation-filter-field { @@ -238,34 +236,207 @@ font-size: 0.75rem; } - .installation-date-filter { - display: flex; - grid-column: span 2; + .installation-filter-compact { + display: block; + } + + .installation-filter-primary { + display: grid; + grid-template-columns: + minmax(28rem, 1.55fr) + minmax(10.5rem, 0.62fr) + minmax(8.5rem, 0.42fr) + auto; + gap: 0.6rem; align-items: center; - gap: 0.4rem; + } + + .installation-filter-time { + display: grid; + grid-template-columns: minmax(7rem, 0.45fr) minmax(17rem, 1.4fr); + gap: 0.55rem; min-width: 0; + } + + .installation-filter-control { + display: flex; + min-width: 0; + flex-direction: column; + gap: 0.22rem; color: #595959; font-size: 0.75rem; + } + + .installation-filter-control > span { + height: 0.95rem; + overflow: hidden; + color: #595959; + font-size: 0.6875rem; + font-weight: 500; + line-height: 0.95rem; + text-overflow: ellipsis; white-space: nowrap; } - .installation-date-filter__label { - flex: 0 0 auto; - } - - .installation-date-filter__range { - flex: 1 1 auto; + .installation-filter-control input, + .installation-filter-control select { + width: 100%; min-width: 0; - } - - .installation-date-filter .upstream-data-date input { - height: 2.1rem; + height: 2rem; + padding: 0 0.55rem; + border: 1px solid #d8d2c7; + border-radius: 0.45rem; + background: #ffffff; + color: #171717; + font: inherit; font-size: 0.75rem; } + .installation-filter-control input:focus, + .installation-filter-control select:focus { + border-color: #856632; + box-shadow: 0 0 0 3px rgba(133, 102, 50, 0.16); + outline: none; + } + + .installation-filter-range .upstream-data-date-range { + gap: 0.35rem; + } + + .installation-filter-range .upstream-data-date input { + height: 2rem; + border: 1px solid #d8d2c7; + font-size: 0.75rem; + } + + .installation-filter-range .upstream-data-date__clear, + .installation-filter-range .upstream-data-date__pick { + height: 2rem; + } + + .installation-filter-actions { + display: flex; + align-items: flex-end; + justify-content: flex-end; + gap: 0.35rem; + white-space: nowrap; + } + + .installation-filter-actions .biz-btn { + height: 2rem; + gap: 0.25rem; + padding: 0 0.62rem; + font-size: 0.75rem; + } + + .installation-filter-actions .material-icons-outlined { + font-size: 1rem; + } + + .installation-more-toggle b { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 1.05rem; + height: 1.05rem; + padding: 0 0.2rem; + border-radius: 999px; + background: #856632; + color: #ffffff; + font-size: 0.625rem; + font-weight: 600; + } + + .installation-more-toggle b[hidden] { + display: none; + } + + .installation-filter-context { + display: flex; + min-height: 1.65rem; + align-items: center; + gap: 0.55rem; + flex-wrap: wrap; + margin-top: 0.55rem; + } + + .installation-quick-ranges, + .installation-active-filters { + display: flex; + align-items: center; + gap: 0.3rem; + flex-wrap: wrap; + } + + .installation-quick-ranges > span { + color: #595959; + font-size: 0.6875rem; + } + + .installation-quick-range { + height: 1.55rem; + padding: 0 0.55rem; + border: 1px solid #ddd6ca; + border-radius: 999px; + background: #ffffff; + color: #595959; + cursor: pointer; + font: inherit; + font-size: 0.6875rem; + line-height: 1; + } + + .installation-quick-range:hover, + .installation-quick-range:focus-visible, + .installation-quick-range.is-active { + border-color: #b9954f; + background: #f7f0de; + color: #856632; + outline: none; + } + + .installation-active-filters { + min-width: 0; + margin-left: auto; + } + + .installation-filter-chip { + display: inline-flex; + max-width: 24rem; + min-height: 1.55rem; + align-items: center; + gap: 0.28rem; + padding: 0.08rem 0.35rem 0.08rem 0.55rem; + border: 1px solid #e2d6bb; + border-radius: 999px; + background: #f7f0de; + color: #6a5129; + font-size: 0.6875rem; + line-height: 1.2; + text-decoration: none; + } + + .installation-filter-chip:hover, + .installation-filter-chip:focus-visible { + border-color: #b9954f; + background: #f2e7c5; + outline: none; + } + + .installation-filter-chip strong { + overflow: hidden; + max-width: 15rem; + font-weight: 500; + text-overflow: ellipsis; + white-space: nowrap; + } + + .installation-filter-chip .material-icons-outlined { + font-size: 0.9rem; + } + .installation-date-range-error { - grid-column: 1 / -1; - margin: 0; + margin: 0.4rem 0 0; color: #b80000; font-size: 0.75rem; } @@ -274,22 +445,92 @@ display: none; } - .installation-filter-actions { - display: flex; - align-items: center; - gap: 0.5rem; + .installation-more-panel { + margin-top: 0.6rem; + padding: 0.65rem 0.75rem 0.75rem; + border: 1px solid #e7dfd1; + border-radius: 0.55rem; + background: #ffffff; } - @media (max-width: 1100px) { - .installation-filter-bar { grid-template-columns: repeat(2, minmax(14rem, 1fr)); } - .installation-date-filter { grid-column: 1 / -1; } + .installation-more-panel[hidden] { + display: none; } + + .installation-more-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + margin-bottom: 0.55rem; + } + + .installation-more-head strong { + color: #171717; + font-size: 0.75rem; + font-weight: 600; + } + + .installation-more-head span { + color: #595959; + font-size: 0.6875rem; + } + + .installation-more-grid { + display: grid; + grid-template-columns: repeat(4, minmax(9rem, 1fr)); + gap: 0.55rem 0.75rem; + } + + @media (max-width: 1280px) { + .installation-filter-primary { + grid-template-columns: minmax(27rem, 1fr) minmax(10rem, 0.55fr) minmax(8rem, 0.4fr); + } + .installation-filter-actions { + grid-column: 1 / -1; + justify-content: flex-start; + } + .installation-more-grid { + grid-template-columns: repeat(3, minmax(9rem, 1fr)); + } + } + + @media (max-width: 980px) { + .installation-filter-primary { + grid-template-columns: minmax(0, 1fr) minmax(11rem, 0.55fr); + } + .installation-filter-time { + grid-column: 1 / -1; + } + .installation-filter-actions { + grid-column: 1 / -1; + } + .installation-more-grid { + grid-template-columns: repeat(2, minmax(9rem, 1fr)); + } + } + @media (max-width: 650px) { - .installation-filter-bar { grid-template-columns: minmax(0, 1fr); padding: 1rem; } - .installation-date-filter { - flex-direction: column; - align-items: stretch; - gap: 0.25rem; + .installation-filter-bar { + padding: 0.75rem; + } + .installation-filter-primary, + .installation-filter-time { + grid-template-columns: minmax(0, 1fr); + } + .installation-filter-time, + .installation-filter-actions { + grid-column: auto; + } + .installation-filter-actions { + justify-content: flex-start; + flex-wrap: wrap; + } + .installation-active-filters { + margin-left: 0; + } + .installation-more-grid { + grid-template-columns: minmax(0, 1fr); } } @@ -773,102 +1014,7 @@
@if ($page['slug'] === 'installation-details') @include('business.pages.partials.date-range-picker') -
- @php - $dateRangeFilters = [ - ['from' => 'created_from', 'to' => 'created_to', 'label' => '创建日期:', 'aria' => '创建日期'], - ['from' => 'customer_review_from', 'to' => 'customer_review_to', 'label' => '客服审核时间:', 'aria' => '客服审核时间'], - ['from' => 'finance_review_from', 'to' => 'finance_review_to', 'label' => '财务审核时间:', 'aria' => '财务审核时间'], - ]; - $filterFields = [ - ['name' => 'push_status', 'label' => '推送状态:'], ['name' => 'order_no', 'label' => '订单编号:'], - ['name' => 'customer_review', 'label' => '客服审核:', 'options' => ['0'=>'未审核','1'=>'已审核']], ['name' => 'finance_review', 'label' => '财务审核:', 'options' => ['0'=>'未审核','1'=>'已审核']], - ['name' => 'quote_status', 'label' => '报价状态:'], ['name' => 'arranged_status', 'label' => '是否安排:', 'options' => ['0'=>'未安排','1'=>'已安排']], - ['name' => 'fee_payer', 'label' => '安装费承担方:'], ['name' => 'completed_status', 'label' => '是否完工:', 'options' => ['0'=>'未完工','1'=>'已完工']], - ['name' => 'shop_name', 'label' => '选择店铺:'], ['name' => 'delete_status', 'label' => '选择删除:', 'options' => ['0'=>'未删除','1'=>'已删除']], - ['name' => 'installation_company', 'label' => '安装公司:'], ['name' => 'installation_no', 'label' => '安装单号:'], - ['name' => 'order_type', 'label' => '订单类型:'], ['name' => 'product_id', 'label' => '商品ID:'], - ]; - @endphp - @foreach ($dateRangeFilters as $dateRange) -
- {{ $dateRange['label'] }} -
- - - - - - - 至 - - - - - - -
-
- @endforeach - @foreach ($filterFields as $filter) - - @endforeach - -
- - 清除 - 导出 -
-
- + @include('business.pages.partials.installation-filters') @include('business.pages.partials.installation-fee-summary') @endif
diff --git a/tests/Feature/Business/ManagementCrudTest.php b/tests/Feature/Business/ManagementCrudTest.php index c47abfa7..e30421c0 100644 --- a/tests/Feature/Business/ManagementCrudTest.php +++ b/tests/Feature/Business/ManagementCrudTest.php @@ -244,25 +244,37 @@ class ManagementCrudTest extends FeatureTestCase ->assertSeeText('安装单号') ->assertSeeText('申请总金额') ->assertSeeText('商品名称(1)') - ->assertSeeText('创建日期:') - ->assertSeeText('客服审核时间:') - ->assertSeeText('财务审核时间:') - ->assertSeeText('推送状态:') - ->assertSeeText('选择店铺:') - ->assertSeeText('选择删除:') - ->assertSeeText('订单类型:') - ->assertSeeText('商品ID:') + ->assertSeeText('时间口径') + ->assertSeeText('日期区间') + ->assertSeeText('店铺') + ->assertSeeText('安装状态') + ->assertSeeText('更多筛选') + ->assertSeeText('今天') + ->assertSeeText('本周') + ->assertSeeText('本月') + ->assertSeeText('上月') + ->assertSeeText('近30天') + ->assertSeeText('推送状态') + ->assertSeeText('订单编号') + ->assertSeeText('店铺 ID') + ->assertSeeText('安装公司') + ->assertSeeText('商品 ID') ->assertSeeText('导出') ->assertSee('id="installationFilterForm"', false) - ->assertSee('name="created_from"', false) - ->assertSee('name="created_to"', false) - ->assertSee('name="customer_review_from"', false) - ->assertSee('name="customer_review_to"', false) - ->assertSee('name="finance_review_from"', false) - ->assertSee('name="finance_review_to"', false) + ->assertSee('name="time_scope"', false) + ->assertSee('name="date_from"', false) + ->assertSee('name="date_to"', false) + ->assertSee('name="shop_name"', false) + ->assertSee('name="completed_status"', false) + ->assertSee('id="installationMoreFilters"', false) + ->assertSee('id="installationMoreFilters" class="installation-more-panel" hidden', false) + ->assertSee('data-installation-more-toggle', false) + ->assertSee('data-installation-quick-range="today"', false) + ->assertSee('data-installation-quick-range="last_30_days"', false) ->assertSee('data-date-picker', false) ->assertSee('data-date-calendar', false) ->assertSee("window.CaiWuDatePicker.setup(form)", false) + ->assertSee("input.addEventListener('input', syncClear)", false) ->assertSee('结束日期不能早于开始日期。') ->assertSee('data-installation-fee-open', false) ->assertSee('id="installation-fee-dialog"', false) @@ -307,6 +319,58 @@ class ManagementCrudTest extends FeatureTestCase $this->assertStringContainsString('INSTALL-EXPORT-001', $response->streamedContent()); } + public function testInstallationDetailsUnifiedFiltersRenderActiveChipsAndKeepLowFrequencyFieldsInMorePanel(): void + { + $mirror = Mockery::mock(InstallationDetailMirrorRepository::class); + $mirror->shouldReceive('paginate')->once()->andReturn([ + 'ready' => true, + 'total' => 1, + 'data' => [array_replace(array_fill_keys(array_keys(InstallationDetail::columnLabels()), null), [ + 'id' => 9, + 'installation_no' => 'INSTALL-CHIP-001', + 'installation_company' => '测试安装公司', + 'store_name' => '测试店铺', + 'requested_amount' => '120.50', + 'settled_amount' => '100.00', + 'related_guid' => 'guid-install-chip', + ])], + ]); + $this->app->instance(InstallationDetailMirrorRepository::class, $mirror); + + $url = route('business.pages.show', [ + 'section' => 'financial-operations', + 'page' => 'installation-details', + ]) . '?' . http_build_query([ + 'time_scope' => 'customer_review', + 'date_from' => '2026-09-01', + 'date_to' => '2026-09-30', + 'shop_name' => '测试店铺', + 'completed_status' => '1', + 'shop_code' => 'SHOP-001', + 'push_status' => 'PUSHED', + 'order_no' => 'ORDER-CHIP-001', + ]); + + $this->loginAs() + ->get($url) + ->assertOk() + ->assertSeeText('客服审核时间') + ->assertSeeText('2026-09-01 至 2026-09-30') + ->assertSeeText('测试店铺') + ->assertSeeText('已完工') + ->assertSeeText('SHOP-001') + ->assertSeeText('PUSHED') + ->assertSeeText('ORDER-CHIP-001') + ->assertSee('aria-label="删除「客服审核时间」条件"', false) + ->assertSee('aria-label="删除「店铺」条件"', false) + ->assertSee('aria-label="删除「安装状态」条件"', false) + ->assertSee('aria-label="删除「店铺 ID」条件"', false) + ->assertSee('aria-label="删除「推送状态」条件"', false) + ->assertSee('aria-label="删除「订单编号」条件"', false) + ->assertSee('data-installation-more-count>3', false) + ->assertDontSee('id="installationMoreFilters" class="installation-more-panel" hidden', false); + } + public function testInstallationDetailsEndpointReturnsChildItemsForMainRecord(): void { $mirror = Mockery::mock(InstallationDetailMirrorRepository::class); diff --git a/tests/Unit/EcommerceFinance/InstallationDetailMirrorRepositoryTest.php b/tests/Unit/EcommerceFinance/InstallationDetailMirrorRepositoryTest.php index 72259c05..77a379ba 100644 --- a/tests/Unit/EcommerceFinance/InstallationDetailMirrorRepositoryTest.php +++ b/tests/Unit/EcommerceFinance/InstallationDetailMirrorRepositoryTest.php @@ -32,7 +32,7 @@ class InstallationDetailMirrorRepositoryTest extends TestCase $schema->create('business_mirror_table_states',function(Blueprint $t){$t->string('dataset_key')->primary();$t->dateTime('initial_completed_at')->nullable();}); $schema->create('fixture_install',function(Blueprint $t){ $t->integer('Id'); - foreach(['SourceGuid','OrderNum','ordernum_id','shop_name','SubmitTime','ShipTIme','kfshTime','cwshTime','apistatus','kfsh','cwsh','BidStatus','costtaker','costWay','installComplete','Delete','installCompanyName','TypeDto'] as $c) $t->string($c)->nullable(); + foreach(['SourceGuid','OrderNum','ordernum_id','shop_name','shopcode','SubmitTime','ShipTIme','kfshTime','cwshTime','apistatus','kfsh','cwsh','BidStatus','costtaker','costWay','installComplete','Delete','installCompanyName','TypeDto'] as $c) $t->string($c)->nullable(); $t->integer('StatusId')->nullable()->default(0); foreach(['installcost','settleAmount','acceptAmount'] as $c) $t->decimal($c,14,2)->default(0); }); @@ -48,7 +48,7 @@ class InstallationDetailMirrorRepositoryTest extends TestCase foreach(array_keys($definitions) as $key) DB::connection('business_mirror')->table('business_mirror_table_states')->insert(['dataset_key'=>$key,'initial_completed_at'=>'2026-08-01']); DB::connection('business_mirror')->table('fixture_install')->insert([ ['Id'=>1,'SourceGuid'=>'A','OrderNum'=>'OLD','shop_name'=>'旧店','SubmitTime'=>'2026-08-01','ShipTIme'=>'2026-08-02','installcost'=>500], - ['Id'=>2,'SourceGuid'=>'A','OrderNum'=>'NEW','shop_name'=>'测试店','SubmitTime'=>'2026-08-20 23:59:59','ShipTIme'=>'2026-08-21','installcost'=>20], + ['Id'=>2,'SourceGuid'=>'A','OrderNum'=>'NEW','shop_name'=>'测试店','shopcode'=>'SHOP-A','SubmitTime'=>'2026-08-20 23:59:59','ShipTIme'=>'2026-08-21','installcost'=>20], ['Id'=>3,'SourceGuid'=>'B','OrderNum'=>'OTHER','shop_name'=>'其他店','SubmitTime'=>'2026-08-20','ShipTIme'=>'2026-08-21','installcost'=>30], ['Id'=>4,'SourceGuid'=>'C','OrderNum'=>'NEXT','shop_name'=>'测试店','SubmitTime'=>'2026-08-21','ShipTIme'=>'2026-08-22','installcost'=>40], ['Id'=>5,'SourceGuid'=>'D','OrderNum'=>'PRE-2026','shop_name'=>'测试店','SubmitTime'=>'2026-08-21','ShipTIme'=>'2025-12-31 23:59:59','installcost'=>999], @@ -71,6 +71,7 @@ class InstallationDetailMirrorRepositoryTest extends TestCase $this->assertSame(1,$this->repository->paginate(1,50,$filters)['total']); $this->assertSame(20.0,$this->repository->summary($filters)['requested']); $this->assertSame(0,$this->repository->paginate(1,50,['shop_name'=>"' OR 1=1 --"])['total']); + $this->assertSame(1,$this->repository->paginate(1,50,['shop_code'=>'SHOP-A'])['total']); } public function test_product_filter_does_not_multiply_main_rows(): void @@ -223,4 +224,27 @@ class InstallationDetailMirrorRepositoryTest extends TestCase $this->expectException(\Illuminate\Validation\ValidationException::class); InstallationDetailMirrorRepository::requestFilters(); } + + public function test_unified_time_scope_maps_to_the_selected_legacy_date_range(): void + { + request()->merge([ + 'time_scope' => 'finance_review', + 'date_from' => '2026-08-01', + 'date_to' => '2026-08-31', + 'created_from' => '2026-07-01', + 'created_to' => '2026-07-31', + 'shop_name' => '测试店', + ]); + + $filters = InstallationDetailMirrorRepository::requestFilters(); + + $this->assertSame('finance_review', $filters['time_scope']); + $this->assertSame('2026-08-01', $filters['date_from']); + $this->assertSame('2026-08-31', $filters['date_to']); + $this->assertSame('2026-08-01', $filters['finance_review_from']); + $this->assertSame('2026-08-31', $filters['finance_review_to']); + $this->assertSame('测试店', $filters['shop_name']); + $this->assertArrayNotHasKey('created_from', $filters); + $this->assertArrayNotHasKey('created_to', $filters); + } } From a2344d9136365a2f71a929a98a20e395ddb9adf5 Mon Sep 17 00:00:00 2001 From: qiuxiang <394405259@qq.com> Date: Mon, 26 Oct 2026 16:01:23 +0800 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20=E9=87=8D=E5=81=9A=E5=AE=89?= =?UTF-8?q?=E8=A3=85=E8=B4=B9=E9=87=91=E9=A2=9D=E6=B1=87=E6=80=BB=E5=8D=A1?= =?UTF-8?q?=20(#603)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 安装费汇总改为单一主视觉卡片,动态展示环比、包安装和不包安装构成。 --- app/Http/Controllers/Business/Pages.php | 4 + .../InstallationDetailMirrorRepository.php | 107 +++++-- .../installation-fee-summary.blade.php | 278 ++++++++++++++---- tests/Feature/Business/ManagementCrudTest.php | 19 +- ...InstallationDetailMirrorRepositoryTest.php | 5 + 5 files changed, 329 insertions(+), 84 deletions(-) diff --git a/app/Http/Controllers/Business/Pages.php b/app/Http/Controllers/Business/Pages.php index ea4535b1..9a7517bb 100644 --- a/app/Http/Controllers/Business/Pages.php +++ b/app/Http/Controllers/Business/Pages.php @@ -1116,6 +1116,10 @@ class Pages extends Controller return response()->json([ 'total' => $payload['total'], 'rows' => $payload['rows'], + 'previous_total' => $payload['previous_total'] ?? null, + 'previous_from' => $payload['previous_from'] ?? null, + 'previous_to' => $payload['previous_to'] ?? null, + 'change_rate' => $payload['change_rate'] ?? null, ], 200, ['Cache-Control' => 'private, no-store']); } diff --git a/modules/EcommerceFinance/Repositories/InstallationDetailMirrorRepository.php b/modules/EcommerceFinance/Repositories/InstallationDetailMirrorRepository.php index 5356e0ac..e77ff7aa 100644 --- a/modules/EcommerceFinance/Repositories/InstallationDetailMirrorRepository.php +++ b/modules/EcommerceFinance/Repositories/InstallationDetailMirrorRepository.php @@ -136,29 +136,32 @@ class InstallationDetailMirrorRepository $items = $this->datasets->get(self::ITEM_DATASET); $connection = $this->mirror->connection(); if (! $this->ready($dataset, $items, $connection->getName())) { - return ['ready' => false, 'total' => 0.0, 'rows' => []]; + return [ + 'ready' => false, + 'total' => 0.0, + 'rows' => [], + 'previous_total' => null, + 'previous_from' => null, + 'previous_to' => null, + 'change_rate' => null, + ]; } $from = trim((string) ($filters['customer_review_from'] ?? '')); $to = trim((string) ($filters['customer_review_to'] ?? '')); if ($from === '' || $to === '') { - return ['ready' => true, 'total' => 0.0, 'rows' => []]; + return [ + 'ready' => true, + 'total' => 0.0, + 'rows' => [], + 'previous_total' => null, + 'previous_from' => null, + 'previous_to' => null, + 'change_rate' => null, + ]; } - $latest = $connection->table($dataset->targetTable()) - ->selectRaw('MAX(Id) AS id') - ->groupBy('SourceGuid'); - $main = $connection->table($dataset->targetTable() . ' AS v') - ->joinSub($latest, 'latest_install', static fn ($join) => $join->on('latest_install.id', '=', 'v.Id')) - ->select(['v.SourceGuid', 'v.shop_name', 'v.installCompanyName', 'v.costWay']); - $this->applyShipmentCutoff($main); - - $query = $connection->table($items->targetTable() . ' AS i') - ->joinSub($main, 'install_main', static fn ($join) => - $join->on('install_main.SourceGuid', '=', 'i.guid')) - ->whereNotNull('i.kfshTime') - ->where('i.kfshTime', '>=', $from) - ->where('i.kfshTime', '<', \Illuminate\Support\Carbon::parse($to)->addDay()->toDateString()); + $query = $this->feeDetailsQuery($connection, $dataset, $items, $from, $to); $platformSql = "CASE" . " WHEN COALESCE(TRIM(install_main.shop_name), '') LIKE '%阿里巴巴%'" @@ -202,10 +205,80 @@ class InstallationDetailMirrorRepository ->values() ->all(); + $total = round(array_sum(array_column($rows, 'amount')), 2); + $previousRange = $this->previousMonthRange($from, $to); + $previousTotal = null; + $changeRate = null; + if ($previousRange !== null) { + $previousQuery = $this->feeDetailsQuery( + $connection, + $dataset, + $items, + $previousRange['from'], + $previousRange['to'], + ); + $previousTotal = round((float) ($previousQuery + ->selectRaw('COALESCE(SUM(CAST(i.settleItemAmount AS DECIMAL(18,2))), 0) AS total') + ->first()->total ?? 0), 2); + if ($previousTotal > 0) { + $changeRate = round((($total - $previousTotal) / $previousTotal) * 100, 1); + } + } + return [ 'ready' => true, - 'total' => round(array_sum(array_column($rows, 'amount')), 2), + 'total' => $total, 'rows' => $rows, + 'previous_total' => $previousTotal, + 'previous_from' => $previousRange['from'] ?? null, + 'previous_to' => $previousRange['to'] ?? null, + 'change_rate' => $changeRate, + ]; + } + + private function feeDetailsQuery( + $connection, + $dataset, + $items, + string $from, + string $to, + ) { + $latest = $connection->table($dataset->targetTable()) + ->selectRaw('MAX(Id) AS id') + ->groupBy('SourceGuid'); + $main = $connection->table($dataset->targetTable() . ' AS v') + ->joinSub($latest, 'latest_install', static fn ($join) => $join->on('latest_install.id', '=', 'v.Id')) + ->select(['v.SourceGuid', 'v.shop_name', 'v.installCompanyName', 'v.costWay']); + $this->applyShipmentCutoff($main); + + return $connection->table($items->targetTable() . ' AS i') + ->joinSub($main, 'install_main', static fn ($join) => + $join->on('install_main.SourceGuid', '=', 'i.guid')) + ->whereNotNull('i.kfshTime') + ->where('i.kfshTime', '>=', $from) + ->where('i.kfshTime', '<', \Illuminate\Support\Carbon::parse($to)->addDay()->toDateString()); + } + + /** @return array{from:string,to:string}|null */ + private function previousMonthRange(string $from, string $to): ?array + { + $start = \Illuminate\Support\Carbon::parse($from)->startOfDay(); + $end = \Illuminate\Support\Carbon::parse($to)->startOfDay(); + if ($end->lt($start)) { + return null; + } + + if ($start->day === 1 && $end->isSameDay($end->copy()->endOfMonth())) { + $previousStart = $start->copy()->subMonthNoOverflow()->startOfMonth(); + $previousEnd = $previousStart->copy()->endOfMonth(); + } else { + $previousStart = $start->copy()->subMonthNoOverflow(); + $previousEnd = $end->copy()->subMonthNoOverflow(); + } + + return [ + 'from' => $previousStart->toDateString(), + 'to' => $previousEnd->toDateString(), ]; } diff --git a/resources/views/business/pages/partials/installation-fee-summary.blade.php b/resources/views/business/pages/partials/installation-fee-summary.blade.php index 65440a1a..f0d5ed38 100644 --- a/resources/views/business/pages/partials/installation-fee-summary.blade.php +++ b/resources/views/business/pages/partials/installation-fee-summary.blade.php @@ -1,47 +1,21 @@
-
-
- 安装费金额 - 按客服审核时间汇总安装明细的结算明细金额;收款渠道为空归为包安装,有值归为不包安装。 -
- 请选择完整的客服审核起止日期 -
@@ -384,7 +458,12 @@ var fromInput = form.elements.namedItem('date_from'); var toInput = form.elements.namedItem('date_to'); var cardTotal = card.querySelector('[data-installation-fee-total]'); - var status = document.querySelector('[data-installation-fee-status]'); + var status = card.querySelector('[data-installation-fee-status]'); + var trend = card.querySelector('[data-installation-fee-trend]'); + var packageAmount = card.querySelector('[data-installation-fee-package]'); + var packagePercent = card.querySelector('[data-installation-fee-package-percent]'); + var nonPackageAmount = card.querySelector('[data-installation-fee-non-package]'); + var nonPackagePercent = card.querySelector('[data-installation-fee-non-package-percent]'); var subtitle = dialog.querySelector('[data-installation-fee-subtitle]'); var currentTotal = dialog.querySelector('[data-installation-fee-current-total]'); var platformFilter = dialog.querySelector('[data-installation-fee-platform]'); @@ -414,6 +493,78 @@ }); } + function amount(value) { + return Number(value || 0).toLocaleString('zh-CN', { + minimumFractionDigits: 2, + maximumFractionDigits: 2 + }); + } + + function captionRange(value) { + var matchFrom = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value.from); + var matchTo = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value.to); + if (!matchFrom || !matchTo) { + return value.from + ' 至 ' + value.to; + } + var end = matchFrom[1] === matchTo[1] + ? matchTo[2] + '-' + matchTo[3] + : value.to; + return value.from + ' 至 ' + end; + } + + function setTrend(changeRate) { + trend.className = 'installation-fee-card__trend'; + if (changeRate === null || changeRate === undefined || !Number.isFinite(Number(changeRate))) { + trend.textContent = '—'; + return; + } + + var rate = Number(changeRate); + if (rate > 0) { + trend.textContent = '↑' + Math.abs(rate).toLocaleString('zh-CN', { maximumFractionDigits: 1 }) + '%'; + trend.classList.add('is-up'); + } else if (rate < 0) { + trend.textContent = '↓' + Math.abs(rate).toLocaleString('zh-CN', { maximumFractionDigits: 1 }) + '%'; + trend.classList.add('is-down'); + } else { + trend.textContent = '持平'; + } + } + + function setCardValue(total, rows, value, data) { + var packageTotal = 0; + var nonPackageTotal = 0; + rows.forEach(function (row) { + if (row.payment_channel === '包安装') { + packageTotal += Number(row.amount || 0); + } else { + nonPackageTotal += Number(row.amount || 0); + } + }); + var packageRate = total > 0 ? Math.round((packageTotal / total) * 100) : 0; + var nonPackageRate = total > 0 ? 100 - packageRate : 0; + + cardTotal.textContent = wan(total); + packageAmount.textContent = amount(packageTotal); + nonPackageAmount.textContent = amount(nonPackageTotal); + packagePercent.textContent = '(' + packageRate + '%)'; + nonPackagePercent.textContent = '(' + nonPackageRate + '%)'; + setTrend(data.change_rate); + status.classList.remove('is-error'); + status.textContent = '按客服审核时间 · ' + captionRange(value) + ' · ' + rows.length + ' 单'; + } + + function resetCard(message) { + cardTotal.textContent = '0.00'; + packageAmount.textContent = '0.00'; + nonPackageAmount.textContent = '0.00'; + packagePercent.textContent = '(0%)'; + nonPackagePercent.textContent = '(0%)'; + setTrend(null); + status.textContent = message; + status.classList.toggle('is-error', message.indexOf('失败') >= 0 || message.indexOf('不能') >= 0); + } + function range() { return { from: String(fromInput?.value || ''), @@ -542,14 +693,12 @@ function refresh(force) { var value = range(); if (!value.from || !value.to) { - cardTotal.textContent = '0.00'; - status.textContent = '请选择完整的客服审核起止日期'; + resetCard('请选择完整的客服审核起止日期'); resetDialog('请选择完整的客服审核起止日期'); return Promise.resolve(false); } if (value.from > value.to) { - cardTotal.textContent = '0.00'; - status.textContent = '客服审核结束日期不能早于开始日期'; + resetCard('客服审核结束日期不能早于开始日期'); resetDialog('客服审核结束日期不能早于开始日期'); return Promise.resolve(false); } @@ -561,7 +710,8 @@ if (controller) { controller.abort(); } controller = new AbortController(); dialog.setAttribute('aria-busy', 'true'); - status.textContent = '正在汇总安装费…'; + status.classList.remove('is-error'); + status.textContent = '按客服审核时间 · 正在汇总…'; cardTotal.textContent = '…'; var url = new URL(card.dataset.url, window.location.origin); url.searchParams.set('customer_review_from', value.from); @@ -581,8 +731,7 @@ }).then(function (data) { allRows = Array.isArray(data.rows) ? data.rows : []; loadedSignature = currentSignature; - cardTotal.textContent = wan(data.total); - status.textContent = value.from + ' 至 ' + value.to + ' · ' + allRows.length + ' 条汇总'; + setCardValue(Number(data.total || 0), allRows, value, data); subtitle.textContent = value.from + ' 至 ' + value.to + ' · 按客服审核时间汇总'; fillFilter(platformFilter, '全部平台', distinct('platform')); fillFilter(storeFilter, '全部店铺', distinct('store_name')); @@ -593,8 +742,7 @@ return true; }).catch(function (error) { if (error && error.name === 'AbortError') { return false; } - cardTotal.textContent = '0.00'; - status.textContent = error.message || '安装费汇总读取失败'; + resetCard(error.message || '安装费汇总读取失败'); resetDialog(error.message || '安装费汇总读取失败,请稍后重试。'); dialog.removeAttribute('aria-busy'); return false; diff --git a/tests/Feature/Business/ManagementCrudTest.php b/tests/Feature/Business/ManagementCrudTest.php index e30421c0..9b7f581e 100644 --- a/tests/Feature/Business/ManagementCrudTest.php +++ b/tests/Feature/Business/ManagementCrudTest.php @@ -279,7 +279,16 @@ class ManagementCrudTest extends FeatureTestCase ->assertSee('data-installation-fee-open', false) ->assertSee('id="installation-fee-dialog"', false) ->assertSeeText('安装费金额') - ->assertSeeText('按客服审核时间汇总安装明细的结算明细金额') + ->assertSeeText('较上月') + ->assertSeeText('包安装') + ->assertSeeText('不包安装') + ->assertSee('data-installation-fee-trend', false) + ->assertSee('data-installation-fee-package', false) + ->assertSee('data-installation-fee-non-package', false) + ->assertSee('border-left: 3px solid #006395', false) + ->assertSee('font-size: 2.625rem', false) + ->assertSee('function captionRange(value)', false) + ->assertSee('function setCardValue(total, rows, value, data)', false) ->assertSeeText('平台') ->assertSeeText('店铺名称') ->assertSeeText('安装公司') @@ -406,6 +415,10 @@ class ManagementCrudTest extends FeatureTestCase ['platform' => '淘系', 'store_name' => '店铺A', 'installation_company' => '万师傅', 'payment_channel' => '包安装', 'amount' => 1250.50], ['platform' => '京东', 'store_name' => '店铺B', 'installation_company' => '鲁班到家', 'payment_channel' => '不包安装', 'amount' => 750.00], ], + 'previous_total' => 1600.40, + 'previous_from' => '2026-08-01', + 'previous_to' => '2026-08-31', + 'change_rate' => 25.0, ]); $this->app->instance(InstallationDetailMirrorRepository::class, $mirror); @@ -419,7 +432,9 @@ class ManagementCrudTest extends FeatureTestCase ->assertJsonPath('rows.0.platform', '淘系') ->assertJsonPath('rows.0.store_name', '店铺A') ->assertJsonPath('rows.0.payment_channel', '包安装') - ->assertJsonPath('rows.1.payment_channel', '不包安装'); + ->assertJsonPath('rows.1.payment_channel', '不包安装') + ->assertJsonPath('previous_total', 1600.4) + ->assertJsonPath('change_rate', 25.0); } public function testInstallationSummaryRequiresCompleteCustomerReviewRange(): void diff --git a/tests/Unit/EcommerceFinance/InstallationDetailMirrorRepositoryTest.php b/tests/Unit/EcommerceFinance/InstallationDetailMirrorRepositoryTest.php index 77a379ba..7716bb9c 100644 --- a/tests/Unit/EcommerceFinance/InstallationDetailMirrorRepositoryTest.php +++ b/tests/Unit/EcommerceFinance/InstallationDetailMirrorRepositoryTest.php @@ -140,6 +140,7 @@ class InstallationDetailMirrorRepositoryTest extends TestCase DB::connection('business_mirror')->table('fixture_install')->where('Id',3)->update(['installCompanyName'=>'鲁班到家','costWay'=>'银行转账']); DB::connection('business_mirror')->table('fixture_install')->where('Id',4)->update(['installCompanyName'=>'万师傅']); DB::connection('business_mirror')->table('fixture_items')->insert([ + ['Id'=>19,'guid'=>'A','shopproid'=>'P0','Sourceno'=>'A-0','kfshTime'=>'2026-08-15 10:00:00','settleItemAmount'=>90.00,'acceptItemAmount'=>80.00], ['Id'=>20,'guid'=>'A','shopproid'=>'P1','Sourceno'=>'A-1','kfshTime'=>'2026-09-01 09:00:00','settleItemAmount'=>60.10,'acceptItemAmount'=>40.00], ['Id'=>21,'guid'=>'A','shopproid'=>'P2','Sourceno'=>'A-1','kfshTime'=>'2026-09-30 18:00:00','settleItemAmount'=>39.90,'acceptItemAmount'=>0.00], ['Id'=>22,'guid'=>'B','shopproid'=>'P3','Sourceno'=>'B-1','kfshTime'=>'2026-09-15 10:00:00','settleItemAmount'=>80.00,'acceptItemAmount'=>70.00], @@ -159,6 +160,10 @@ class InstallationDetailMirrorRepositoryTest extends TestCase ['platform'=>'其他','store_name'=>'其他店','installation_company'=>'鲁班到家','payment_channel'=>'不包安装','amount'=>80.0], ],$result['rows']); $this->assertSame(180.0,$result['total']); + $this->assertSame(90.0,$result['previous_total']); + $this->assertSame('2026-08-01',$result['previous_from']); + $this->assertSame('2026-08-31',$result['previous_to']); + $this->assertSame(100.0,$result['change_rate']); } public function test_fee_details_classifies_platform_by_store_name_rules(): void From b434adbf611ddbe8683f88adc89db8ba1491f658 Mon Sep 17 00:00:00 2001 From: qiuxiang <394405259@qq.com> Date: Mon, 26 Oct 2026 16:05:38 +0800 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20=E5=AE=89=E8=A3=85=E8=B4=B9=E7=8E=AF?= =?UTF-8?q?=E6=AF=94=E5=9B=BA=E5=AE=9A=E6=98=BE=E7=A4=BA=E4=B8=80=E4=BD=8D?= =?UTF-8?q?=E5=B0=8F=E6=95=B0=20(#604)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 统一安装费环比的小数位展示。 --- .../pages/partials/installation-fee-summary.blade.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/views/business/pages/partials/installation-fee-summary.blade.php b/resources/views/business/pages/partials/installation-fee-summary.blade.php index f0d5ed38..b236ce43 100644 --- a/resources/views/business/pages/partials/installation-fee-summary.blade.php +++ b/resources/views/business/pages/partials/installation-fee-summary.blade.php @@ -521,10 +521,10 @@ var rate = Number(changeRate); if (rate > 0) { - trend.textContent = '↑' + Math.abs(rate).toLocaleString('zh-CN', { maximumFractionDigits: 1 }) + '%'; + trend.textContent = '↑' + Math.abs(rate).toLocaleString('zh-CN', { minimumFractionDigits: 1, maximumFractionDigits: 1 }) + '%'; trend.classList.add('is-up'); } else if (rate < 0) { - trend.textContent = '↓' + Math.abs(rate).toLocaleString('zh-CN', { maximumFractionDigits: 1 }) + '%'; + trend.textContent = '↓' + Math.abs(rate).toLocaleString('zh-CN', { minimumFractionDigits: 1, maximumFractionDigits: 1 }) + '%'; trend.classList.add('is-down'); } else { trend.textContent = '持平'; From 450a9e42f970e36d131ccb7fd72d18b728c0910c Mon Sep 17 00:00:00 2001 From: DoDo <1060330844@qq.com> Date: Mon, 26 Oct 2026 16:13:27 +0800 Subject: [PATCH 4/4] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E7=BB=8F?= =?UTF-8?q?=E8=90=A5=E6=8A=A5=E8=A1=A8=E6=B1=87=E6=80=BB=E4=B8=8E=E5=9F=BA?= =?UTF-8?q?=E7=A1=80=E8=B5=84=E6=96=99=E7=8A=B6=E6=80=81=E6=A0=87=E7=AD=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 经营报表保留原表头并补齐本表汇总;基础资料只增加表内分色标签。 --- app/Business/BusinessSummaryCards.php | 182 +++++++++++++ app/Business/UpstreamBusinessInformation.php | 256 +++++++----------- app/Http/Controllers/Business/Pages.php | 28 +- config/business_navigation.php | 99 +++++-- .../BusinessInformationMirrorRepository.php | 6 +- .../InternalFlowReportRepository.php | 50 +++- .../partials/business-summary-cards.blade.php | 52 +++- .../pages/partials/directory-table.blade.php | 83 +++++- .../Business/InternalFlowReportPageTest.php | 12 + tests/Feature/Business/PrototypePageTest.php | 108 +++++--- .../Console/InternalFlowReportImportTest.php | 5 + ...usinessInformationMirrorRepositoryTest.php | 2 + .../StoreReportRepositoryTest.php | 162 ++++++++++- .../UpstreamBusinessInformationTest.php | 23 +- 14 files changed, 805 insertions(+), 263 deletions(-) create mode 100644 app/Business/BusinessSummaryCards.php diff --git a/app/Business/BusinessSummaryCards.php b/app/Business/BusinessSummaryCards.php new file mode 100644 index 00000000..dbbaac72 --- /dev/null +++ b/app/Business/BusinessSummaryCards.php @@ -0,0 +1,182 @@ + $summary + * @return list,tone:string}> + */ + public function forPage(string $key, array $summary = []): array + { + return match ($key) { + 'store-management/store-report' => $summary === [] ? [] : $this->storeReportCards($summary), + 'company-management/subject-operating-report' => $summary === [] ? [] : $this->subjectOperatingReportCards($summary), + 'store-management/internal-flow-report' => $summary === [] ? [] : $this->internalFlowReportCards($summary), + 'company-management/company-cost-arrangement' => $this->companyCostArrangementCards(), + 'company-management/tax-forecast' => $this->taxForecastCards(), + 'company-management/personnel-inventory' => $this->personnelInventoryCards(), + default => [], + }; + } + + /** @param array $summary */ + private function subjectOperatingReportCards(array $summary): array + { + $ready = (bool) ($summary['company_status_ready'] ?? false); + $previousLabel = (string) ($summary['previous_month_label'] ?? '上月'); + $currentLabel = (string) ($summary['current_month_label'] ?? '当月'); + $previousStores = (int) ($summary['previous_income_entities'] ?? 0); + $currentStores = (int) ($summary['current_income_entities'] ?? 0); + + if (! $ready) { + return [ + $this->card('正常存续主体', '—', '公司资料镜像未就绪,暂时无法剔除注销主体', [], 'text', 'warning'), + $this->card('上月收入', '—', '公司资料镜像未就绪', [], 'text', 'warning'), + $this->card('当月收入', '—', '公司资料镜像未就绪', [], 'text', 'warning'), + $this->card('小规模纳税人', '—', '公司资料镜像未就绪', [], 'text'), + $this->card('一般纳税人', '—', '公司资料镜像未就绪', [], 'text'), + ]; + } + + return [ + $this->card('正常存续主体', $summary['normal'] ?? 0, '只统计公司台账标签为保留的主体', ['company_status' => '正常'], 'integer', 'accent'), + $this->card($previousLabel . '收入', $summary['previous_income'] ?? 0, $previousStores . ' 个存续主体有收入', [ + 'company_status' => '正常', + 'sort' => $summary['previous_month_key'] ?? null, + 'direction' => 'desc', + ], 'amount', 'success'), + $this->card($currentLabel . '收入', $summary['current_month_income'] ?? 0, $currentStores . ' 个存续主体有收入', [ + 'company_status' => '正常', + 'sort' => $summary['current_month_key'] ?? null, + 'direction' => 'desc', + ], 'amount', 'warning'), + $this->card('小规模纳税人', $summary['small_taxpayer'] ?? 0, '正常存续主体中的小规模纳税人', [ + 'company_status' => '正常', + 'tax_type' => '小规模纳税人', + ]), + $this->card('一般纳税人', $summary['general_taxpayer'] ?? 0, '正常存续主体中的一般纳税人', [ + 'company_status' => '正常', + 'tax_type' => '一般纳税人', + ]), + ]; + } + + /** @param array $summary */ + private function storeReportCards(array $summary): array + { + $previousLabel = (string) ($summary['previous_month_label'] ?? '上月'); + $currentLabel = (string) ($summary['current_month_label'] ?? '当月'); + $normalStores = (int) ($summary['normal_stores'] ?? 0); + $previousStores = (int) ($summary['previous_month_stores'] ?? 0); + $currentStores = (int) ($summary['current_month_stores'] ?? 0); + $previousShare = $normalStores > 0 + ? number_format($previousStores / $normalStores * 100, 1) . '%' + : '0.0%'; + + return [ + $this->card('正常存续店铺', $normalStores, '只统计开店中店铺,不含关店和已注销', ['status' => '0'], 'integer', 'accent'), + $this->card($previousLabel . '销量', $summary['previous_month_sales'] ?? 0, $previousStores . ' 家存续店铺有销量', [ + 'status' => '0', + 'sort' => $summary['previous_month_key'] ?? null, + 'direction' => 'desc', + ], 'amount', 'success'), + $this->card($currentLabel . '销量', $summary['current_month_sales'] ?? 0, $currentStores . ' 家存续店铺有销量', [ + 'status' => '0', + 'sort' => $summary['current_month_key'] ?? null, + 'direction' => 'desc', + ], 'amount', 'warning'), + $this->card($previousLabel . '有销量店铺', $previousStores, '占正常存续店铺 ' . $previousShare, [ + 'status' => '0', + 'sort' => $summary['previous_month_key'] ?? null, + 'direction' => 'desc', + ]), + $this->card($currentLabel . '有销量店铺', $currentStores, $currentStores === 0 ? '当前还未同步当月销量' : '正常存续店铺中当月已有销量', [ + 'status' => '0', + 'sort' => $summary['current_month_key'] ?? null, + 'direction' => 'desc', + ]), + ]; + } + + /** @param array $summary */ + private function internalFlowReportCards(array $summary): array + { + return [ + $this->card('公司数', $summary['companies'] ?? 0, '当前筛选内内部流转公司', [], 'integer', 'accent'), + $this->card('预计销量合计', $summary['projected_sales'] ?? 0, '当前筛选内预计销量合计', [ + 'sort' => 'projected_sales', + 'direction' => 'desc', + ], 'amount'), + $this->card('已收票合计', $summary['receipt_total'] ?? 0, '供应商、关联公司与内部流转收票合计', [ + 'sort' => 'receipt_total', + 'direction' => 'desc', + ], 'amount', 'success'), + $this->card('剩余缺口合计', $summary['remaining_gap'] ?? 0, '金额越大越应优先处理', [ + 'sort' => 'remaining_gap', + 'direction' => 'desc', + ], 'amount', 'warning'), + $this->card('平均开票比例', $summary['average_invoicing_ratio'] ?? 0, '按有比例的公司简单平均', [], 'percent'), + ]; + } + + private function companyCostArrangementCards(): array + { + return [ + $this->card('数据状态', '待接入', '当前表尚无业务数据', [], 'text', 'warning'), + $this->card('完整表头', '11 列', '包含序号,收入、成本、税金、人工等字段全部保留', [], 'text'), + $this->card('核心结果', '预计结余 / 缺口', '当月收入预测减去成本与费用', [], 'text'), + $this->card('缺口提示', '负值标红', '成本或费用未覆盖收入时提醒', [], 'text', 'danger'), + $this->card('配平提示', '零或正结余标绿', '表示当月预算已覆盖', [], 'text', 'success'), + ]; + } + + private function taxForecastCards(): array + { + return [ + $this->card('数据状态', '待取数', '规则已定义,业务数据尚未接入', [], 'text', 'warning'), + $this->card('完整表头', '22 列', '包含序号、征管口径、收入、税额与备注', [], 'text'), + $this->card('计算税种', '4 类', '增值税、附加税、印花税、所得税', [], 'text'), + $this->card('必补口径', '2 项', '工商类型与征收方式', [], 'text', 'warning'), + $this->card('当前记录', '0 条', '数据接入后按完整字段展示', [], 'text'), + ]; + } + + private function personnelInventoryCards(): array + { + return [ + $this->card('数据状态', '待接入', '当前表尚无人员数据', [], 'text', 'warning'), + $this->card('完整表头', '17 列', '包含序号和全部人员、容量字段', [], 'text'), + $this->card('敏感字段', '4 项', '密码、身份证、实名手机和身份证地址只显示维护状态', [], 'text', 'danger'), + $this->card('容量规则', '3 档', '可担任公司数量按风险档位提示', [], 'text'), + $this->card('当前记录', '0 条', '接入后按正常在职人员汇总', [], 'text'), + ]; + } + + /** + * @return array{label:string,value:mixed,hint:string,format:string,query:array,tone:string} + */ + private function card( + string $label, + mixed $value, + string $hint, + array $query = [], + string $format = 'integer', + string $tone = 'neutral', + ): array { + return [ + 'label' => $label, + 'value' => $value, + 'hint' => $hint, + 'format' => $format, + 'query' => ['page' => null, ...$query], + 'tone' => $tone, + ]; + } +} diff --git a/app/Business/UpstreamBusinessInformation.php b/app/Business/UpstreamBusinessInformation.php index 9481db3a..67b39aa1 100644 --- a/app/Business/UpstreamBusinessInformation.php +++ b/app/Business/UpstreamBusinessInformation.php @@ -45,7 +45,7 @@ class UpstreamBusinessInformation }; request()->attributes->set( 'business-page-summary-cards', - $this->businessSummaryCards($key, $payload['summary'] ?? []), + app(BusinessSummaryCards::class)->forPage($key, $payload['summary'] ?? []), ); return new LengthAwarePaginator( @@ -60,97 +60,6 @@ class UpstreamBusinessInformation ); } - private function businessSummaryCards(string $key, array $summary): array - { - if ($summary === []) { - return []; - } - - return match ($key) { - 'store-management/store-report' => [ - $this->summaryCard('店铺总数', $summary['stores'] ?? 0, '当前筛选内唯一店铺,明细按期间拆分'), - $this->summaryCard('正常店铺', $summary['normal'] ?? 0, '店铺状态为正常(开店中)', ['status' => '0']), - $this->summaryCard('关店中', $summary['closing'] ?? 0, '店铺状态为关闭中', ['status' => '3']), - $this->summaryCard('已关闭', $summary['closed'] ?? 0, '店铺状态为已关闭', ['status' => '6']), - $this->summaryCard('期间销量', $summary['period_sales'] ?? 0, '收入 + 优惠 + 冲减 + 退款', [ - 'sort' => 'period_sales', - 'direction' => 'desc', - ], 'money'), - ], - 'company-management/subject-operating-report' => [ - $this->summaryCard('主体总数', $summary['total'] ?? 0, '当前筛选内唯一主体'), - ($summary['company_status_ready'] ?? false) - ? $this->summaryCard('有效主体', $summary['normal'] ?? 0, '经营状态为正常', ['company_status' => '正常']) - : $this->summaryCard('有效主体', '—', '公司资料镜像未就绪', [], 'text'), - ($summary['company_status_ready'] ?? false) - ? $this->summaryCard('已注销主体', $summary['deregistered'] ?? 0, '最新公司资料状态为已注销', ['company_status' => '已注销']) - : $this->summaryCard('已注销主体', '—', '公司资料镜像未就绪', [], 'text'), - $this->summaryCard('最近12个月收入', $summary['rolling_income'] ?? 0, '最近 12 个完整月账单收入', [ - 'sort' => 'rolling_12m_income', - 'direction' => 'desc', - ], 'money'), - $this->summaryCard('本月收入', $summary['current_month_income'] ?? 0, (string) ($summary['current_month_label'] ?? '本月') . '账单收入', [ - 'sort' => 'income_' . ($summary['current_month_number'] ?? now()->format('m')), - 'direction' => 'desc', - ], 'money'), - ], - 'store-management/basic-information' => [ - $this->summaryCard('店铺总数', $summary['total'] ?? 0, '当前筛选内店铺档案'), - $this->summaryCard('正常店铺', $summary['normal'] ?? 0, '店铺状态为正常', ['store_status' => '正常']), - $this->summaryCard('关闭中', $summary['closing'] ?? 0, '店铺状态为关闭中', ['store_status' => '关闭中']), - $this->summaryCard('已关闭', $summary['closed'] ?? 0, '店铺状态为已关闭', ['store_status' => '已关闭']), - $this->summaryCard('平台数', $summary['platforms'] ?? 0, '按店铺档案平台去重', [ - 'sort' => 'platform', - 'direction' => 'asc', - ]), - ], - 'store-management/store-accounts' => [ - $this->summaryCard('账号总数', $summary['total'] ?? 0, '当前筛选内账号记录'), - $this->summaryCard('主账号', $summary['main_accounts'] ?? 0, '账号类型为 MAIN', ['account_type' => '主账号']), - $this->summaryCard('子账号', $summary['sub_accounts'] ?? 0, '账号类型为 SUB', ['account_type' => '子账号']), - $this->summaryCard('密码已设置', $summary['password_set'] ?? 0, '仅统计是否设置,不展示密码', ['password_configured' => '已设置']), - $this->summaryCard('密码未设置', $summary['password_missing'] ?? 0, '需店铺负责人补充', ['password_configured' => '未设置']), - ], - 'company-management/basic-information' => [ - $this->summaryCard('公司总数', $summary['total'] ?? 0, '当前筛选内公司档案'), - $this->summaryCard('正常公司', $summary['normal'] ?? 0, '公司状态为正常', ['company_status' => '正常']), - $this->summaryCard('已注销公司', $summary['deregistered'] ?? 0, '公司状态为已注销', ['company_status' => '已注销']), - $this->summaryCard('一般纳税人', $summary['general_taxpayer'] ?? 0, '纳税人类型为一般纳税人', ['taxpayer_type' => '一般纳税人']), - $this->summaryCard('小规模纳税人', $summary['small_taxpayer'] ?? 0, '纳税人类型为小规模纳税人', ['taxpayer_type' => '小规模纳税人']), - ], - 'trademark-management/information' => [ - $this->summaryCard('商标总数', $summary['total'] ?? 0, '当前筛选内商标档案'), - $this->summaryCard('有效商标', $summary['enabled'] ?? 0, '商标状态为有效', ['trademark_status' => '有效']), - $this->summaryCard('未启用', $summary['disabled'] ?? 0, '商标状态为未启用', ['trademark_status' => '未启用']), - $this->summaryCard('最终持有人', $summary['holders'] ?? 0, '按非空最终持有人去重', [ - 'sort' => 'final_holder', - 'direction' => 'asc', - ]), - $this->summaryCard('保管部门', $summary['departments'] ?? 0, '按非空保管部门去重', [ - 'sort' => 'custody_department', - 'direction' => 'asc', - ]), - ], - default => [], - }; - } - - private function summaryCard( - string $label, - mixed $value, - string $hint, - array $query = [], - string $format = 'integer', - ): array { - return [ - 'label' => $label, - 'value' => $value, - 'hint' => $hint, - 'format' => $format, - 'query' => ['page' => null, ...$query], - ]; - } - /** * 财务经营风险管控 › 流程统计:店铺申请流程(开店 / 变更 / 关店)只读镜像。 * @@ -410,12 +319,6 @@ SQL); $filters = StoreReportRepository::requestFilters(); $rows = $this->storeReportRows($filters); - // 所有店铺该月都没有业绩的月份列默认隐藏;「筛选字段」中仍保留勾选项,可手动选出来。 - request()->attributes->set( - 'store-report.default_hidden_columns', - $this->columnsWithoutData($rows, 'sales_', zeroCountsAsData: false), - ); - $offset = ($page - 1) * $perPage; return [ @@ -428,9 +331,21 @@ SQL); /** @param list> $rows */ private function storeReportSummary(array $rows): array { - $stores = []; - $periodSales = 0.0; + $normalStores = []; + $previousMonth = now()->startOfMonth()->subMonthNoOverflow(); + $currentMonth = now()->startOfMonth(); + $previousMonthKey = 'sales_' . $previousMonth->format('m'); + $currentMonthKey = 'sales_' . $currentMonth->format('m'); + $previousSales = 0.0; + $currentSales = 0.0; + $previousStores = []; + $currentStores = []; + foreach ($rows as $row) { + if (trim((string) ($row['status'] ?? '')) !== '开店中') { + continue; + } + $key = trim((string) ($row['erp_store_name'] ?? '')); if ($key === '') { $key = trim((string) ($row['store_id'] ?? $row['id'] ?? '')); @@ -438,18 +353,32 @@ SQL); if ($key === '') { continue; } - $stores[$key] = (string) ($row['status'] ?? ''); - $periodSales += (float) ($row['period_sales'] ?? 0); + + $normalStores[$key] = true; + $previousValue = (float) ($row[$previousMonthKey] ?? 0); + $currentValue = (float) ($row[$currentMonthKey] ?? 0); + $previousSales += $previousValue; + $currentSales += $currentValue; + if ($previousValue > 0) { + $previousStores[$key] = true; + } + if ($currentValue > 0) { + $currentStores[$key] = true; + } } - $statusCounts = array_count_values($stores); - return [ - 'stores' => count($stores), - 'normal' => (int) ($statusCounts['开店中'] ?? 0), - 'closing' => (int) ($statusCounts['关店中'] ?? 0), - 'closed' => (int) ($statusCounts['关店'] ?? 0), - 'period_sales' => round($periodSales, 2), + 'normal_stores' => count($normalStores), + 'previous_month_sales' => round($previousSales, 2), + 'current_month_sales' => round($currentSales, 2), + 'previous_month_stores' => count($previousStores), + 'current_month_stores' => count($currentStores), + 'previous_month_key' => $previousMonthKey, + 'current_month_key' => $currentMonthKey, + 'previous_month_label' => $previousMonth->isSameYear($currentMonth) + ? '上月(' . $previousMonth->format('n月') . ')' + : '上月(' . $previousMonth->format('Y年n月') . ')', + 'current_month_label' => '当月(' . $currentMonth->format('n月') . ')', ]; } @@ -601,21 +530,26 @@ SQL); $row['city'] = $this->cityFromDetails($row['company_details']); $row['company_status'] = $row['company_details'][0]['company_status'] ?? null; $classification = $classifications[$row['entity']] ?? []; - $row['tax_type'] = $classification['tax_type'] + $taxType = $classification['tax_type'] ?? $row['company_details'][0]['taxpayer_category'] ?? null; - $row['business_registration_type'] = $classification['business_registration_type'] + $businessType = $classification['business_registration_type'] ?? $row['company_details'][0]['business_registration_type'] ?? null; + $classificationStatus = trim((string) ($classification['status'] ?? '')); + $retained = $classificationStatus !== '' + ? $classificationStatus === '保留' + : $row['company_status'] === '正常'; + $row['company_status'] = $retained ? '正常' : '已注销'; + $row['tax_type'] = $retained + ? ($this->normalizeTaxType($taxType) ?? '待维护') + : '/'; + $row['business_registration_type'] = $retained + ? (trim((string) $businessType) !== '' ? trim((string) $businessType) : '待维护') + : '/'; } unset($row); - // 所有主体该月收入均为空时,该月份列在页面上默认隐藏;「筛选字段」中仍保留勾选项,可手动选出来。 - request()->attributes->set( - 'subject-operating-report.default_hidden_columns', - $this->columnsWithoutData($rows, 'income_', zeroCountsAsData: true), - ); - $rows = array_values(array_filter($rows, fn (array $row): bool => $this->subjectMatchesFilters($row, $filters))); $summary = $this->subjectSummary($rows, $companyStatusReady); $this->sortSubjectRows($rows, $filters); @@ -630,9 +564,17 @@ SQL); { $normal = 0; $deregistered = 0; - $rollingIncome = 0.0; - $currentMonthKey = 'income_' . now()->format('m'); + $smallTaxpayers = 0; + $generalTaxpayers = 0; + $previousMonth = now()->startOfMonth()->subMonthNoOverflow(); + $currentMonth = now()->startOfMonth(); + $previousMonthKey = 'income_' . $previousMonth->format('m'); + $currentMonthKey = 'income_' . $currentMonth->format('m'); + $previousIncome = 0.0; $currentMonthIncome = 0.0; + $previousIncomeEntities = 0; + $currentIncomeEntities = 0; + foreach ($rows as $row) { $status = trim((string) ($row['company_status'] ?? '')); if ($status === '正常') { @@ -640,11 +582,28 @@ SQL); } elseif ($status === '已注销') { $deregistered++; } - if (($row['rolling_12m_income'] ?? null) !== null) { - $rollingIncome += (float) $row['rolling_12m_income']; + + if ($status !== '正常') { + continue; } - if (($row[$currentMonthKey] ?? null) !== null) { - $currentMonthIncome += (float) $row[$currentMonthKey]; + + $taxType = (string) ($row['tax_type'] ?? ''); + if (str_contains($taxType, '小规模')) { + $smallTaxpayers++; + } + if (str_contains($taxType, '一般纳税人')) { + $generalTaxpayers++; + } + + $previousValue = (float) ($row[$previousMonthKey] ?? 0); + $currentValue = (float) ($row[$currentMonthKey] ?? 0); + $previousIncome += $previousValue; + $currentMonthIncome += $currentValue; + if ($previousValue > 0) { + $previousIncomeEntities++; + } + if ($currentValue > 0) { + $currentIncomeEntities++; } } @@ -652,45 +611,34 @@ SQL); 'total' => count($rows), 'normal' => $normal, 'deregistered' => $deregistered, - 'rolling_income' => round($rollingIncome, 2), + 'small_taxpayer' => $smallTaxpayers, + 'general_taxpayer' => $generalTaxpayers, + 'previous_income' => round($previousIncome, 2), 'current_month_income' => round($currentMonthIncome, 2), - 'current_month_number' => now()->format('m'), - 'current_month_label' => now()->format('Y年n月'), + 'previous_income_entities' => $previousIncomeEntities, + 'current_income_entities' => $currentIncomeEntities, + 'previous_month_key' => $previousMonthKey, + 'current_month_key' => $currentMonthKey, + 'previous_month_label' => $previousMonth->isSameYear($currentMonth) + ? '上月(' . $previousMonth->format('n月') . ')' + : '上月(' . $previousMonth->format('Y年n月') . ')', + 'current_month_label' => '当月(' . $currentMonth->format('n月') . ')', 'company_status_ready' => $companyStatusReady, ]; } - /** - * 没有数据的月份列:所有行该月都没有数值时返回该列名,页面据此默认隐藏。 - * - * 收入(income_)以 null/空字符串表示无账单,金额 0 也算有账单; - * 销量(sales_)没有空值标记,数值 0 视为当月没有业绩。 - * - * @param array> $rows - * @return array - */ - private function columnsWithoutData(array $rows, string $prefix, bool $zeroCountsAsData): array + private function normalizeTaxType(mixed $value): ?string { - $columns = []; - - for ($month = 1; $month <= 12; $month++) { - $key = sprintf('%s%02d', $prefix, $month); - foreach ($rows as $row) { - $value = $row[$key] ?? null; - if ($value === null || $value === '') { - continue; - } - if (! $zeroCountsAsData && (float) $value === 0.0) { - continue; - } - - continue 2; - } - - $columns[] = $key; + $taxType = trim((string) $value); + if ($taxType === '') { + return null; } - return $columns; + return match ($taxType) { + '小规模' => '小规模纳税人', + '一般纳税人', '小规模纳税人', '个体工商户', '无税务登记' => $taxType, + default => $taxType, + }; } /** @param array $row @param array $filters */ diff --git a/app/Http/Controllers/Business/Pages.php b/app/Http/Controllers/Business/Pages.php index 9a7517bb..617fd6e6 100644 --- a/app/Http/Controllers/Business/Pages.php +++ b/app/Http/Controllers/Business/Pages.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers\Business; use App\Business\BusinessTableImporter; +use App\Business\BusinessSummaryCards; use App\Business\InventoryInboundDetailImporter; use App\Business\InventoryWarehouseNameMappingWorkbookImporter; use App\Business\InventoryWarehouseShortNames; @@ -3768,9 +3769,25 @@ class Pages extends Controller $agreementCourierContractParties = $this->agreementCourierContractParties($page['slug'] ?? null, $agreementCourierNames); $agreementSupplierOptions = $this->agreementSupplierOptions($page['slug'] ?? null); + $staticSummaryKey = (string) ($section['slug'] ?? '') . '/' . (string) ($page['slug'] ?? ''); + if (in_array($staticSummaryKey, [ + 'company-management/company-cost-arrangement', + 'company-management/tax-forecast', + 'company-management/personnel-inventory', + ], true)) { + request()->attributes->set( + 'business-page-summary-cards', + app(BusinessSummaryCards::class)->forPage($staticSummaryKey), + ); + } + // 内部流转报表读取共享业务镜像中由工作簿导入的数据集,不走上游只读查询。 if ($page && $content === null && $this->isInternalFlowReportPage($section, $page)) { - [$rows, $upstreamError] = $this->internalFlowReportRows(); + [$rows, $upstreamError, $flowSummary] = $this->internalFlowReportRows(); + request()->attributes->set( + 'business-page-summary-cards', + app(BusinessSummaryCards::class)->forPage('store-management/internal-flow-report', $flowSummary), + ); } if (($section['slug'] ?? null) === 'product-inventory' && ($page['slug'] ?? null) === 'inventory-report') { @@ -4220,7 +4237,7 @@ class Pages extends Controller * 内部流转报表明细。数据来自共享业务镜像中按工作簿整份替换的导入结果, * 尚未导入时保留空页面并说明原因,不把“未导入”当成业务数据为 0。 * - * @return array{0: LengthAwarePaginator, 1: ?string} + * @return array{0: LengthAwarePaginator, 1: ?string, 2: array} */ private function internalFlowReportRows(): array { @@ -4240,6 +4257,7 @@ class Pages extends Controller return [ $paginate([], 0), '内部流转数据尚未导入。导入《一般纳税人内部流转》工作簿后,本页会显示明细。', + [], ]; } @@ -4254,7 +4272,11 @@ class Pages extends Controller (string) request('direction', 'asc'), ); - return [$paginate($payload['data'], $payload['total']), null]; + return [ + $paginate($payload['data'], $payload['total']), + null, + (array) ($payload['summary'] ?? []), + ]; } private function storeReceivingAccountRows(?string $tab): LengthAwarePaginator diff --git a/config/business_navigation.php b/config/business_navigation.php index dd31dff8..88ae629f 100644 --- a/config/business_navigation.php +++ b/config/business_navigation.php @@ -17,9 +17,16 @@ $navigation = [ 'fields' => [ ['name' => 'city', 'label' => '市'], ['name' => 'entity', 'label' => '主体'], - ['name' => 'company_type', 'label' => '公司类型'], - ['name' => 'tax_type', 'label' => '税务类型'], - ['name' => 'business_registration_type', 'label' => '工商类型'], + ['name' => 'company_type', 'label' => '公司类型', 'kind' => 'badge'], + ['name' => 'tax_type', 'label' => '税务类型', 'kind' => 'badge', 'tones' => [ + '小规模纳税人' => 'info', + '一般纳税人' => 'accent', + '个体工商户' => 'success', + '无税务登记' => 'warning', + '待维护' => 'warning', + '/' => 'muted', + ]], + ['name' => 'business_registration_type', 'label' => '工商类型', 'kind' => 'badge'], ['name' => 'rolling_12m_income', 'label' => '最近12个月累计收入', 'kind' => 'number'], ...array_map(static fn (int $month): array => ['name' => sprintf('income_%02d', $month), 'label' => $month . '月收入', 'kind' => 'number'], range(1, 12)), ['name' => 'can_change_entity', 'label' => '能否变更主体'], @@ -38,8 +45,11 @@ $navigation = [ 'empty' => '公司成本安排尚未同步数据。', 'fields' => [ ['name' => 'company', 'label' => '公司'], - ['name' => 'company_type', 'label' => '公司类型'], - ['name' => 'invoice_type', 'label' => '发票类型'], + ['name' => 'company_type', 'label' => '公司类型', 'kind' => 'badge'], + ['name' => 'invoice_type', 'label' => '发票类型', 'kind' => 'badge', 'tones' => [ + '专票' => 'accent', + '普票' => 'info', + ]], ['name' => 'current_month_income_forecast', 'label' => '当月收入预测', 'kind' => 'number'], ['name' => 'historical_cost_gap', 'label' => '往期成本缺口', 'kind' => 'number'], ['name' => 'supplier_cost_arrangement', 'label' => '供应商成本安排', 'kind' => 'number'], @@ -61,9 +71,21 @@ $navigation = [ 'empty' => '税金预测表尚未接入取数:工商类型、征收方式(查账征收/核定征收)确认并补齐数据来源后开始计算。', 'fields' => [ ['name' => 'company_name', 'label' => '公司名称'], - ['name' => 'taxpayer_type', 'label' => '公司税务类型'], - ['name' => 'business_registration_type', 'label' => '工商类型'], - ['name' => 'collection_method', 'label' => '征收方式'], + ['name' => 'taxpayer_type', 'label' => '公司税务类型', 'kind' => 'badge', 'tones' => [ + '小规模纳税人' => 'info', + '一般纳税人' => 'accent', + '个体工商户' => 'success', + '无税务登记' => 'warning', + '待维护' => 'warning', + '/' => 'muted', + ]], + ['name' => 'business_registration_type', 'label' => '工商类型', 'kind' => 'badge'], + ['name' => 'collection_method', 'label' => '征收方式', 'kind' => 'badge', 'tones' => [ + '查账征收' => 'info', + '核定征收' => 'accent', + '待维护' => 'warning', + '/' => 'muted', + ]], ['name' => 'assessed_income_rate', 'label' => '核定应税所得率(%)', 'kind' => 'number'], ['name' => 'forecast_month', 'label' => '预测月份'], ['name' => 'revenue_incl_tax', 'label' => '当月含税收入预测', 'kind' => 'number'], @@ -95,9 +117,16 @@ $navigation = [ 'fields' => [ ['name' => 'unified_social_credit_code', 'label' => '统一社会信用代码'], ['name' => 'company_name', 'label' => '公司名称'], - ['name' => 'taxpayer_type', 'label' => '税务类型', 'source' => 'taxpayer_category'], - ['name' => 'business_registration_type', 'label' => '工商类型'], - ['name' => 'company_status', 'label' => '公司状态'], + ['name' => 'taxpayer_type', 'label' => '税务类型', 'source' => 'taxpayer_category', 'kind' => 'badge', 'tones' => [ + '小规模纳税人' => 'info', + '一般纳税人' => 'accent', + '个体工商户' => 'success', + '无税务登记' => 'warning', + '待维护' => 'warning', + '/' => 'muted', + ]], + ['name' => 'business_registration_type', 'label' => '工商类型', 'kind' => 'badge'], + ['name' => 'company_status', 'label' => '公司状态', 'kind' => 'badge'], ['name' => 'subscribed_capital', 'label' => '认缴注册资本(万元)', 'kind' => 'number'], ['name' => 'registered_at', 'label' => '注册时间', 'kind' => 'date', 'source' => 'establishment_date'], ['name' => 'legal_representative', 'label' => '法人'], @@ -123,8 +152,8 @@ $navigation = [ 'fields' => [ ['name' => 'person_name', 'label' => '人员名称'], ['name' => 'alias', 'label' => '花名'], - ['name' => 'position', 'label' => '职位'], - ['name' => 'tag', 'label' => '标签'], + ['name' => 'position', 'label' => '职位', 'kind' => 'badge'], + ['name' => 'tag', 'label' => '标签', 'kind' => 'badge'], ['name' => 'tax_account', 'label' => '税务局账号'], ['name' => 'tax_password', 'label' => '税务局密码', 'kind' => 'secret'], ['name' => 'department', 'label' => '部门'], @@ -133,7 +162,7 @@ $navigation = [ ['name' => 'verified_mobile', 'label' => '实名手机号', 'kind' => 'secret'], ['name' => 'sim_card_keeper', 'label' => '电话卡保管人'], ['name' => 'id_address', 'label' => '身份证地址', 'kind' => 'secret'], - ['name' => 'company_type', 'label' => '公司类型'], + ['name' => 'company_type', 'label' => '公司类型', 'kind' => 'badge'], ['name' => 'total_company_count', 'label' => '合计担任公司数量', 'kind' => 'number'], ['name' => 'retained_company_count', 'label' => '保留担任公司数量', 'kind' => 'number'], ['name' => 'available_company_count', 'label' => '可担任公司数量', 'kind' => 'number'], @@ -169,7 +198,7 @@ $navigation = [ 'fields' => [ ['name' => 'trademark_name', 'label' => '商标名称'], ['name' => 'trademark_number', 'label' => '商标号'], - ['name' => 'trademark_status', 'label' => '商标状态'], + ['name' => 'trademark_status', 'label' => '商标状态', 'kind' => 'badge'], ['name' => 'registration_date', 'label' => '注册时间', 'kind' => 'date'], ['name' => 'expiry_date', 'label' => '有效期', 'kind' => 'date'], ['name' => 'international_classification', 'label' => '国际分类', 'source' => 'trademark_type'], @@ -265,10 +294,10 @@ $navigation = [ ['name' => 'store_manager', 'label' => '店铺负责人'], ['name' => 'department_manager', 'label' => '部门负责人'], ['name' => 'opened_at', 'label' => '开店时间', 'kind' => 'date'], - ['name' => 'status', 'label' => '状态'], + ['name' => 'status', 'label' => '状态', 'kind' => 'badge'], ['name' => 'can_change', 'label' => '能否变更'], ['name' => 'entity', 'label' => '主体'], - ['name' => 'company_type', 'label' => '公司类型'], + ['name' => 'company_type', 'label' => '公司类型', 'kind' => 'badge'], ['name' => 'effective_at', 'label' => '生效时间', 'kind' => 'date'], ['name' => 'invalid_at', 'label' => '失效时间', 'kind' => 'date'], ['name' => 'period_sales', 'label' => '期间销量', 'kind' => 'number'], @@ -290,7 +319,11 @@ $navigation = [ 'empty' => '内部流转数据尚未导入。导入工作簿后这里会显示明细。', 'fields' => [ ['name' => 'company_name', 'label' => '抬头'], - ['name' => 'tag', 'label' => '标签'], + ['name' => 'tag', 'label' => '标签', 'kind' => 'badge', 'tones' => [ + '1' => 'accent', + '2' => 'warning', + '3' => 'info', + ]], ['name' => 'projected_sales', 'label' => '预计9月销量', 'kind' => 'number'], ['name' => 'invoiced_to_buyer', 'label' => '开票-给买家开票', 'kind' => 'number'], ['name' => 'internal_invoiced', 'label' => '开票-内部开票', 'kind' => 'number'], @@ -316,11 +349,11 @@ $navigation = [ 'description' => '优先读取共享业务镜像中的店铺资料、店铺 ID 和续签时间;提现账户仅显示配置状态。', 'empty' => '尚未读取到店铺基础资料。请检查共享镜像或上游同步状态。', 'fields' => [ - ['name' => 'platform', 'label' => '平台'], + ['name' => 'platform', 'label' => '平台', 'kind' => 'badge'], ['name' => 'store_id', 'label' => '店铺ID'], ['name' => 'store_name', 'label' => '店铺名称', 'source' => 'erp_store_name'], ['name' => 'store_entity', 'label' => '店铺主体', 'source' => 'company_name'], - ['name' => 'store_status', 'label' => '店铺状态'], + ['name' => 'store_status', 'label' => '店铺状态', 'kind' => 'badge'], ['name' => 'trademark', 'label' => '商标', 'source' => 'trademark_name'], ['name' => 'trademark_number', 'label' => '商标号'], ['name' => 'business_department', 'label' => '业务部门', 'source' => 'profit_center'], @@ -346,16 +379,32 @@ $navigation = [ ['name' => 'operation_department_head', 'label' => '店铺运营部门负责人'], ['name' => 'operation_director', 'label' => '店铺运营总监'], ['name' => 'profit_center', 'label' => '店铺所属利润中心'], - ['name' => 'account_type', 'label' => '账号类型'], + ['name' => 'account_type', 'label' => '账号类型', 'kind' => 'badge', 'tones' => [ + '主账号' => 'accent', + '子账号' => 'info', + ]], ['name' => 'account', 'label' => '账号'], ['name' => 'password_configured', 'label' => '密码', 'kind' => 'secret_status'], ['name' => 'bound_phone', 'label' => '绑定手机号'], ['name' => 'verification_contact', 'label' => '手机号验证码对接人'], ['name' => 'account_group_name', 'label' => '账号分组名称'], - ['name' => 'authorization_status', 'label' => '授权状态'], - ['name' => 'store_status', 'label' => '店铺状态'], - ['name' => 'flow_type', 'label' => '流程类型'], - ['name' => 'approval_status', 'label' => '审批状态'], + ['name' => 'authorization_status', 'label' => '授权状态', 'kind' => 'badge', 'tones' => [ + '已授权' => 'success', + '已过期' => 'warning', + '未授权' => 'danger', + ]], + ['name' => 'store_status', 'label' => '店铺状态', 'kind' => 'badge'], + ['name' => 'flow_type', 'label' => '流程类型', 'kind' => 'badge'], + ['name' => 'approval_status', 'label' => '审批状态', 'kind' => 'badge', 'tones' => [ + '完成' => 'success', + '已签署' => 'success', + '拒绝' => 'danger', + '撤回' => 'muted', + '流程中' => 'info', + '待审批' => 'warning', + '暂存' => 'warning', + '待签署' => 'warning', + ]], ['name' => 'created_at', 'label' => '店铺创建时间', 'kind' => 'date'], ['name' => 'updated_at', 'label' => '店铺修改时间', 'kind' => 'date'], ], diff --git a/modules/EcommerceFinance/Repositories/BusinessInformationMirrorRepository.php b/modules/EcommerceFinance/Repositories/BusinessInformationMirrorRepository.php index a1145da4..aa5b7217 100644 --- a/modules/EcommerceFinance/Repositories/BusinessInformationMirrorRepository.php +++ b/modules/EcommerceFinance/Repositories/BusinessInformationMirrorRepository.php @@ -175,7 +175,7 @@ SQL) ->all(); } - /** @return array */ + /** @return array */ public function companyClassificationsByNames(array $names): array { $names = array_values(array_unique(array_filter(array_map('trim', $names)))); @@ -184,13 +184,15 @@ SQL) } return $this->connection->table('fin_companies') - ->select(['name', 'tax_type', 'business_type']) + ->select(['name', 'status', 'tax_type', 'business_type']) ->whereIn('name', $names) ->get() ->mapWithKeys(static function (object $row): array { $businessType = trim((string) ($row->business_type ?? '')); + $status = trim((string) ($row->status ?? '')); return [trim((string) $row->name) => [ + 'status' => $status !== '' ? $status : null, 'tax_type' => trim((string) ($row->tax_type ?? '')) ?: null, 'business_registration_type' => in_array($businessType, ['', '/'], true) ? null : $businessType, ]]; diff --git a/modules/EcommerceFinance/Repositories/InternalFlowReportRepository.php b/modules/EcommerceFinance/Repositories/InternalFlowReportRepository.php index 3435f0f1..e8668bef 100644 --- a/modules/EcommerceFinance/Repositories/InternalFlowReportRepository.php +++ b/modules/EcommerceFinance/Repositories/InternalFlowReportRepository.php @@ -47,12 +47,22 @@ class InternalFlowReportRepository /** * @param array{company_name?:string,tag?:string} $filters - * @return array{data: list>, total: int} + * @return array{data: list>, total: int, summary: array} */ public function paginate(int $page, int $perPage, array $filters = [], ?string $sort = null, string $direction = 'asc'): array { if (! $this->ready()) { - return ['data' => [], 'total' => 0]; + return [ + 'data' => [], + 'total' => 0, + 'summary' => [ + 'companies' => 0, + 'projected_sales' => 0.0, + 'receipt_total' => 0.0, + 'remaining_gap' => 0.0, + 'average_invoicing_ratio' => 0.0, + ], + ]; } $query = $this->connection->table(self::TABLE); @@ -68,6 +78,30 @@ class InternalFlowReportRepository } $total = (clone $query)->count(); + $totals = (clone $query)->selectRaw( + 'COUNT(*) AS companies, ' + . 'COALESCE(SUM(projected_sales), 0) AS projected_sales, ' + . 'COALESCE(SUM(receipt_total), 0) AS receipt_total, ' + . 'COALESCE(SUM(remaining_gap), 0) AS remaining_gap', + )->first(); + $ratios = (clone $query) + ->whereNotNull('invoicing_ratio') + ->pluck('invoicing_ratio') + ->map(static function (mixed $value): ?float { + $text = str_replace([',', ' ', '%'], '', trim((string) $value)); + if ($text === '' || $text === '/' || ! is_numeric($text)) { + return null; + } + + $ratio = (float) $text; + if (str_contains((string) $value, '%') || $ratio > 1) { + $ratio /= 100; + } + + return $ratio >= 0 ? $ratio : null; + }) + ->filter(static fn (?float $value): bool => $value !== null) + ->values(); $column = in_array($sort, self::SORTABLE, true) ? $sort : null; $direction = strtolower($direction) === 'desc' ? 'desc' : 'asc'; @@ -84,6 +118,16 @@ class InternalFlowReportRepository ->map(static fn (object $row): array => (array) $row) ->all(); - return ['data' => $rows, 'total' => $total]; + return [ + 'data' => $rows, + 'total' => $total, + 'summary' => [ + 'companies' => (int) ($totals->companies ?? $total), + 'projected_sales' => round((float) ($totals->projected_sales ?? 0), 2), + 'receipt_total' => round((float) ($totals->receipt_total ?? 0), 2), + 'remaining_gap' => round((float) ($totals->remaining_gap ?? 0), 2), + 'average_invoicing_ratio' => round((float) ($ratios->avg() ?? 0) * 100, 2), + ], + ]; } } diff --git a/resources/views/business/pages/partials/business-summary-cards.blade.php b/resources/views/business/pages/partials/business-summary-cards.blade.php index 7f8fe31a..a714bcb3 100644 --- a/resources/views/business/pages/partials/business-summary-cards.blade.php +++ b/resources/views/business/pages/partials/business-summary-cards.blade.php @@ -32,6 +32,49 @@ background: #fbf7ec; } + .business-summary-card[data-tone="accent"] { + border-color: #cdb985; + background: #fbf7ec; + } + + .business-summary-card[data-tone="success"] { + border-color: #b9c9ad; + background: #f3f8ef; + } + + .business-summary-card[data-tone="warning"] { + border-color: #ddc79e; + background: #fff8ea; + } + + .business-summary-card[data-tone="danger"] { + border-color: #dfbbbb; + background: #fff4f4; + } + + .business-summary-card[data-tone]::before { + position: absolute; + top: 0.7rem; + bottom: 0.7rem; + left: 0; + width: 3px; + border-radius: 0 999px 999px 0; + background: var(--vip-content-accent-dark, #856632); + content: ""; + } + + .business-summary-card[data-tone="success"]::before { + background: #53793e; + } + + .business-summary-card[data-tone="warning"]::before { + background: #935f07; + } + + .business-summary-card[data-tone="danger"]::before { + background: #b80000; + } + .business-summary-card:hover, .business-summary-card:focus-visible, .business-summary-card[aria-current="true"] { @@ -107,23 +150,28 @@ $format = $card['format'] ?? 'integer'; $formattedValue = match ($format) { 'money' => '¥' . number_format((float) ($card['value'] ?? 0), 2), + 'amount' => number_format((float) ($card['value'] ?? 0), 2), + 'percent' => number_format((float) ($card['value'] ?? 0), 2) . '%', 'text' => (string) ($card['value'] ?? '—'), default => number_format((int) ($card['value'] ?? 0)), }; + $tone = (string) ($card['tone'] ?? 'neutral'); + $isAmount = in_array($format, ['money', 'amount'], true); $activeQuery = collect($query) ->filter(fn ($value, string $key): bool => $key !== 'page' && $value !== null && $value !== ''); $isActive = $activeQuery->isNotEmpty() && $activeQuery->every(fn ($value, string $key): bool => (string) request($key) === (string) $value); @endphp {{ $card['label'] ?? '' }} - {{ $formattedValue }} + {{ $formattedValue }} {{ $card['hint'] ?? '' }} diff --git a/resources/views/business/pages/partials/directory-table.blade.php b/resources/views/business/pages/partials/directory-table.blade.php index c744a5c9..304c64d7 100644 --- a/resources/views/business/pages/partials/directory-table.blade.php +++ b/resources/views/business/pages/partials/directory-table.blade.php @@ -584,6 +584,61 @@ text-align: right; } + .directory-value-badge { + display: inline-flex; + max-width: 12rem; + min-height: 1.45rem; + align-items: center; + justify-content: center; + padding: 0.15rem 0.48rem; + border: 1px solid #d8d2c7; + border-radius: 999px; + background: #f4f1eb; + color: #595959; + font-size: 0.6875rem; + line-height: 1.2; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: middle; + white-space: nowrap; + } + + .directory-value-badge[data-tone="accent"] { + border-color: #dcc99d; + background: #f7f0de; + color: #6a5129; + } + + .directory-value-badge[data-tone="success"] { + border-color: #bfd1b3; + background: #f1f7ed; + color: #53793e; + } + + .directory-value-badge[data-tone="warning"] { + border-color: #e1ca9c; + background: #fff7e7; + color: #935f07; + } + + .directory-value-badge[data-tone="danger"] { + border-color: #e5bcbc; + background: #fff1f1; + color: #b80000; + } + + .directory-value-badge[data-tone="info"] { + border-color: #b7ceda; + background: #eef5f8; + color: #006395; + } + + .directory-value-badge[data-tone="muted"] { + border-color: #ddd7ce; + background: #f3f1ed; + color: #68635c; + } + /* 快递运费:这几列是从来源表/快递应付带出来的金额与计算列,按业务要求表头和数值都靠左展示。 */ .directory-table-page .directory-table th.is-number[data-column-key="freight_amount"], .directory-table-page .directory-table td.is-number[data-column-key="freight_amount"], @@ -1367,6 +1422,12 @@ > @if ($directoryUsesFieldQuery) {{ $directory['status'] ?? '尚未同步' }} +
+
+ {{ $page['title'] ?? $section['title'] }} +

{{ $directoryQueryPageDescriptions[$directoryPageKey] ?? '按当前字段查询、筛选并导出业务数据' }}

+
+
@if (!empty($upstreamError))