CaiWuXiTong/resources/views/business/pages/partials/directory-table.blade.php
chenxi 26556a9169 Add warehouse short names to inventory report
Restore warehouse name mapping ledger and inventory short-name column.
2026-09-28 16:33:38 +08:00

2137 lines
125 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

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

@php
$directoryRows = $rows ?? collect();
$directoryFields = $directory['fields'] ?? [];
$isInventoryReportPage = ($page['slug'] ?? '') === 'inventory-report';
$inventoryWarehouseMappings = $isInventoryReportPage
? ($inventoryWarehouseMappings ?? collect())
: collect();
$warehouseMappingPreview = $isInventoryReportPage ? session('warehouse_mapping_preview') : null;
// “最近三个月销量”必须是三个独立的月份列;月份按当前月向前取三个完整月份。
// 保留原字段名作为兼容契约,渲染时展开为 *_1、*_2、*_3 三个值。
$directoryFields = collect($directoryFields)->flatMap(function (array $field): array {
if (($field['name'] ?? null) !== 'recent_three_month_sales') {
return [$field];
}
return collect(range(3, 1))->map(function (int $monthsAgo): array {
$month = now()->startOfMonth()->subMonthsNoOverflow($monthsAgo);
return [
'name' => 'recent_three_month_sales_' . (4 - $monthsAgo),
'label' => $month->format('n月') . '销量',
'kind' => 'number',
];
})->all();
})->values()->all();
$directoryIsPaginated = is_object($directoryRows) && method_exists($directoryRows, 'total');
$directoryTotal = $directoryIsPaginated ? $directoryRows->total() : (is_countable($directoryRows) ? count($directoryRows) : 0);
$directoryCurrentPage = $directoryIsPaginated ? $directoryRows->currentPage() : 1;
$directoryLastPage = $directoryIsPaginated ? $directoryRows->lastPage() : 1;
$directoryPerPage = $directoryIsPaginated ? $directoryRows->perPage() : 0;
// 页面配置在部分导航上下文中会省略父级 slug,优先使用当前路由参数避免功能开关失配。
$directoryRouteSection = request()->route('section') ?: request()->segment(3) ?: ($section['slug'] ?? '');
$directoryRoutePage = request()->route('page') ?: request()->segment(4) ?: ($page['slug'] ?? '');
$directoryPageKey = (string) $directoryRouteSection . '/' . (string) $directoryRoutePage;
$directoryUsesFieldQuery = in_array($directoryPageKey, [
'company-management/basic-information',
'store-management/basic-information',
'trademark-management/information',
'store-management/store-accounts',
'store-management/store-report',
'company-management/subject-operating-report',
'company-management/company-cost-arrangement',
'company-management/personnel-inventory',
], true);
$directoryQueryPageDescriptions = [
'company-management/basic-information' => '公司主体、状态和登记资料的查询与导出',
'store-management/basic-information' => '店铺主体、平台和经营资料的查询与导出',
'trademark-management/information' => '商标主体、状态和登记资料的查询与导出',
'store-management/store-accounts' => '店铺账号资料的查询与安全状态导出',
'store-management/store-report' => '按店铺、主体和月份销量查询经营数据并导出',
'company-management/subject-operating-report' => '按主体和收入月份查询经营汇总并导出',
'company-management/company-cost-arrangement' => '按公司类型和发票类型查询成本安排字段',
'company-management/personnel-inventory' => '按人员、职位和公司类型查询人员盘点字段',
];
@endphp
<style>
.directory-table-page {
margin-top: 1.5rem;
overflow: hidden;
border: 1px solid var(--vip-border, #d8d2c7);
border-radius: 0.75rem;
background: var(--vip-surface, #ffffff);
color: var(--vip-content-ink, #171717);
}
.directory-table-page.has-field-query {
margin-top: 1.5rem;
overflow: visible;
border: 0;
border-radius: 0;
background: transparent;
}
.directory-query-page-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
margin-bottom: 1.25rem;
}
.directory-query-page-head strong {
display: block;
color: var(--vip-content-ink, #171717);
font-size: 0.875rem;
font-weight: 600;
line-height: 1.25;
}
.directory-query-page-head p {
margin: 0.4rem 0 0;
color: var(--vip-content-muted, #595959);
font-size: 0.75rem;
line-height: 1.5;
}
.directory-table-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
padding: 1rem 1.25rem;
border-bottom: 1px solid var(--vip-border, #d8d2c7);
}
.directory-table-title {
display: flex;
min-width: 0;
align-items: flex-start;
gap: 0.625rem;
}
.directory-table-title .material-icons-outlined {
flex: 0 0 auto;
margin-top: 0.05rem;
color: var(--vip-content-accent-dark, #856632);
font-size: 1.25rem;
}
.directory-table-title strong {
display: block;
font-size: 0.875rem;
font-weight: 600;
line-height: 1.35;
}
.directory-table-description {
max-width: 72rem;
margin-top: 0.3rem;
color: var(--vip-content-muted, #595959);
font-size: 0.75rem;
line-height: 1.55;
}
.directory-table-status {
flex: 0 0 auto;
padding: 0.25rem 0.55rem;
border: 1px solid var(--vip-border, #d8d2c7);
border-radius: 999px;
background: var(--vip-content-accent-soft, #f7f0de);
color: var(--vip-content-accent-dark, #856632);
font-size: 0.75rem;
line-height: 1.2;
white-space: nowrap;
}
.directory-table-notice {
display: flex;
align-items: flex-start;
gap: 0.5rem;
margin: 1rem 1.25rem;
padding: 0.7rem 0.8rem;
border: 1px solid var(--vip-border, #d8d2c7);
border-radius: 0.5rem;
background: #fbfaf7;
color: var(--vip-content-muted, #595959);
font-size: 0.75rem;
line-height: 1.5;
}
.directory-table-notice .material-icons-outlined {
flex: 0 0 auto;
color: var(--vip-content-accent-dark, #856632);
font-size: 1.1rem;
}
.directory-table-tools {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
align-items: center;
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--vip-border, #d8d2c7);
background: var(--vip-content-bg, #f8f6f1);
}
.directory-query-bar {
display: grid;
grid-template-columns: repeat(4, minmax(12rem, 1fr));
gap: 1rem 1.25rem;
margin: 0;
padding: 1.25rem;
border: 1px solid var(--vip-border, #d8d2c7);
border-radius: 0.5rem;
border-bottom: 1px solid var(--vip-border, #d8d2c7);
background: #fff;
}
.directory-query-heading { display: flex; grid-column: 1 / -1; align-items: center; justify-content: space-between; gap: 1rem; padding-bottom: 0.125rem; color: var(--vip-content-accent-dark, #856632); }
.directory-query-heading strong { font-size: 0.8125rem; }
.directory-query-heading span { color: var(--vip-content-muted, #595959); font-size: 0.6875rem; font-weight: 400; }
.directory-query-field { position: relative; display: flex; min-width: 0; flex-direction: column; align-items: stretch; gap: 0.45rem; color: var(--vip-content-muted, #595959); font-size: 0.75rem; }
.directory-query-field > span:first-child { overflow: hidden; color: var(--vip-content-ink, #171717); font-weight: 500; text-overflow: ellipsis; white-space: nowrap; }
.directory-query-field input, .directory-query-field select { width: 100%; min-width: 0; height: 2.125rem; padding: 0 0.625rem; border: 1px solid var(--vip-border, #d8d2c7); border-radius: 0.375rem; background: #fff; color: var(--vip-content-ink, #171717); font-size: 0.75rem; }
.directory-query-field input:focus, .directory-query-field select:focus { border-color: var(--vip-content-accent, #b9954f); outline: 2px solid rgba(185, 149, 79, 0.16); }
.directory-query-dates { display: grid; flex: 1 1 auto; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); align-items: center; gap: 0.25rem; width: 100%; min-width: 0; }
.directory-query-dates input { min-width: 0; }
.directory-query-more { grid-column: 1 / -1; margin-top: 0.25rem; border-top: 1px solid #e8e3da; }
.directory-query-more > summary { width: fit-content; padding-top: 0.75rem; color: var(--vip-content-accent-dark, #856632); cursor: pointer; list-style: disclosure-closed; }
.directory-query-more[open] > summary { list-style: disclosure-open; }
.directory-query-more__fields { display: grid; grid-template-columns: repeat(4, minmax(12rem, 1fr)); gap: 1rem 1.25rem; margin-top: 1rem; }
.directory-query-suggestions { position: absolute; top: calc(100% + 0.25rem); right: 0; left: 0; z-index: 6; margin: 0; padding: 0.25rem; border: 1px solid var(--vip-border, #d8d2c7); border-radius: 0.375rem; background: #fff; box-shadow: 0 5px 12px rgba(23,23,23,0.12); list-style: none; }
.directory-query-suggestions[hidden] { display: none; }
.directory-query-suggestions button { display: block; width: 100%; overflow: hidden; padding: 0.4rem 0.5rem; border: 0; border-radius: 0.25rem; background: transparent; color: var(--vip-content-ink, #171717); font: inherit; text-align: left; text-overflow: ellipsis; white-space: nowrap; cursor: pointer; }
.directory-query-suggestions button:hover, .directory-query-suggestions button:focus { background: var(--vip-content-accent-soft, #f7f0de); outline: 0; }
.directory-query-actions { display: flex; grid-column: 1 / -1; flex-wrap: wrap; align-items: flex-start; justify-content: flex-end; gap: 0.5rem; padding-top: 0.125rem; }
.directory-query-actions button { height: 2.1rem; padding: 0 0.8rem; border: 1px solid var(--vip-content-accent, #b9954f); border-radius: 0.375rem; background: #fff; color: var(--vip-content-accent-dark, #856632); cursor: pointer; }
.directory-query-actions .is-primary { background: var(--vip-content-accent-dark, #856632); color: #fff; }
.directory-query-picker, .directory-query-export { position: relative; }
.directory-query-picker summary, .directory-query-export summary { display: flex; height: 2.1rem; align-items: center; padding: 0 0.8rem; border: 1px solid var(--vip-content-accent, #b9954f); border-radius: 0.375rem; background: #fff; color: var(--vip-content-accent-dark, #856632); cursor: pointer; list-style: none; }
.directory-query-export summary { gap: 0.35rem; }
.directory-query-export summary .material-icons-outlined { font-size: 1rem; }
.directory-query-picker summary::-webkit-details-marker, .directory-query-export summary::-webkit-details-marker { display: none; }
.directory-query-picker__panel, .directory-query-export__panel { position: absolute; top: calc(100% + 0.35rem); right: 0; z-index: 8; display: grid; grid-template-columns: repeat(2, minmax(10rem, 1fr)); gap: 0.35rem 0.75rem; width: max-content; min-width: 24rem; max-width: min(34rem, calc(100vw - 2rem)); max-height: 22rem; overflow: auto; padding: 0.75rem; border: 1px solid var(--vip-border, #d8d2c7); border-radius: 0.5rem; background: #fff; box-shadow: 0 4px 8px rgba(23, 23, 23, 0.10); }
.directory-query-picker__hint { grid-column: 1 / -1; color: var(--vip-content-muted, #595959); font-size: 0.6875rem; }
.directory-query-picker__option-form { min-width: 0; }
.directory-query-picker__option { display: flex; align-items: center; gap: 0.4rem; width: 100%; min-width: 0; padding: 0.25rem; border: 0; border-radius: 0.25rem; background: transparent; color: var(--vip-content-ink, #171717); font: inherit; font-size: 0.75rem; text-align: left; cursor: pointer; }
.directory-query-picker__option:hover, .directory-query-picker__option:focus { background: var(--vip-content-accent-soft, #f7f0de); outline: 0; }
.directory-query-picker__option input { flex: 0 0 auto; width: 1rem; height: 1rem; margin: 0; accent-color: var(--vip-content-accent, #b9954f); }
.directory-query-export__option { display: flex; align-items: center; gap: 0.4rem; min-width: 0; color: var(--vip-content-ink, #171717); font-size: 0.75rem; cursor: pointer; }
.directory-query-export__option input { flex: 0 0 auto; width: 1rem; height: 1rem; margin: 0; accent-color: var(--vip-content-accent, #b9954f); }
.directory-query-export__button { grid-column: 1 / -1; justify-self: end; }
@media (max-width: 1100px) { .directory-query-bar { grid-template-columns: repeat(2, minmax(14rem, 1fr)); } }
@media (max-width: 1100px) { .directory-query-more__fields { grid-template-columns: repeat(2, minmax(14rem, 1fr)); } }
@media (max-width: 650px) {
.directory-query-page-head { margin-bottom: 1rem; }
.directory-query-bar { grid-template-columns: minmax(0, 1fr); padding: 1rem; }
.directory-query-more__fields { grid-template-columns: minmax(0, 1fr); }
.directory-query-heading { align-items: flex-start; flex-direction: column; gap: 0.3rem; }
.directory-query-actions { justify-content: flex-start; }
.directory-query-actions > * { flex: 1 1 calc(50% - 0.25rem); }
.directory-query-actions button,
.directory-query-picker summary,
.directory-query-export summary { justify-content: center; width: 100%; }
.directory-query-picker__panel, .directory-query-export__panel { right: auto; left: 0; grid-template-columns: minmax(0, 1fr); width: min(18rem, calc(100vw - 2rem)); min-width: 0; }
}
.directory-table-tools > [data-directory-search] {
flex: 1 1 16rem;
min-width: 12rem;
height: 2.25rem;
padding: 0 0.75rem;
border: 1px solid var(--vip-border, #d8d2c7);
border-radius: 0.25rem;
background: #fff;
color: var(--vip-content-ink, #171717);
}
.directory-table-tools button {
height: 2.25rem;
padding: 0 0.875rem;
border: 1px solid var(--vip-content-accent, #b9954f);
border-radius: 0.25rem;
background: #fff;
color: var(--vip-content-accent-dark, #856632);
cursor: pointer;
}
.directory-table-tools button:disabled {
cursor: wait;
opacity: 0.65;
}
.directory-table-tools select {
height: 2.25rem;
padding: 0 0.5rem;
border: 1px solid var(--vip-border, #d8d2c7);
border-radius: 0.25rem;
background: #fff;
color: var(--vip-content-ink, #171717);
}
.inventory-month-picker {
position: relative;
flex: 1 1 auto;
min-width: 0;
}
.inventory-month-picker > input[type="hidden"] { display: none; }
.inventory-month-picker__trigger {
display: flex;
width: 100%;
height: 2.1rem;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
padding: 0 0.6rem 0 0.7rem;
border: 1px solid var(--vip-border, #d8d2c7);
border-radius: 0.375rem;
background: var(--vip-surface, #fff);
color: var(--vip-content-ink, #171717);
font: inherit;
font-size: 0.75rem;
text-align: left;
cursor: pointer;
}
.inventory-month-picker__trigger:hover { border-color: var(--vip-content-accent, #b9954f); }
.inventory-month-picker__trigger:focus-visible {
border-color: var(--vip-content-accent-dark, #856632);
outline: 2px solid rgba(133, 102, 50, 0.2);
outline-offset: 1px;
}
.inventory-month-picker__trigger [data-inventory-month-label]:empty::before { content: '选择月份'; color: var(--vip-content-muted, #595959); }
.inventory-month-picker__trigger [data-inventory-month-label] { flex: 1 1 auto; min-width: 0; }
.inventory-month-picker__trigger .material-icons-outlined { flex: 0 0 auto; color: var(--vip-content-accent-dark, #856632); font-size: 1.05rem; }
.inventory-month-picker__panel {
position: absolute;
top: calc(100% + 0.4rem);
left: 0;
z-index: 12;
width: min(18rem, calc(100vw - 2rem));
padding: 0.8rem;
border: 1px solid var(--vip-border, #d8d2c7);
border-radius: 0.5rem;
background: var(--vip-surface, #fff);
box-shadow: 0 4px 8px rgba(23, 23, 23, 0.1);
}
.inventory-month-picker__panel[hidden] { display: none; }
.inventory-month-picker__yearbar,
.inventory-month-picker__footer { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; }
.inventory-month-picker__yearbar { padding-bottom: 0.65rem; border-bottom: 1px solid #eeeae3; }
.inventory-month-picker__yearbar strong { color: var(--vip-content-ink, #171717); font-size: 0.8125rem; font-weight: 600; }
.inventory-month-picker__nav,
.inventory-month-picker__footer button {
height: 1.9rem;
border: 1px solid transparent;
border-radius: 0.3rem;
background: transparent;
color: var(--vip-content-accent-dark, #856632);
font: inherit;
cursor: pointer;
}
.inventory-month-picker__nav { display: inline-flex; width: 1.9rem; align-items: center; justify-content: center; }
.inventory-month-picker__nav:hover,
.inventory-month-picker__nav:focus-visible,
.inventory-month-picker__footer button:hover,
.inventory-month-picker__footer button:focus-visible { border-color: var(--vip-border, #d8d2c7); background: var(--vip-content-accent-soft, #f7f0de); outline: 0; }
.inventory-month-picker__nav .material-icons-outlined { font-size: 1rem; }
.inventory-month-picker__months { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0.35rem; padding: 0.75rem 0; }
.inventory-month-picker__month {
height: 2.15rem;
border: 1px solid transparent;
border-radius: 0.3rem;
background: transparent;
color: var(--vip-content-ink, #171717);
font: inherit;
font-size: 0.75rem;
cursor: pointer;
}
.inventory-month-picker__month:hover,
.inventory-month-picker__month:focus-visible { border-color: var(--vip-content-accent, #b9954f); background: var(--vip-content-accent-soft, #f7f0de); outline: 0; }
.inventory-month-picker__month.is-selected { border-color: var(--vip-content-accent-dark, #856632); background: var(--vip-content-accent-dark, #856632); color: #fff; }
.inventory-month-picker__footer { padding-top: 0.65rem; border-top: 1px solid #eeeae3; }
.inventory-month-picker__footer button { padding: 0 0.55rem; }
.inventory-month-picker__footer button:last-child { border-color: var(--vip-content-accent, #b9954f); background: var(--vip-content-accent-soft, #f7f0de); }
.directory-field-picker {
position: relative;
display: inline-block;
}
.directory-field-picker__panel {
position: absolute;
top: calc(100% + 0.35rem);
right: 0;
z-index: 5;
display: grid;
grid-template-columns: repeat(2, minmax(10rem, 1fr));
gap: 0.35rem 0.75rem;
min-width: 24rem;
max-height: 22rem;
overflow: auto;
padding: 0.75rem;
border: 1px solid var(--vip-border, #d8d2c7);
border-radius: 0.5rem;
background: var(--vip-surface, #fff);
box-shadow: 0 4px 8px rgba(23, 23, 23, 0.1);
}
.directory-field-picker__panel[hidden] { display: none; }
.directory-field-picker__option {
display: flex;
align-items: center;
gap: 0.4rem;
min-width: 0;
color: var(--vip-content-ink, #171717);
font-size: 0.75rem;
line-height: 1.35;
cursor: pointer;
}
.directory-field-picker__option[draggable="true"] { cursor: grab; }
.directory-field-picker__option.is-dragging { opacity: 0.55; }
.directory-field-picker__option.is-drop-target { outline: 1px solid var(--vip-content-accent, #b9954f); background: var(--vip-content-accent-soft, #f7f0de); }
.directory-field-picker__hint { grid-column: 1 / -1; color: var(--vip-content-muted, #595959); font-size: 0.6875rem; }
.directory-table-tools .directory-export-group { display: flex; flex-wrap: wrap; gap: 0.35rem; }
.directory-field-picker__option input[type="checkbox"] {
flex: 0 0 auto;
width: 1rem;
min-width: 1rem;
height: 1rem;
margin: 0;
accent-color: var(--vip-content-accent, #b9954f);
}
.directory-field-picker__option span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.directory-field-picker__option label { display: block; flex: 1 1 auto; min-width: 0; cursor: pointer; }
.directory-table [data-column-key="sales_01"],
.directory-table [data-column-key="sales_02"],
.directory-table [data-column-key="sales_03"],
.directory-table [data-column-key="sales_04"],
.directory-table [data-column-key="sales_05"],
.directory-table [data-column-key="sales_06"] { display: none; }
.directory-table[data-columns-ready] [data-column-key="sales_01"],
.directory-table[data-columns-ready] [data-column-key="sales_02"],
.directory-table[data-columns-ready] [data-column-key="sales_03"],
.directory-table[data-columns-ready] [data-column-key="sales_04"],
.directory-table[data-columns-ready] [data-column-key="sales_05"],
.directory-table[data-columns-ready] [data-column-key="sales_06"] { display: table-cell; }
.directory-table[data-columns-ready] [data-column-key].is-column-hidden { display: none; }
.directory-table-scroll {
max-height: calc(100vh - 20rem);
overflow: auto;
scrollbar-color: var(--vip-content-accent, #b9954f) #efede8;
scrollbar-width: thin;
}
.directory-table-page.has-field-query .directory-table-scroll {
margin-top: 1.25rem;
border: 1px solid var(--vip-border, #d8d2c7);
background: var(--vip-surface, #fff);
}
.directory-table-page.has-field-query .directory-table-pagination {
border: 1px solid var(--vip-border, #d8d2c7);
border-top: 0;
background: var(--vip-surface, #fff);
}
.directory-table {
width: 100%;
min-width: 68rem;
border-collapse: collapse;
font-size: 0.78125rem;
font-variant-numeric: tabular-nums;
}
.directory-table th,
.directory-table td {
min-width: 8rem;
height: 2.5rem;
padding: 0.5rem 0.625rem;
border-right: 1px solid var(--vip-border, #d8d2c7);
border-bottom: 1px solid var(--vip-border, #d8d2c7);
text-align: left;
vertical-align: middle;
white-space: nowrap;
}
.directory-table th:first-child,
.directory-table td:first-child {
min-width: 3rem;
text-align: center;
}
.directory-table th:last-child,
.directory-table td:last-child {
border-right: 0;
}
.directory-table thead th {
position: sticky;
top: 0;
z-index: 2;
background: var(--vip-content-accent-soft, #f7f0de);
color: var(--vip-content-ink, #171717);
font-size: 0.75rem;
font-weight: 600;
box-shadow: 0 1px 0 var(--vip-border, #d8d2c7);
}
.directory-sort-link { display: inline-flex; align-items: center; gap: 0.25rem; color: inherit; text-decoration: none; }
.directory-sort-link:hover { color: var(--vip-content-accent-dark, #856632); }
.directory-sort-icon { width: 1rem; height: 1rem; color: var(--vip-content-muted, #595959); font-size: 1rem; line-height: 1; }
.directory-sort-link[aria-current="true"] .directory-sort-icon { color: var(--vip-content-accent-dark, #856632); }
.directory-inventory-sort { display: inline-flex; align-items: center; gap: 0.25rem; padding: 0; border: 0; background: transparent; color: inherit; font: inherit; cursor: pointer; }
.directory-inventory-sort:hover, .directory-inventory-sort:focus-visible { color: var(--vip-content-accent-dark, #856632); }
.directory-header-menu { position: relative; display: inline-flex; align-items: center; gap: 0.15rem; }
.directory-header-menu summary { display: inline-flex; align-items: center; cursor: pointer; list-style: none; }
.directory-header-menu summary::-webkit-details-marker { display: none; }
.directory-header-menu__panel { position: absolute; top: calc(100% + 0.35rem); left: 0; z-index: 9; display: grid; min-width: 8rem; gap: 0.2rem; padding: 0.35rem; border: 1px solid var(--vip-border, #d8d2c7); border-radius: 0.375rem; background: #fff; box-shadow: 0 4px 8px rgba(23, 23, 23, 0.10); }
.directory-header-menu__panel a, .directory-header-menu__panel button { display: block; width: 100%; padding: 0.35rem 0.5rem; border: 0; border-radius: 0.25rem; background: transparent; color: var(--vip-content-ink, #171717); font: inherit; font-size: 0.6875rem; text-align: left; text-decoration: none; cursor: pointer; white-space: nowrap; }
.directory-header-menu__panel a:hover, .directory-header-menu__panel button:hover, .directory-header-menu__panel a:focus, .directory-header-menu__panel button:focus { background: var(--vip-content-accent-soft, #f7f0de); outline: 0; }
.directory-table tbody tr:hover {
background: #fbf8f0;
}
.directory-table tbody tr[data-subject-row] { cursor: pointer; }
.directory-table tbody tr[data-subject-row][aria-selected="true"] { background: var(--vip-content-accent-soft, #f7f0de); }
.directory-subject-details {
margin: 1rem 1.25rem 1.25rem;
border: 1px solid var(--vip-border, #d8d2c7);
background: var(--vip-surface, #fff);
}
.directory-subject-details__head {
display: flex; justify-content: space-between; gap: 1rem; align-items: center;
padding: 0.75rem 1rem; border-bottom: 1px solid var(--vip-border, #d8d2c7);
color: var(--vip-content-ink, #171717); font-size: 0.8125rem;
}
.directory-subject-details__head span { color: var(--vip-content-muted, #595959); font-size: 0.75rem; }
.directory-subject-details__body { padding: 0.75rem 1rem; overflow-x: auto; }
.directory-subject-details__record { display: grid; grid-template-columns: repeat(3, minmax(12rem, 1fr)); gap: 0.75rem 1rem; margin: 0; }
.directory-subject-details__record + .directory-subject-details__record { margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--vip-border, #d8d2c7); }
.directory-subject-details__record dt { color: var(--vip-content-muted, #595959); font-size: 0.6875rem; }
.directory-subject-details__record dd { margin: 0.2rem 0 0; color: var(--vip-content-ink, #171717); font-size: 0.75rem; }
@media (max-width: 800px) { .directory-subject-details__record { grid-template-columns: repeat(2, minmax(12rem, 1fr)); } }
@media (max-width: 520px) { .directory-subject-details__record { grid-template-columns: minmax(12rem, 1fr); } }
.directory-table .is-number {
text-align: right;
}
.directory-table .is-empty td {
height: 8rem;
color: var(--vip-content-muted, #595959);
text-align: center;
white-space: normal;
}
.directory-table-empty {
display: flex;
min-height: 8rem;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 1.5rem;
color: var(--vip-content-muted, #595959);
text-align: center;
}
.directory-table-empty .material-icons-outlined {
color: var(--vip-content-accent, #b9954f);
font-size: 1.5rem;
}
.directory-table-pagination {
display: flex;
min-height: 3rem;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0.625rem 1.25rem;
color: var(--vip-content-muted, #595959);
font-size: 0.75rem;
}
.directory-table-pagination a {
color: var(--vip-content-accent-dark, #856632);
text-decoration: none;
}
.directory-table-pagination a:hover {
text-decoration: underline;
}
.inventory-report-tabs {
min-height: 5rem;
margin: 0;
padding: 0 2rem;
gap: 2rem;
border-color: var(--vip-border, #d8d2c7);
border-radius: 1rem;
box-shadow: none;
}
.inventory-report-tabs [data-inventory-report-tab] {
position: relative;
display: inline-flex;
min-height: 5rem;
align-items: center;
padding: 0;
border: 0;
background: transparent;
color: var(--vip-content-muted, #595959);
font: inherit;
font-size: 1rem;
font-weight: 600;
white-space: nowrap;
cursor: pointer;
}
.inventory-report-tabs [data-inventory-report-tab]:hover,
.inventory-report-tabs [data-inventory-report-tab][aria-selected="true"] {
color: var(--vip-content-accent-dark, #856632);
}
.inventory-report-tabs [data-inventory-report-tab][aria-selected="true"]::after {
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 3px;
border-radius: 999px 999px 0 0;
background: var(--vip-content-accent, #b9954f);
content: '';
}
.inventory-report-tabs [data-inventory-report-tab]:focus-visible {
outline: 2px solid var(--vip-content-accent-dark, #856632);
outline-offset: 3px;
}
.inventory-report-panel[hidden] {
display: none;
}
.inventory-warehouse-mapping {
overflow: visible;
}
.inventory-warehouse-mapping__table {
min-width: 52rem;
border-collapse: separate;
border-spacing: 0;
isolation: isolate;
}
.inventory-warehouse-mapping__table thead th {
position: sticky !important;
top: 0 !important;
z-index: 20 !important;
background: var(--vip-content-accent-soft, #f7f0de);
box-shadow: 0 1px 0 var(--vip-border, #d8d2c7);
}
.inventory-warehouse-mapping__table thead,
.inventory-warehouse-mapping__table thead tr {
position: sticky;
top: 0;
z-index: 5;
}
.inventory-warehouse-mapping__table thead th[data-directory-fixed="start"],
.inventory-warehouse-mapping__table thead th[data-directory-fixed="end"] {
z-index: 21 !important;
background: var(--vip-content-accent-soft, #f7f0de);
}
.inventory-warehouse-mapping__table tbody td {
position: relative !important;
z-index: 0 !important;
background: var(--vip-surface, #fff);
}
.inventory-warehouse-mapping__table tbody tr:hover td {
background: #fbf8f0;
}
.inventory-warehouse-mapping__empty {
padding: 3rem 1.25rem;
color: var(--vip-content-muted, #595959);
text-align: center;
}
.inventory-warehouse-mapping__filters {
grid-template-columns: repeat(2, minmax(12rem, 1fr)) auto;
align-items: end;
border: 0;
border-bottom: 1px solid var(--vip-border, #d8d2c7);
}
.inventory-warehouse-mapping__filters .directory-query-actions {
grid-column: auto;
justify-content: flex-start;
padding-top: 0;
}
.inventory-warehouse-mapping__dialog input {
min-height: 2.25rem;
padding: 0.45rem 0.65rem;
border: 1px solid var(--vip-border, #d8d2c7);
border-radius: 0.5rem;
background: #fff;
color: var(--vip-content-ink, #171717);
}
.inventory-warehouse-mapping__filters .biz-btn {
display: inline-flex;
height: 2.1rem;
align-items: center;
padding: 0 0.8rem;
text-decoration: none;
}
.inventory-warehouse-mapping__import {
display: inline-flex;
flex-wrap: wrap;
align-items: center;
gap: 0.4rem;
}
.inventory-warehouse-mapping__export,
.inventory-warehouse-mapping__file-label,
.inventory-warehouse-mapping__import button {
display: inline-flex;
height: 2.25rem;
align-items: center;
gap: 0.35rem;
padding: 0 0.875rem;
border: 1px solid var(--vip-content-accent, #b9954f);
border-radius: 0.25rem;
background: #fff;
color: var(--vip-content-accent-dark, #856632);
font: inherit;
text-decoration: none;
cursor: pointer;
}
.inventory-warehouse-mapping__file-label input[type="file"] {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
clip-path: inset(50%);
white-space: nowrap;
}
.inventory-warehouse-mapping__actions {
display: inline-flex;
gap: 0.4rem;
}
.inventory-warehouse-mapping__actions button {
padding: 0.25rem 0.45rem;
border: 1px solid var(--vip-border, #d8d2c7);
border-radius: 0.4rem;
background: #fff;
color: var(--vip-content-accent-dark, #856632);
cursor: pointer;
}
.inventory-warehouse-mapping__preview {
margin: 1rem 1.25rem;
overflow: auto;
border: 1px solid var(--vip-border, #d8d2c7);
border-radius: 0.5rem;
}
.inventory-warehouse-mapping__preview summary {
padding: 0.75rem 1rem;
cursor: pointer;
color: var(--vip-content-accent-dark, #856632);
}
.inventory-warehouse-mapping__preview table {
width: 100%;
border-collapse: collapse;
font-size: 0.75rem;
}
.inventory-warehouse-mapping__preview th,
.inventory-warehouse-mapping__preview td {
padding: 0.5rem 0.75rem;
border-top: 1px solid var(--vip-border, #d8d2c7);
text-align: left;
white-space: nowrap;
}
.inventory-warehouse-mapping__preview .is-invalid,
.inventory-warehouse-mapping__preview .is-conflict {
color: #a33a32;
}
.inventory-warehouse-mapping__preview .is-same {
color: var(--vip-content-muted, #595959);
}
.inventory-warehouse-mapping__dialog {
width: min(32rem, calc(100vw - 2rem));
padding: 0;
border: 1px solid var(--vip-border, #d8d2c7);
border-radius: 0.75rem;
box-shadow: 0 1.5rem 4rem rgba(0, 0, 0, 0.2);
}
.inventory-warehouse-mapping__dialog::backdrop {
background: rgba(21, 18, 12, 0.38);
}
.inventory-warehouse-mapping__dialog form {
display: grid;
gap: 1rem;
padding: 1.25rem;
}
.inventory-warehouse-mapping__dialog h3 {
margin: 0;
color: var(--vip-content-ink, #171717);
font-size: 1rem;
}
.inventory-warehouse-mapping__dialog label {
display: grid;
gap: 0.35rem;
color: var(--vip-content-muted, #595959);
font-size: 0.8rem;
}
.inventory-warehouse-mapping__dialog__actions {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
}
@media (max-width: 700px) {
.directory-table-head {
flex-direction: column;
}
.directory-table-status {
align-self: flex-start;
}
.directory-table-pagination {
align-items: flex-start;
flex-direction: column;
}
}
</style>
<section
class="directory-table-page {{ $directoryUsesFieldQuery ? 'has-field-query' : '' }}"
data-directory-page="{{ $page['slug'] ?? '' }}"
data-directory-last-page="{{ $directoryLastPage }}"
data-directory-user="{{ auth()->id() ?? 'guest' }}"
data-directory-section="{{ $section['slug'] ?? '' }}"
data-directory-tab="{{ $activeTab['slug'] ?? '' }}"
aria-label="{{ $page['title'] ?? $section['title'] }}"
>
@if ($directoryUsesFieldQuery)
<header class="directory-query-page-head">
<div>
<strong>{{ $page['title'] ?? $section['title'] }}</strong>
<p>{{ $directoryQueryPageDescriptions[$directoryPageKey] ?? '基础资料的查询与导出' }}</p>
</div>
<span class="sr-only" data-directory-status>{{ $directory['status'] ?? '尚未同步' }}</span>
</header>
@if (!empty($upstreamError))
<div class="directory-table-notice" role="alert">
<span class="material-icons-outlined" aria-hidden="true">error_outline</span>
<span>{{ $upstreamError }}</span>
</div>
@elseif ($directoryTotal > 0 && in_array($directoryPageKey, ['store-management/store-accounts', 'store-management/store-report', 'company-management/subject-operating-report', 'company-management/company-cost-arrangement', 'company-management/personnel-inventory'], true))
<div class="directory-table-notice" role="status">
<span class="material-icons-outlined" aria-hidden="true">cloud_done</span>
<span>已从共享业务镜像读取 {{ $directoryTotal }} 条数据,当前页面为只读展示。</span>
</div>
@endif
@else
@if ($isInventoryReportPage)
<nav class="business-tab-bar inventory-report-tabs" data-inventory-report-tabs role="tablist" aria-label="进销存报表数据表">
<button type="button" role="tab" aria-selected="true" aria-controls="inventory-report-total-panel" id="inventory-report-total-tab" data-inventory-report-tab="total">
进销存总表
</button>
<button type="button" role="tab" aria-selected="false" aria-controls="inventory-report-mapping-panel" id="inventory-report-mapping-tab" data-inventory-report-tab="mapping" tabindex="-1">
仓库名称映射表
</button>
</nav>
<section
id="inventory-report-total-panel"
class="inventory-report-panel"
data-inventory-report-panel="total"
data-directory-table-scope
data-directory-table-key="total"
data-directory-last-page="{{ $directoryLastPage }}"
data-directory-source-url="{{ request()->fullUrlWithQuery(['page' => 1]) }}"
role="tabpanel"
aria-labelledby="inventory-report-total-tab"
>
@else
<header class="directory-table-head">
<div class="directory-table-title">
<span class="material-icons-outlined" aria-hidden="true">{{ $directory['icon'] ?? 'table_view' }}</span>
<div>
<strong>{{ $page['title'] ?? $section['title'] }} · 字段结构</strong>
@if (!empty($directory['description']))
<p class="directory-table-description">{{ $directory['description'] }}</p>
@endif
</div>
</div>
<span class="directory-table-status" data-directory-status>{{ $directory['status'] ?? '尚未同步' }}</span>
</header>
@endif
@if (!empty($upstreamError))
<div class="directory-table-notice" role="alert">
<span class="material-icons-outlined" aria-hidden="true">error_outline</span>
<span>{{ $upstreamError }}</span>
</div>
@elseif (($directory['status'] ?? null) === '演示数据' && $directoryTotal > 0)
<div class="directory-table-notice" role="status">
<span class="material-icons-outlined" aria-hidden="true">preview</span>
<span>已按附件模板导入 {{ $directoryTotal }} 条演示数据,当前页面为只读展示。</span>
</div>
@elseif ($directoryTotal > 0)
<div class="directory-table-notice" role="status">
<span class="material-icons-outlined" aria-hidden="true">cloud_done</span>
<span>已从共享业务镜像读取 {{ $directoryTotal }} 条数据,当前页面为只读展示。</span>
</div>
@elseif (($directory['status'] ?? null) === '既有只读上游')
<div class="directory-table-notice" role="status">
<span class="material-icons-outlined" aria-hidden="true">visibility</span>
<span>当前页面只读显示既有上游资料;未匹配的 Excel 字段保留为空,不代表业务上没有数据。</span>
</div>
@elseif (($directory['status'] ?? null) === '共享镜像' && $directoryTotal > 0)
<div class="directory-table-notice" role="status">
<span class="material-icons-outlined" aria-hidden="true">cloud_done</span>
<span>当前显示团队共享业务镜像数据,分页和汇总均基于已同步镜像。</span>
</div>
@else
<div class="directory-table-notice" role="status">
<span class="material-icons-outlined" aria-hidden="true">sync_problem</span>
<span>{{ $directory['empty'] ?? '数据尚未同步,当前只展示已确认的字段结构。' }}</span>
</div>
@endif
@endif
@if (!empty($directoryFields))
@php
$directoryQueryFields = $directoryPageKey === 'company-management/basic-information'
? [
['name' => 'unified_social_credit_code', 'label' => '统一社会信用代码', 'placeholder' => '输入完整或部分代码'],
['name' => 'company_name', 'label' => '公司名称', 'placeholder' => '输入公司名称关键词'],
['name' => 'taxpayer_type', 'label' => '纳税人类型'],
['name' => 'company_status', 'label' => '公司状态'],
['name' => 'legal_representative', 'label' => '法人', 'placeholder' => '按姓名查询'],
['name' => 'city', 'label' => '城市', 'placeholder' => '城市较多,使用搜索'],
['name' => 'related_people', 'label' => '相关人员', 'placeholder' => '输入人员姓名,模糊匹配'],
['name' => 'company_type', 'label' => '公司类型'],
]
: $directoryFields;
$directoryExcludedQueryFields = match ($directoryPageKey) {
'store-management/basic-information' => ['opened_at', 'renewed_at'],
'trademark-management/information' => ['registration_date', 'expiry_date'],
default => [],
};
if (!empty($directoryExcludedQueryFields)) {
$directoryQueryFields = collect($directoryQueryFields)
->reject(fn (array $field): bool => in_array(
$field['name'],
$directoryExcludedQueryFields,
true,
))
->values()
->all();
}
$directoryMoreQueryFields = collect($directoryQueryFields)
->filter(function (array $field) use ($directoryPageKey): bool {
$kind = $field['kind'] ?? 'text';
return in_array($directoryPageKey, [
'store-management/store-report',
'company-management/subject-operating-report',
'company-management/company-cost-arrangement',
'company-management/personnel-inventory',
], true) && in_array($kind, ['date', 'number', 'integer', 'secret', 'secret_status'], true);
})
->values()
->all();
$directoryPrimaryQueryFields = collect($directoryQueryFields)
->reject(fn (array $field): bool => in_array($field['name'], array_column($directoryMoreQueryFields, 'name'), true))
->values()
->all();
$directorySelectFields = match (($section['slug'] ?? '') . '/' . ($page['slug'] ?? '')) {
'company-management/basic-information' => ['taxpayer_type', 'company_status', 'company_type'],
'store-management/basic-information' => ['platform', 'store_status', 'withdrawal_account'],
'trademark-management/information' => ['trademark_status'],
'store-management/store-accounts' => ['account_type', 'password_configured'],
'store-management/store-report' => ['status', 'can_change', 'company_type'],
'company-management/subject-operating-report' => ['company_type', 'can_change_entity'],
'company-management/company-cost-arrangement' => ['company_type', 'invoice_type'],
'company-management/personnel-inventory' => ['company_type'],
default => [],
};
$directoryColumnNames = collect($directoryFields)->pluck('name')->all();
$requestedDirectoryColumns = array_values(array_intersect(
array_filter(explode(',', (string) request('columns', ''))),
$directoryColumnNames,
));
$visibleDirectoryColumns = request()->has('columns') ? $requestedDirectoryColumns : $directoryColumnNames;
$directoryQueryFormId = 'directory-query-form-' . ($page['slug'] ?? 'page');
@endphp
@if ($directoryUsesFieldQuery)
<div class="directory-query-bar" data-directory-query aria-label="{{ $page['title'] ?? '' }}查询条件">
<div class="directory-query-heading">
<strong>查询条件</strong>
<span>类型较少的字段直接选择,内容较多的字段支持关键词搜索</span>
</div>
<form id="{{ $directoryQueryFormId }}" method="GET" action="{{ request()->url() }}"></form>
@if (request()->has('columns'))
<input type="hidden" form="{{ $directoryQueryFormId }}" name="columns" value="{{ implode(',', $visibleDirectoryColumns) }}">
@endif
@foreach ($directoryPrimaryQueryFields as $field)
@php $kind = $field['kind'] ?? 'text'; @endphp
@continue($kind === 'secret' && $field['name'] !== 'withdrawal_account')
@continue(($page['slug'] ?? '') === 'store-accounts' && in_array($field['name'], [
'account',
'password_configured',
'bound_phone',
'verification_contact',
'account_group_name',
'created_at',
'updated_at',
], true))
<label class="directory-query-field" data-directory-query-field="{{ $field['name'] }}">
<span title="{{ $field['label'] }}">{{ $field['label'] }}</span>
@if ($kind === 'date')
<span class="directory-query-dates">
<input type="date" form="{{ $directoryQueryFormId }}" name="{{ $field['name'] }}_from" value="{{ request($field['name'] . '_from') }}" data-directory-query-from="{{ $field['name'] }}" aria-label="{{ $field['label'] }}开始">
<em>至</em>
<input type="date" form="{{ $directoryQueryFormId }}" name="{{ $field['name'] }}_to" value="{{ request($field['name'] . '_to') }}" data-directory-query-to="{{ $field['name'] }}" aria-label="{{ $field['label'] }}结束">
</span>
@elseif (in_array($field['name'], $directorySelectFields, true))
<select form="{{ $directoryQueryFormId }}" name="{{ $field['name'] }}" data-directory-query-select="{{ $field['name'] }}" aria-label="{{ $field['label'] }}">
<option value="">全部</option>
@php
$enumOptions = match ($field['name']) {
'platform' => ['淘宝', '淘宝分销', '阿里巴巴', '天猫', '唯品会', '京东', '京喜', '抖音', '拼多多', '淘工厂', '得物'],
'store_status' => ['正常', '关闭中', '已关闭'],
'withdrawal_account' => ['已配置', '未配置'],
'trademark_status' => ['未启用', '有效'],
'account_type' => ['主账号', '子账号'],
'password_configured' => ['已设置', '未设置', '状态未同步'],
'taxpayer_type' => ['一般纳税人', '小规模纳税人', '个体工商户', '无税务登记', 'CORP'],
'company_status' => ['正常', '已注销'],
'company_type' => ['个体工商户', '个体户', '二级公司', '人力资源公司', '仓储公司', '其他', '合同公司', '品牌公司', '店铺-国标公司', '店铺-国标商标', '店铺-大店公司', '店铺-普通公司', '母公司', '资产型'],
'status' => ['0' => '开店中', '3' => '关店中', '6' => '关店'],
'can_change', 'can_change_entity' => ['是', '否'],
'invoice_type' => ['普票', '专票'],
default => [],
};
@endphp
@foreach ($enumOptions as $option)
<option value="{{ $option }}" @selected((string) request($field['name']) === $option)>{{ $option }}</option>
@endforeach
</select>
@else
<input type="search" form="{{ $directoryQueryFormId }}" name="{{ $field['name'] }}" value="{{ request($field['name']) }}" autocomplete="off" placeholder="{{ $field['placeholder'] ?? ('搜索' . $field['label']) }}" data-directory-query-input="{{ $field['name'] }}" aria-label="搜索{{ $field['label'] }}">
<ul class="directory-query-suggestions" data-directory-query-suggestions="{{ $field['name'] }}" hidden></ul>
@endif
</label>
@endforeach
@if ($directoryMoreQueryFields !== [])
<details class="directory-query-more">
<summary>更多查询条件({{ count($directoryMoreQueryFields) }})</summary>
<div class="directory-query-more__fields">
@foreach ($directoryMoreQueryFields as $field)
@php $kind = $field['kind'] ?? 'text'; @endphp
@continue($kind === 'secret' && $field['name'] !== 'withdrawal_account')
@continue(($page['slug'] ?? '') === 'store-accounts' && in_array($field['name'], [
'account',
'password_configured',
'bound_phone',
'verification_contact',
'account_group_name',
'created_at',
'updated_at',
], true))
<label class="directory-query-field" data-directory-query-field="{{ $field['name'] }}">
<span title="{{ $field['label'] }}">{{ $field['label'] }}</span>
@if ($kind === 'date')
<span class="directory-query-dates">
<input type="date" form="{{ $directoryQueryFormId }}" name="{{ $field['name'] }}_from" value="{{ request($field['name'] . '_from') }}" data-directory-query-from="{{ $field['name'] }}" aria-label="{{ $field['label'] }}开始">
<em>至</em>
<input type="date" form="{{ $directoryQueryFormId }}" name="{{ $field['name'] }}_to" value="{{ request($field['name'] . '_to') }}" data-directory-query-to="{{ $field['name'] }}" aria-label="{{ $field['label'] }}结束">
</span>
@elseif (in_array($field['name'], $directorySelectFields, true))
<select form="{{ $directoryQueryFormId }}" name="{{ $field['name'] }}" data-directory-query-select="{{ $field['name'] }}" aria-label="{{ $field['label'] }}">
<option value="">全部</option>
@php
$enumOptions = match ($field['name']) {
'platform' => ['淘宝', '淘宝分销', '阿里巴巴', '天猫', '唯品会', '京东', '京喜', '抖音', '拼多多', '淘工厂', '得物'],
'store_status' => ['正常', '关闭中', '已关闭'],
'withdrawal_account' => ['已配置', '未配置'],
'trademark_status' => ['未启用', '有效'],
'account_type' => ['主账号', '子账号'],
'password_configured' => ['已设置', '未设置', '状态未同步'],
'taxpayer_type' => ['一般纳税人', '小规模纳税人', '个体工商户', '无税务登记', 'CORP'],
'company_status' => ['正常', '已注销'],
'company_type' => ['个体工商户', '个体户', '二级公司', '人力资源公司', '仓储公司', '其他', '合同公司', '品牌公司', '店铺-国标公司', '店铺-国标商标', '店铺-大店公司', '店铺-普通公司', '母公司', '资产型'],
'status' => ['0' => '开店中', '3' => '关店中', '6' => '关店'],
'can_change', 'can_change_entity' => ['是', '否'],
'invoice_type' => ['普票', '专票'],
default => [],
};
@endphp
@foreach ($enumOptions as $option)
<option value="{{ $option }}" @selected((string) request($field['name']) === $option)>{{ $option }}</option>
@endforeach
</select>
@else
<input type="search" form="{{ $directoryQueryFormId }}" name="{{ $field['name'] }}" value="{{ request($field['name']) }}" autocomplete="off" placeholder="{{ $field['placeholder'] ?? ('搜索' . $field['label']) }}" data-directory-query-input="{{ $field['name'] }}" aria-label="搜索{{ $field['label'] }}">
<ul class="directory-query-suggestions" data-directory-query-suggestions="{{ $field['name'] }}" hidden></ul>
@endif
</label>
@endforeach
</div>
</details>
@endif
<div class="directory-query-actions">
<details class="directory-query-picker" data-directory-query-picker>
<summary>筛选字段</summary>
<div class="directory-query-picker__panel" role="group" aria-label="勾选展示字段">
<span class="directory-query-picker__hint">勾选需要显示的表头,可重复调整。</span>
@foreach ($directoryFields as $field)
<label class="directory-query-picker__option">
<input type="checkbox" value="{{ $field['name'] }}" data-directory-query-column @checked(in_array($field['name'], $visibleDirectoryColumns, true))>
<span>{{ $field['label'] }}</span>
</label>
@endforeach
</div>
</details>
<button type="button" data-directory-query-reset>重置</button>
<button type="submit" form="{{ $directoryQueryFormId }}" class="is-primary">查询</button>
<details class="directory-query-export" data-directory-query-export-picker>
<summary><span class="material-icons-outlined" aria-hidden="true">file_download</span>导出 Excel</summary>
<div class="directory-query-export__panel" role="group" aria-label="选择导出字段">
<span class="directory-query-picker__hint">默认全选,可取消不需要导出的表头。</span>
@foreach ($directoryFields as $field)
<label class="directory-query-export__option">
<input type="checkbox" value="{{ $field['name'] }}" checked data-directory-query-export-field>
<span>{{ $field['label'] }}</span>
</label>
@endforeach
<button type="button" class="directory-query-export__button" data-directory-query-export>导出选中字段</button>
</div>
</details>
</div>
</div>
@endif
@if (($page['slug'] ?? '') === 'store-report' && ! $directoryUsesFieldQuery)
<form method="GET" action="{{ request()->url() }}" class="installation-filter-bar" aria-label="店铺维度报表筛选查询栏">
@php
$storeFilterTextFields = [
['name' => 'store_name', 'label' => '店铺名称:'],
['name' => 'store_id', 'label' => '店铺ID:'],
['name' => 'profit_center', 'label' => '利润中心:'],
['name' => 'store_manager', 'label' => '店铺负责人:'],
['name' => 'department_manager', 'label' => '部门负责人:'],
['name' => 'entity', 'label' => '主体:'],
['name' => 'company_type', 'label' => '公司类型:'],
];
$storeFilterDateFields = [
['name' => 'opened_at', 'label' => '开店时间:'],
['name' => 'effective_at', 'label' => '生效时间:'],
['name' => 'invalid_at', 'label' => '失效时间:'],
];
@endphp
@foreach ($storeFilterTextFields as $filter)
<label class="installation-filter-field">
<span>{{ $filter['label'] }}</span>
<input type="text" name="{{ $filter['name'] }}" value="{{ request($filter['name']) }}" placeholder="按原值精确查询">
</label>
@endforeach
<label class="installation-filter-field">
<span>状态:</span>
<select name="status">
<option value="">全部</option>
@foreach (['0' => '开店中', '3' => '关店中', '6' => '关店'] as $value => $label)
<option value="{{ $value }}" @selected((string) request('status') === $value)>{{ $label }}</option>
@endforeach
</select>
</label>
<label class="installation-filter-field">
<span>能否变更:</span>
<select name="can_change">
<option value="">全部</option>
@foreach (['是', '否'] as $value)
<option value="{{ $value }}" @selected((string) request('can_change') === $value)>{{ $value }}</option>
@endforeach
</select>
</label>
@foreach ($storeFilterDateFields as $filter)
<label class="installation-filter-field">
<span>{{ $filter['label'] }}</span>
<input type="date" name="{{ $filter['name'] }}_from" value="{{ request($filter['name'] . '_from') }}" aria-label="{{ $filter['label'] }}开始(含)">
<span>至</span>
<input type="date" name="{{ $filter['name'] }}_to" value="{{ request($filter['name'] . '_to') }}" aria-label="{{ $filter['label'] }}结束(不含)">
</label>
@endforeach
<div class="installation-filter-actions">
<button type="submit" class="biz-btn biz-btn-primary">查询</button>
<a href="{{ request()->url() }}" class="biz-btn biz-btn-ghost">清除</a>
</div>
</form>
@elseif (($page['slug'] ?? '') === 'inventory-report')
<form method="GET" action="{{ request()->url() }}" class="installation-filter-bar" aria-label="进销存报表筛选查询栏">
<label class="installation-filter-field">
<span>月份:</span>
@php
$inventorySelectedMonth = (string) request('month', now()->startOfMonth()->subMonth()->format('Y-m'));
@endphp
<div class="inventory-month-picker" data-inventory-month-picker data-min-month="2026-07">
<input type="hidden" name="month" min="2026-07" value="{{ $inventorySelectedMonth }}" data-inventory-month-value>
<button type="button" class="inventory-month-picker__trigger" data-inventory-month-trigger aria-haspopup="dialog" aria-expanded="false" aria-label="选择进销存报表月份">
<span data-inventory-month-label>{{ $inventorySelectedMonth !== '' ? str_replace('-', ' 年 ', $inventorySelectedMonth) . ' 月' : '' }}</span>
<span class="material-icons-outlined" aria-hidden="true">calendar_month</span>
</button>
<div class="inventory-month-picker__panel" data-inventory-month-panel role="dialog" aria-label="选择报表月份" hidden>
<div class="inventory-month-picker__yearbar">
<button type="button" class="inventory-month-picker__nav" data-inventory-month-prev aria-label="上一年"><span class="material-icons-outlined" aria-hidden="true">chevron_left</span></button>
<strong data-inventory-month-year></strong>
<button type="button" class="inventory-month-picker__nav" data-inventory-month-next aria-label="下一年"><span class="material-icons-outlined" aria-hidden="true">chevron_right</span></button>
</div>
<div class="inventory-month-picker__months" data-inventory-month-grid></div>
<div class="inventory-month-picker__footer">
<button type="button" data-inventory-month-clear>清除</button>
<button type="button" data-inventory-month-current>本月</button>
</div>
</div>
</div>
</label>
<label class="installation-filter-field">
<span>商品编码:</span>
<input type="text" name="product_code" value="{{ request('product_code') }}" placeholder="按商品编码精确查询">
</label>
<label class="installation-filter-field">
<span>仓储方:</span>
<input type="text" name="warehouse_provider" value="{{ request('warehouse_provider') }}" placeholder="支持包含查询">
</label>
<label class="installation-filter-field">
<span>分类:</span>
<input type="text" name="category" value="{{ request('category') }}" placeholder="支持包含查询">
</label>
<div class="installation-filter-actions">
<button type="submit" class="biz-btn biz-btn-primary">查询</button>
<a href="{{ request()->url() }}" class="biz-btn biz-btn-ghost">清除</a>
</div>
</form>
@endif
@if (! $directoryUsesFieldQuery)
<div class="directory-table-tools" data-directory-tools @if (($page['slug'] ?? '') === 'inventory-report') data-directory-server-export @endif>
<label class="sr-only" for="directory-search-{{ $page['slug'] ?? 'page' }}">搜索当前表格</label>
<input id="directory-search-{{ $page['slug'] ?? 'page' }}" type="search" placeholder="搜索当前页数据..." data-directory-search>
<div class="directory-field-picker" data-directory-field-picker>
<button type="button" aria-expanded="false" aria-controls="directory-fields-{{ $page['slug'] ?? 'page' }}" data-directory-field-toggle>列设置</button>
<div class="directory-field-picker__panel" id="directory-fields-{{ $page['slug'] ?? 'page' }}" role="group" aria-label="选择展示字段" hidden>
<span class="directory-field-picker__hint">拖动字段调整顺序;固定序号列不可隐藏。</span>
@foreach ($directoryFields as $field)
<label class="directory-field-picker__option" draggable="true" data-directory-field-option>
<input type="checkbox" value="{{ $field['name'] }}" data-directory-field-check @checked(!in_array($field['name'], ['sales_01', 'sales_02', 'sales_03', 'sales_04', 'sales_05', 'sales_06'], true))>
<span>{{ $field['label'] }}</span>
</label>
@endforeach
</div>
</div>
@if (($page['slug'] ?? '') === 'inventory-report')
<select data-inventory-export-scope aria-label="导出范围">
<option value="all">全部筛选结果</option>
<option value="page">当前页(第 {{ $directoryCurrentPage }} 页)</option>
</select>
<select data-inventory-export-fields aria-label="导出字段范围">
<option value="visible">当前显示列及顺序</option>
<option value="all">全部可导出字段</option>
</select>
<button type="button" data-inventory-export data-export-url="{{ route('business.product-inventory.inventory-report.export') }}">导出</button>
@else
<div class="directory-export-group" aria-label="导出范围">
<button type="button" data-directory-export="visible">当前页·显示列</button>
<button type="button" data-directory-export="all-fields">当前页·全部字段</button>
<button type="button" data-directory-export-all="visible">全部结果·显示列</button>
<button type="button" data-directory-export-all="all-fields">全部结果·全部字段</button>
</div>
<button type="button" data-directory-reset>恢复默认</button>
<button type="button" data-directory-template>下载表头模板</button>
@endif
</div>
@endif
<div class="directory-table-scroll">
<table
id="{{ $isInventoryReportPage ? 'inventory-report-total-table' : 'directory-table-' . ($page['slug'] ?? 'page') }}"
class="directory-table"
@if($isInventoryReportPage) data-directory-table-key="total" @endif
@if($directoryUsesFieldQuery) data-columns-ready="true" @endif
>
<thead>
<tr>
<th scope="col" data-directory-fixed="start">#</th>
@foreach ($directoryFields as $field)
<th scope="col" data-column-key="{{ $field['name'] }}" @if($isInventoryReportPage && $field['name'] === 'warehouse_short_name') aria-sort="none" @endif class="{{ in_array($field['kind'] ?? null, ['number', 'integer'], true) ? 'is-number' : '' }} {{ !in_array($field['name'], $visibleDirectoryColumns, true) ? 'is-column-hidden' : '' }}">
@if ($directoryUsesFieldQuery)
@php
$isSorted = request('sort') === $field['name'];
$direction = $isSorted ? request('direction', 'asc') : null;
$nextDirection = $isSorted && $direction === 'asc' ? 'desc' : 'asc';
@endphp
<div class="directory-header-menu" data-directory-header-menu="{{ $field['name'] }}">
<span data-directory-header-label>{{ $field['label'] }}</span>
<details>
<summary aria-label="{{ $field['label'] }}排序和字段操作"><span class="material-icons-outlined directory-sort-icon" aria-hidden="true">{{ !$isSorted ? 'unfold_more' : ($direction === 'asc' ? 'arrow_upward' : 'arrow_downward') }}</span></summary>
<div class="directory-header-menu__panel">
<a href="{{ request()->fullUrlWithQuery(['sort' => $field['name'], 'direction' => 'asc', 'page' => 1]) }}">升序</a>
<a href="{{ request()->fullUrlWithQuery(['sort' => $field['name'], 'direction' => 'desc', 'page' => 1]) }}">降序</a>
<button type="button" data-directory-header-hide="{{ $field['name'] }}">隐藏字段</button>
</div>
</details>
</div>
@elseif ($isInventoryReportPage && $field['name'] === 'warehouse_short_name')
<button type="button" class="directory-inventory-sort" data-inventory-short-name-sort aria-label="仓库简称按当前页升序排序" title="当前页排序">{{ $field['label'] }}<span class="material-icons-outlined directory-sort-icon" aria-hidden="true">unfold_more</span></button>
@else
{{ $field['label'] }}
@endif
</th>
@endforeach
</tr>
</thead>
<tbody>
@forelse ($directoryRows as $rowIndex => $row)
<tr @if (($page['slug'] ?? '') === 'subject-operating-report') data-subject-row data-subject-details='@json($row->company_details ?? [])' tabindex="0" aria-label="查看主体详细资料" @endif>
<td data-directory-fixed="start">{{ $rowIndex + 1 + (($directoryCurrentPage - 1) * $directoryPerPage) }}</td>
@foreach ($directoryFields as $field)
@php
$source = array_key_exists('source', $field) ? $field['source'] : $field['name'];
$value = $source ? data_get($row, $source) : null;
$kind = $field['kind'] ?? 'text';
$display = $value;
if ($kind === 'secret') {
$display = filled($value) ? '已配置' : '—';
} elseif ($kind === 'secret_status') {
$display = $value === null || $value === ''
? '状态未同步'
: ($value ? '已设置' : '未设置');
} elseif ($kind === 'date' && filled($value)) {
$display = substr((string) $value, 0, 10);
} elseif ($kind === 'number' && is_numeric($value)) {
$display = number_format((float) $value, 2);
} elseif ($kind === 'integer' && is_numeric($value)) {
$display = number_format((int) $value);
} elseif ($kind === 'boolean' && $value !== null && $value !== '') {
$display = in_array($value, [true, 1, '1', '是', '✔'], true) ? '是' : '否';
}
@endphp
<td data-column-key="{{ $field['name'] }}" class="{{ in_array($kind, ['number', 'integer'], true) ? 'is-number' : '' }} {{ !in_array($field['name'], $visibleDirectoryColumns, true) ? 'is-column-hidden' : '' }}">
@if (($page['slug'] ?? '') === 'store-accounts' && $field['name'] === 'password_configured')
<span class="sr-only" data-password-status>{{ filled($display) ? $display : '—' }}</span>
@if ($value === false)
<span class="store-password-state">未设置</span>
@else
<span
data-store-password-cell
data-account-id="{{ data_get($row, 'id') }}"
data-copy-url="{{ route('business.store-account-passwords.reveal', ['account' => data_get($row, 'id')]) }}"
>
<span class="store-password-value" data-store-password-value aria-live="polite">读取中…</span>
<button type="button" data-store-password-copy hidden aria-label="复制{{ data_get($row, 'store_name') }}的账号密码">复制</button>
</span>
@endif
@else
{{ filled($display) ? $display : ($isInventoryReportPage && $field['name'] === 'warehouse_short_name' ? '' : '—') }}
@endif
</td>
@endforeach
</tr>
@empty
<tr class="is-empty">
<td colspan="{{ count($directoryFields) + 1 }}">
<div class="directory-table-empty">
<span class="material-icons-outlined" aria-hidden="true">inbox</span>
<span>{{ !empty($upstreamError) ? '资料暂时无法读取,请按上方提示检查连接;这不代表业务数据为 0。' : ($directory['empty'] ?? '尚未同步数据。') }}</span>
</div>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@if ($directoryIsPaginated && empty($upstreamError))
<footer class="directory-table-pagination">
<span>共 {{ $directoryTotal }} 条 · 第 {{ $directoryCurrentPage }} / {{ $directoryLastPage }} 页</span>
<span>
@if ($directoryRows->onFirstPage())
<span>上一页</span>
@else
<a href="{{ $directoryRows->previousPageUrl() }}">上一页</a>
@endif
·
@if ($directoryRows->hasMorePages())
<a href="{{ $directoryRows->nextPageUrl() }}">下一页</a>
@else
<span>下一页</span>
@endif
</span>
</footer>
@endif
@if ($isInventoryReportPage)
</section>
<section
id="inventory-report-mapping-panel"
class="inventory-report-panel"
data-inventory-report-panel="mapping"
data-directory-table-scope
data-directory-table-key="warehouse-mapping"
data-directory-last-page="{{ $inventoryWarehouseMappings->lastPage() }}"
data-directory-source-url="{{ request()->fullUrlWithQuery(['mapping' => 1, 'page' => 1]) }}"
role="tabpanel"
aria-labelledby="inventory-report-mapping-tab"
hidden
>
<div class="inventory-warehouse-mapping">
<form method="GET" action="{{ request()->url() }}" class="directory-query-bar inventory-warehouse-mapping__filters" aria-label="仓库名称映射查询">
<input type="hidden" name="mapping" value="1">
<label class="directory-query-field">
<span>仓储方</span>
<input type="search" name="mapping_warehouse_provider" value="{{ request('mapping_warehouse_provider') }}" placeholder="按仓储方包含查询">
</label>
<label class="directory-query-field">
<span>仓库名</span>
<input type="search" name="mapping_warehouse" value="{{ request('mapping_warehouse') }}" placeholder="按仓库名包含查询">
</label>
<div class="directory-query-actions">
<button type="submit" class="is-primary">查询</button>
<a href="{{ request()->url() }}?mapping=1" class="biz-btn biz-btn-ghost">清除</a>
</div>
</form>
<div class="directory-table-tools" data-directory-tools>
<label class="sr-only" for="directory-search-inventory-warehouse-mapping">搜索当前表格</label>
<input id="directory-search-inventory-warehouse-mapping" type="search" placeholder="搜索当前页数据..." data-directory-search>
<div class="directory-field-picker" data-directory-field-picker>
<button type="button" aria-expanded="false" aria-controls="directory-fields-inventory-warehouse-mapping" data-directory-field-toggle>列设置</button>
<div class="directory-field-picker__panel" id="directory-fields-inventory-warehouse-mapping" role="group" aria-label="选择展示字段" hidden>
<span class="directory-field-picker__hint">可隐藏业务字段并拖动调整顺序;序号和操作列固定。</span>
@foreach ([
['name' => 'warehouse_provider', 'label' => '仓储方'],
['name' => 'warehouse', 'label' => '仓库'],
['name' => 'warehouse_short_name', 'label' => '仓库简称'],
] as $mappingField)
<label class="directory-field-picker__option" draggable="true" data-directory-field-option>
<input type="checkbox" value="{{ $mappingField['name'] }}" data-directory-field-check checked>
<span>{{ $mappingField['label'] }}</span>
</label>
@endforeach
</div>
</div>
<div class="directory-export-group" aria-label="导出范围">
<button type="button" data-directory-export="visible">当前页·显示列</button>
<button type="button" data-directory-export="all-fields">当前页·全部字段</button>
<button type="button" data-directory-export-all="visible">全部筛选结果·显示列</button>
<button type="button" data-directory-export-all="all-fields">全部筛选结果·全部字段</button>
</div>
<button type="button" data-directory-reset>恢复默认</button>
<button type="button" data-inventory-mapping-template>下载导入模板</button>
<button type="button" class="is-primary" data-inventory-mapping-create>新增仓库</button>
<a class="inventory-warehouse-mapping__export" href="{{ route('business.product-inventory.inventory-report.warehouse-mappings.export', request()->only(['mapping_warehouse_provider', 'mapping_warehouse'])) }}">导出完整表格(Excel)</a>
<form method="POST" enctype="multipart/form-data" action="{{ route('business.product-inventory.inventory-report.warehouse-mappings.import.preview') }}" class="inventory-warehouse-mapping__import">
@csrf
<input type="hidden" name="mapping_warehouse_provider" value="{{ request('mapping_warehouse_provider') }}">
<input type="hidden" name="mapping_warehouse" value="{{ request('mapping_warehouse') }}">
<label class="inventory-warehouse-mapping__file-label" for="inventory-warehouse-mapping-file">
<span>选择文件</span>
<input id="inventory-warehouse-mapping-file" type="file" name="import" accept=".xlsx,.xls,.csv" required data-inventory-mapping-import-file>
</label>
<button type="submit" class="inventory-warehouse-mapping__import-button">导入并预览</button>
</form>
</div>
@if (is_array($warehouseMappingPreview))
@php($previewSummary = $warehouseMappingPreview['summary'] ?? [])
<details class="inventory-warehouse-mapping__preview" open>
<summary>导入预览:新增 {{ $previewSummary['created'] ?? 0 }} 条,重复 {{ $previewSummary['same'] ?? 0 }} 条,冲突 {{ $previewSummary['conflicts'] ?? 0 }} 条,失败 {{ $previewSummary['invalid'] ?? 0 }} 条</summary>
<table>
<thead><tr><th>行号</th><th>仓储方</th><th>仓库</th><th>仓库简称</th><th>结果</th><th>原因</th></tr></thead>
<tbody>
@foreach (($warehouseMappingPreview['rows'] ?? []) as $previewRow)
<tr class="is-{{ $previewRow['status'] }}">
<td>{{ $previewRow['row_number'] }}</td>
<td>{{ $previewRow['warehouse_provider'] ?: '—' }}</td>
<td>{{ $previewRow['warehouse'] ?: '—' }}</td>
<td>{{ $previewRow['warehouse_short_name'] ?: '—' }}</td>
<td>{{ match ($previewRow['status']) { 'new' => '待新增', 'same' => '跳过', 'conflict' => '冲突', default => '失败' } }}</td>
<td>{{ $previewRow['message'] }}</td>
</tr>
@endforeach
</tbody>
</table>
@if (($previewSummary['conflicts'] ?? 0) === 0 && ($previewSummary['invalid'] ?? 0) === 0)
<form method="POST" action="{{ route('business.product-inventory.inventory-report.warehouse-mappings.import.commit') }}" style="padding:0.75rem 1rem;">
@csrf
<input type="hidden" name="preview_token" value="{{ $warehouseMappingPreview['token'] ?? '' }}">
<button type="submit" class="is-primary">确认导入</button>
</form>
@else
<p style="padding:0 1rem 0.75rem;margin:0;color:#a33a32;">存在失败或冲突行,系统不会写入任何记录。请修正 Excel 后重新预览。</p>
@endif
</details>
@endif
@if ($inventoryWarehouseMappings->isEmpty())
<div class="inventory-warehouse-mapping__empty">当前查询暂无仓库名称映射。可点击“新增仓库”或上传 Sheet1 导入。</div>
@else
<div class="directory-table-scroll">
<table id="inventory-warehouse-mapping-table" class="directory-table inventory-warehouse-mapping__table" data-directory-table-key="warehouse-mapping">
<thead>
<tr>
<th scope="col" data-directory-fixed="start">#</th>
<th scope="col" data-column-key="warehouse_provider">仓储方</th>
<th scope="col" data-column-key="warehouse">仓库</th>
<th scope="col" data-column-key="warehouse_short_name">仓库简称</th>
<th scope="col" data-directory-fixed="end">操作</th>
</tr>
</thead>
<tbody>
@foreach ($inventoryWarehouseMappings as $mapping)
<tr>
<td data-directory-fixed="start">{{ ($inventoryWarehouseMappings->currentPage() - 1) * $inventoryWarehouseMappings->perPage() + $loop->iteration }}</td>
<td data-column-key="warehouse_provider">{{ $mapping->warehouse_provider }}</td>
<td data-column-key="warehouse">{{ $mapping->warehouse }}</td>
<td data-column-key="warehouse_short_name">{{ $mapping->warehouse_short_name }}</td>
<td data-directory-fixed="end">
<span class="inventory-warehouse-mapping__actions">
<button type="button" data-inventory-mapping-edit data-id="{{ $mapping->id }}" data-provider="{{ $mapping->warehouse_provider }}" data-warehouse="{{ $mapping->warehouse }}" data-short-name="{{ $mapping->warehouse_short_name }}">修改</button>
</span>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<footer class="directory-table-pagination">
<span>共 {{ $inventoryWarehouseMappings->total() }} 条 · 第 {{ $inventoryWarehouseMappings->currentPage() }} / {{ $inventoryWarehouseMappings->lastPage() }} 页</span>
<span>
@if ($inventoryWarehouseMappings->onFirstPage()) <span>上一页</span> @else <a href="{{ $inventoryWarehouseMappings->previousPageUrl() }}&mapping=1">上一页</a> @endif
·
@if ($inventoryWarehouseMappings->hasMorePages()) <a href="{{ $inventoryWarehouseMappings->nextPageUrl() }}&mapping=1">下一页</a> @else <span>下一页</span> @endif
</span>
</footer>
@endif
</div>
<dialog class="inventory-warehouse-mapping__dialog" data-inventory-mapping-dialog>
<form method="POST" action="{{ route('business.product-inventory.inventory-report.warehouse-mappings.store') }}" data-inventory-mapping-form>
@csrf
<input type="hidden" name="_method" value="POST" data-inventory-mapping-method>
<h3 data-inventory-mapping-dialog-title>新增仓库名称映射</h3>
<label>仓储方 <input name="warehouse_provider" required maxlength="191" data-inventory-mapping-provider></label>
<label>仓库 <input name="warehouse" required maxlength="191" data-inventory-mapping-warehouse></label>
<label>仓库简称 <input name="warehouse_short_name" required maxlength="191" data-inventory-mapping-short-name><small>输入仓储方和仓库后会自动预填,可按业务需要修改。</small></label>
<div class="inventory-warehouse-mapping__dialog__actions">
<button type="button" data-inventory-mapping-cancel>取消</button>
<button type="submit" class="is-primary">保存</button>
</div>
</form>
</dialog>
</section>
@endif
@if (($page['slug'] ?? '') === 'subject-operating-report')
<section class="directory-subject-details" data-subject-details-panel hidden aria-live="polite">
<header class="directory-subject-details__head">
<strong>主体详细基础资料</strong>
<span data-subject-details-name>请选择主体行</span>
</header>
<div class="directory-subject-details__body" data-subject-details-body>
<span>请选择上方主体行查看基础资料。</span>
</div>
</section>
@endif
@endif
</section>
@if (($page['slug'] ?? '') === 'store-accounts')
@include('business.pages.partials.store-account-password')
@endif
@once
<script>
(() => {
const csvEscape = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`;
document.querySelectorAll('[data-inventory-month-picker]').forEach((picker) => {
const valueInput = picker.querySelector('[data-inventory-month-value]');
const trigger = picker.querySelector('[data-inventory-month-trigger]');
const label = picker.querySelector('[data-inventory-month-label]');
const panel = picker.querySelector('[data-inventory-month-panel]');
const yearLabel = picker.querySelector('[data-inventory-month-year]');
const grid = picker.querySelector('[data-inventory-month-grid]');
if (!valueInput || !trigger || !label || !panel || !yearLabel || !grid) return;
const monthPattern = /^(\d{4})-(0[1-9]|1[0-2])$/;
const minMonth = picker.dataset.minMonth || '';
const parsed = (value) => {
const match = monthPattern.exec(value || '');
return match ? { year: Number(match[1]), month: Number(match[2]) } : null;
};
const formatValue = (year, month) => `${year}-${String(month).padStart(2, '0')}`;
const formatLabel = (value) => {
const date = parsed(value);
return date ? `${date.year} 年 ${String(date.month).padStart(2, '0')} 月` : '';
};
const today = new Date();
const selected = parsed(valueInput.value);
let viewYear = selected?.year || today.getFullYear();
const isBeforeMin = (value) => Boolean(minMonth && value < minMonth);
const close = () => {
panel.hidden = true;
trigger.setAttribute('aria-expanded', 'false');
};
const render = () => {
yearLabel.textContent = `${viewYear} 年`;
const current = valueInput.value;
grid.innerHTML = Array.from({ length: 12 }, (_, index) => {
const month = index + 1;
const value = formatValue(viewYear, month);
const selectedClass = current === value ? ' is-selected' : '';
const disabled = isBeforeMin(value);
return `<button type="button" class="inventory-month-picker__month${selectedClass}" data-inventory-month-value-option="${value}"${disabled ? ' disabled aria-disabled="true"' : ''}>${String(month).padStart(2, '0')} 月</button>`;
}).join('');
};
const setValue = (value) => {
valueInput.value = value;
label.textContent = formatLabel(value);
const date = parsed(value);
if (date) viewYear = date.year;
render();
close();
};
label.textContent = formatLabel(valueInput.value);
render();
trigger.addEventListener('click', () => {
if (panel.hidden) {
render();
panel.hidden = false;
trigger.setAttribute('aria-expanded', 'true');
} else {
close();
}
});
picker.querySelector('[data-inventory-month-prev]')?.addEventListener('click', () => { viewYear -= 1; render(); });
picker.querySelector('[data-inventory-month-next]')?.addEventListener('click', () => { viewYear += 1; render(); });
grid.addEventListener('click', (event) => {
const option = event.target.closest('[data-inventory-month-value-option]');
if (option && !option.disabled) setValue(option.dataset.inventoryMonthValueOption);
});
picker.querySelector('[data-inventory-month-clear]')?.addEventListener('click', () => setValue(''));
picker.querySelector('[data-inventory-month-current]')?.addEventListener('click', () => setValue(formatValue(today.getFullYear(), today.getMonth() + 1)));
document.addEventListener('click', (event) => { if (!picker.contains(event.target)) close(); });
trigger.addEventListener('keydown', (event) => { if (event.key === 'Escape') close(); });
panel.addEventListener('keydown', (event) => { if (event.key === 'Escape') { close(); trigger.focus(); } });
});
document.querySelectorAll('[data-directory-query]').forEach((queryBar) => {
const section = queryBar.closest('[data-directory-page]');
const table = section?.querySelector('.directory-table');
if (!table) return;
const fields = [...queryBar.querySelectorAll('[data-directory-query-field]')];
const rows = () => [...table.querySelectorAll('tbody tr:not(.is-empty)')];
const fieldValues = (row, key) => {
if (key === 'related_people') {
return ['legal_representative', 'shareholders', 'supervisor', 'billing_officer']
.map((column) => (row.querySelector(`[data-column-key="${column}"]`)?.textContent || '').trim())
.filter((value) => value && value !== '—')
.join(' / ');
}
const cell = row.querySelector(`[data-column-key="${CSS.escape(key)}"]`);
return (cell?.textContent || '').trim();
};
const refreshSuggestions = (field, value) => {
const list = field.querySelector('[data-directory-query-suggestions]');
if (!list) return;
const key = field.dataset.directoryQueryField;
const needle = value.trim().toLocaleLowerCase();
const candidates = key === 'related_people'
? rows().flatMap((row) => ['legal_representative', 'shareholders', 'supervisor', 'billing_officer']
.map((column) => (row.querySelector(`[data-column-key="${column}"]`)?.textContent || '').trim()))
: rows().map((row) => fieldValues(row, key));
const values = [...new Set(candidates.filter((item) => item && item !== '—' && (!needle || item.toLocaleLowerCase().includes(needle))))].slice(0, 8);
list.innerHTML = values.map((item) => `<li><button type="button" data-directory-suggestion-value="${item.replaceAll('&','&amp;').replaceAll('\"','&quot;').replaceAll('<','&lt;').replaceAll('>','&gt;')}">${item.replaceAll('&','&amp;').replaceAll('<','&lt;').replaceAll('>','&gt;')}</button></li>`).join('');
list.hidden = values.length === 0;
list.querySelectorAll('button').forEach((button) => button.addEventListener('click', () => {
const input = field.querySelector('[data-directory-query-input]');
if (input) { input.value = button.dataset.directorySuggestionValue || ''; input.dispatchEvent(new Event('input', { bubbles: true })); input.focus(); }
list.hidden = true;
}));
};
fields.forEach((field) => {
const input = field.querySelector('[data-directory-query-input]');
const select = field.querySelector('[data-directory-query-select]');
const key = field.dataset.directoryQueryField;
const values = [...new Set(rows().map((row) => fieldValues(row, key)).filter((item) => item && item !== '—'))].sort((a,b) => a.localeCompare(b, 'zh-Hans-CN'));
if (select && select.options.length <= 1) { values.forEach((value) => { const option = document.createElement('option'); option.value = value; option.textContent = value; select.appendChild(option); }); }
input?.addEventListener('input', () => refreshSuggestions(field, input.value));
input?.addEventListener('focus', () => refreshSuggestions(field, input.value));
input?.addEventListener('blur', () => window.setTimeout(() => { const list = field.querySelector('[data-directory-query-suggestions]'); if (list) list.hidden = true; }, 150));
});
const columnChecks = [...queryBar.querySelectorAll('[data-directory-query-column]')];
const pickerSummary = queryBar.querySelector('[data-directory-query-picker] summary');
const applyVisibleColumns = () => {
const selected = columnChecks.filter((check) => check.checked).map((check) => check.value);
table.querySelectorAll('[data-column-key]').forEach((cell) => {
cell.classList.toggle('is-column-hidden', !selected.includes(cell.dataset.columnKey));
});
if (pickerSummary) pickerSummary.textContent = `筛选字段 ${selected.length}/${columnChecks.length}`;
let hiddenColumns = queryBar.querySelector(`input[form="${CSS.escape(queryBar.querySelector('form[id]')?.id || '')}"][name="columns"]`);
if (!hiddenColumns) {
hiddenColumns = document.createElement('input');
hiddenColumns.type = 'hidden';
hiddenColumns.name = 'columns';
hiddenColumns.setAttribute('form', queryBar.querySelector('form[id]')?.id || '');
queryBar.appendChild(hiddenColumns);
}
hiddenColumns.value = selected.join(',');
const url = new URL(window.location.href);
url.searchParams.set('columns', selected.join(','));
url.searchParams.delete('page');
window.history.replaceState({}, '', url);
};
columnChecks.forEach((check) => check.addEventListener('change', applyVisibleColumns));
section.querySelectorAll('[data-directory-header-hide]').forEach((button) => button.addEventListener('click', () => {
const key = button.dataset.directoryHeaderHide;
const check = columnChecks.find((item) => item.value === key);
if (!check) return;
check.checked = false;
check.dispatchEvent(new Event('change', { bubbles: true }));
button.closest('details')?.removeAttribute('open');
}));
queryBar.querySelector('[data-directory-query-reset]')?.addEventListener('click', () => {
const url = new URL(window.location.href);
fields.forEach((field) => {
const key = field.dataset.directoryQueryField;
url.searchParams.delete(key); url.searchParams.delete(`${key}_from`); url.searchParams.delete(`${key}_to`);
});
url.searchParams.delete('page');
window.location.assign(url);
});
queryBar.querySelector('[data-directory-query-export]')?.addEventListener('click', async (event) => {
const button = event.currentTarget;
const picker = button.closest('[data-directory-query-export-picker]');
const selected = new Set([...picker.querySelectorAll('[data-directory-query-export-field]:checked')].map((input) => input.value));
if (selected.size === 0) {
window.alert('请至少勾选一个导出字段');
return;
}
const headerCells = [...table.tHead.rows[0].querySelectorAll('[data-column-key]')]
.filter((cell) => selected.has(cell.dataset.columnKey));
const header = headerCells.map((cell) => csvEscape((cell.querySelector('[data-directory-header-label]') || cell).textContent.trim())).join(',');
const lines = [];
const lastPage = Number(section.dataset.directoryLastPage || 1);
button.disabled = true;
const originalLabel = button.textContent;
try {
for (let page = 1; page <= lastPage; page += 1) {
button.textContent = `导出中 ${page}/${lastPage}`;
const url = new URL(window.location.href);
url.searchParams.set('page', String(page));
const response = await fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } });
if (!response.ok) throw new Error(`第 ${page} 页读取失败`);
const parsed = new DOMParser().parseFromString(await response.text(), 'text/html');
parsed.querySelectorAll('.directory-table tbody tr:not(.is-empty)').forEach((row) => {
const values = headerCells.map((headerCell) => {
const cell = row.querySelector(`[data-column-key="${CSS.escape(headerCell.dataset.columnKey)}"]`);
return csvEscape((cell?.querySelector('[data-password-status]') || cell)?.textContent?.trim() || '');
});
lines.push(values.join(','));
});
}
const blob = new Blob([`\ufeff${[header, ...lines].join('\n')}`], { type: 'text/csv;charset=utf-8' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = `${section.dataset.directoryPage || 'directory'}-export.csv`;
link.click();
URL.revokeObjectURL(link.href);
picker.open = false;
} catch (error) {
window.alert(error.message || '导出失败,请稍后重试');
} finally {
button.disabled = false;
button.textContent = originalLabel;
}
});
});
document.querySelectorAll('[data-directory-tools]').forEach((tools) => {
const section = tools.closest('[data-directory-page]');
const tableScope = tools.closest('[data-directory-table-scope]');
const table = tableScope?.querySelector('.directory-table') || section?.querySelector('.directory-table');
const search = tools.querySelector('[data-directory-search]');
if (!table || !search) return;
table.addEventListener('click', (event) => {
const shortNameSort = event.target instanceof Element
? event.target.closest('[data-inventory-short-name-sort]')
: null;
if (!shortNameSort) return;
const header = shortNameSort.closest('th');
const ascending = header.getAttribute('aria-sort') !== 'ascending';
const rows = [...table.tBodies[0].querySelectorAll('tr:not(.is-empty)')];
rows.sort((left, right) => {
const a = left.querySelector('[data-column-key="warehouse_short_name"]')?.textContent.trim() || '';
const b = right.querySelector('[data-column-key="warehouse_short_name"]')?.textContent.trim() || '';
if (!a) return b ? 1 : 0;
if (!b) return -1;
return a.localeCompare(b, 'zh-Hans-CN') * (ascending ? 1 : -1);
}).forEach((row) => table.tBodies[0].appendChild(row));
header.setAttribute('aria-sort', ascending ? 'ascending' : 'descending');
shortNameSort.setAttribute('aria-label', `仓库简称按当前页${ascending ? '降序' : '升序'}排序`);
shortNameSort.querySelector('.directory-sort-icon').textContent = ascending ? 'arrow_upward' : 'arrow_downward';
});
const picker = tools.querySelector('[data-directory-field-picker]');
const toggle = picker?.querySelector('[data-directory-field-toggle]');
const panel = picker?.querySelector('.directory-field-picker__panel');
const checks = [...(picker?.querySelectorAll('[data-directory-field-check]') || [])];
const tableKey = tableScope?.dataset.directoryTableKey || section?.dataset.directoryTab || 'table';
const sourceUrl = tableScope?.dataset.directorySourceUrl || window.location.href;
const lastPage = Number(tableScope?.dataset.directoryLastPage || section?.dataset.directoryLastPage || 1);
const storageKey = [
'directory-fields',
section.dataset.directoryUser || 'guest',
section.dataset.directorySection || 'section',
section.dataset.directoryPage || 'page',
tableKey,
].join(':');
const defaultOrder = checks.map((check) => check.value);
const orderedChecks = () => [...(picker?.querySelectorAll('[data-directory-field-option]') || [])]
.map((option) => option.querySelector('[data-directory-field-check]'))
.filter(Boolean);
const orderedExportCells = (row, selected) => {
const cells = [...row.children];
const fixedStart = cells.filter((cell) => cell.dataset.directoryFixed === 'start');
const rank = new Map(orderedChecks().map((check, index) => [check.value, index]));
const keyed = cells
.filter((cell) => cell.dataset.columnKey && (!selected || selected.has(cell.dataset.columnKey)))
.sort((a, b) => (rank.get(a.dataset.columnKey) ?? 0) - (rank.get(b.dataset.columnKey) ?? 0));
const other = cells.filter((cell) => !cell.dataset.columnKey && !cell.dataset.directoryFixed);
return [...fixedStart, ...keyed, ...other];
};
const applyColumns = (save = true) => {
const ordered = orderedChecks();
const selected = new Set(ordered.filter((check) => check.checked).map((check) => check.value));
const rank = new Map(ordered.map((check, index) => [check.value, index]));
table.dataset.columnsReady = 'true';
table.querySelectorAll('[data-column-key]').forEach((cell) => {
cell.classList.toggle('is-column-hidden', !selected.has(cell.dataset.columnKey));
});
const reorderRow = (row) => {
const cells = [...row.children];
const fixedStart = cells.filter((cell) => cell.dataset.directoryFixed === 'start');
const fixedEnd = cells.filter((cell) => cell.dataset.directoryFixed === 'end');
const keyed = cells
.filter((cell) => cell.dataset.columnKey)
.sort((a, b) => (rank.get(a.dataset.columnKey) ?? 0) - (rank.get(b.dataset.columnKey) ?? 0));
const other = cells.filter((cell) => !cell.dataset.columnKey && !cell.dataset.directoryFixed);
[...fixedStart, ...keyed, ...fixedEnd, ...other].forEach((cell) => row.appendChild(cell));
};
[...table.tHead.rows].forEach((row) => {
reorderRow(row);
});
table.tBodies[0]?.querySelectorAll('tr').forEach((row) => {
reorderRow(row);
});
if (save) localStorage.setItem(storageKey, JSON.stringify({ order: ordered.map((check) => check.value), selected: [...selected] }));
};
try {
const saved = JSON.parse(localStorage.getItem(storageKey) || 'null');
const savedOrder = Array.isArray(saved) ? saved : saved?.order;
const savedSelected = Array.isArray(saved) ? saved : saved?.selected;
if (table.id === 'inventory-report-total-table' && !localStorage.getItem(`${storageKey}:warehouse-short-name-v1`)) {
if (Array.isArray(savedOrder) && !savedOrder.includes('warehouse_short_name')) {
savedOrder.splice(savedOrder.indexOf('warehouse') + 1, 0, 'warehouse_short_name');
}
if (Array.isArray(savedSelected) && !savedSelected.includes('warehouse_short_name')) {
savedSelected.push('warehouse_short_name');
}
if (saved) localStorage.setItem(storageKey, JSON.stringify({ order: savedOrder, selected: savedSelected }));
localStorage.setItem(`${storageKey}:warehouse-short-name-v1`, '1');
}
if (Array.isArray(savedSelected)) checks.forEach((check) => { check.checked = savedSelected.includes(check.value); });
if (Array.isArray(savedOrder)) savedOrder.forEach((key) => {
const option = picker.querySelector(`[data-directory-field-option] input[value="${CSS.escape(key)}"]`)?.closest('[data-directory-field-option]');
if (option) picker.querySelector('.directory-field-picker__panel').appendChild(option);
});
} catch (_) {}
applyColumns(false);
toggle?.addEventListener('click', () => {
const isHidden = panel.hidden;
panel.hidden = !isHidden;
toggle.setAttribute('aria-expanded', String(isHidden));
});
checks.forEach((check) => check.addEventListener('change', () => applyColumns()));
let dragging = null;
picker?.querySelectorAll('[data-directory-field-option]').forEach((option) => {
option.addEventListener('dragstart', () => { dragging = option; option.classList.add('is-dragging'); });
option.addEventListener('dragend', () => { dragging = null; option.classList.remove('is-dragging'); picker.querySelectorAll('.is-drop-target').forEach((item) => item.classList.remove('is-drop-target')); applyColumns(); });
option.addEventListener('dragover', (event) => { event.preventDefault(); if (dragging && dragging !== option) option.classList.add('is-drop-target'); });
option.addEventListener('dragleave', () => option.classList.remove('is-drop-target'));
option.addEventListener('drop', (event) => { event.preventDefault(); if (!dragging || dragging === option) return; option.parentNode.insertBefore(dragging, option); option.classList.remove('is-drop-target'); applyColumns(); });
});
const resetColumns = () => {
const options = [...picker.querySelectorAll('[data-directory-field-option]')];
const byKey = new Map(options.map((option) => [option.querySelector('[data-directory-field-check]').value, option]));
defaultOrder.forEach((key) => picker.querySelector('.directory-field-picker__panel').appendChild(byKey.get(key)));
checks.forEach((check) => { check.checked = !['sales_01', 'sales_02', 'sales_03', 'sales_04', 'sales_05', 'sales_06'].includes(check.value); });
applyColumns();
};
const resetButton = tools.querySelector('[data-directory-reset]');
if (resetButton) resetButton.onclick = resetColumns;
tools.querySelector('[data-inventory-export]')?.addEventListener('click', (event) => {
const url = new URL(event.currentTarget.dataset.exportUrl, window.location.origin);
const current = new URL(window.location.href);
['month', 'product_code', 'warehouse_provider', 'category'].forEach((key) => {
if (current.searchParams.has(key)) url.searchParams.set(key, current.searchParams.get(key));
});
const scope = tools.querySelector('[data-inventory-export-scope]').value;
url.searchParams.set('scope', scope);
if (scope === 'page') url.searchParams.set('page', current.searchParams.get('page') || '1');
if (tools.querySelector('[data-inventory-export-fields]').value === 'visible') {
const columns = [...table.tHead.querySelectorAll('th[data-column-key]')]
.filter((cell) => !cell.classList.contains('is-column-hidden'))
.map((cell) => cell.dataset.columnKey);
if (!columns.length) {
window.alert('请至少选择一个显示列。');
return;
}
url.searchParams.set('columns', columns.join(','));
}
const link = document.createElement('a');
link.href = url.toString();
document.body.appendChild(link);
link.click();
link.remove();
});
search.addEventListener('input', () => {
const query = search.value.trim().toLocaleLowerCase();
table.querySelectorAll('tbody tr').forEach((row) => {
row.hidden = Boolean(query) && !row.textContent.toLocaleLowerCase().includes(query);
});
});
tools.querySelectorAll('[data-directory-export]').forEach((button) => button.addEventListener('click', () => {
const selected = button.dataset.directoryExport === 'all-fields' ? null : new Set(orderedChecks().filter((check) => check.checked).map((check) => check.value));
const rows = [...table.querySelectorAll('tr')].filter((row) => !row.hidden);
const csv = rows.map((row) => orderedExportCells(row, selected).map((cell) => csvEscape((cell.querySelector('[data-password-status]') || cell).textContent.trim())).join(',')).join('\n');
const blob = new Blob([`\ufeff${csv}`], { type: 'text/csv;charset=utf-8' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = `${section.dataset.directoryPage || 'directory'}-current-export.csv`;
link.click();
URL.revokeObjectURL(link.href);
}));
tools.querySelectorAll('[data-directory-export-all]').forEach((button) => button.addEventListener('click', async (event) => {
const button = event.currentTarget;
const selected = button.dataset.directoryExportAll === 'all-fields' ? null : new Set(orderedChecks().filter((check) => check.checked).map((check) => check.value));
const header = orderedExportCells(table.tHead.rows[0], selected).map((cell) => csvEscape(cell.textContent.trim())).join(',');
const lines = [];
button.disabled = true;
const originalLabel = button.textContent;
try {
for (let page = 1; page <= lastPage; page += 1) {
button.textContent = '导出中 ' + page + '/' + lastPage;
const url = new URL(sourceUrl, window.location.origin);
url.searchParams.set('page', String(page));
const response = await fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } });
if (!response.ok) throw new Error(`第 ${page} 页读取失败`);
const html = await response.text();
const parsed = new DOMParser().parseFromString(html, 'text/html');
const parsedTable = table.id
? parsed.querySelector(`#${CSS.escape(table.id)}`)
: parsed.querySelector('.directory-table');
parsedTable?.querySelectorAll('tbody tr:not(.is-empty)').forEach((row) => {
lines.push(orderedExportCells(row, selected).map((cell) => csvEscape(cell.textContent.trim())).join(','));
});
}
const csv = [header, ...lines].join('\n');
const blob = new Blob([`\ufeff${csv}`], { type: 'text/csv;charset=utf-8' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = `${section.dataset.directoryPage || 'directory'}-all-export.csv`;
link.click();
URL.revokeObjectURL(link.href);
} catch (error) {
window.alert(error.message || '导出全部数据失败,请稍后重试');
} finally {
button.disabled = false;
button.textContent = originalLabel;
}
}));
tools.querySelector('[data-directory-template]')?.addEventListener('click', () => {
const header = [...table.querySelectorAll('thead th')].map((cell) => csvEscape(cell.textContent.trim())).join(',');
const blob = new Blob([`\ufeff${header}\n`], { type: 'text/csv;charset=utf-8' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = `${section.dataset.directoryPage || 'directory'}-template.csv`;
link.click();
URL.revokeObjectURL(link.href);
});
tools.querySelector('[data-inventory-mapping-template]')?.addEventListener('click', () => {
const header = ['仓储方', '仓库', '仓库简称'].map(csvEscape).join(',');
const blob = new Blob(['\ufeff' + header + '\n'], { type: 'text/csv;charset=utf-8' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = '仓库名称映射表-导入模板.csv';
link.click();
URL.revokeObjectURL(link.href);
});
});
document.querySelectorAll('[data-inventory-report-tabs]').forEach((tabs) => {
const report = tabs.closest('[data-directory-page]');
const buttons = [...tabs.querySelectorAll('[data-inventory-report-tab]')];
const panels = [...(report?.querySelectorAll('[data-inventory-report-panel]') || [])];
if (!report || !buttons.length || !panels.length) return;
const select = (name, focus = false) => {
buttons.forEach((button) => {
const selected = button.dataset.inventoryReportTab === name;
button.setAttribute('aria-selected', String(selected));
button.tabIndex = selected ? 0 : -1;
if (focus && selected) button.focus();
});
panels.forEach((panel) => {
panel.hidden = panel.dataset.inventoryReportPanel !== name;
});
};
buttons.forEach((button, index) => {
button.addEventListener('click', () => select(button.dataset.inventoryReportTab, true));
button.addEventListener('keydown', (event) => {
if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return;
event.preventDefault();
const nextIndex = event.key === 'Home' ? 0
: event.key === 'End' ? buttons.length - 1
: (index + (event.key === 'ArrowRight' ? 1 : -1) + buttons.length) % buttons.length;
select(buttons[nextIndex].dataset.inventoryReportTab, true);
});
});
select(new URLSearchParams(window.location.search).get('mapping') === '1' ? 'mapping' : 'total');
});
document.querySelectorAll('[data-inventory-mapping-dialog]').forEach((dialog) => {
const form = dialog.querySelector('[data-inventory-mapping-form]');
const method = form?.querySelector('[data-inventory-mapping-method]');
const title = dialog.querySelector('[data-inventory-mapping-dialog-title]');
const provider = form?.querySelector('[data-inventory-mapping-provider]');
const warehouse = form?.querySelector('[data-inventory-mapping-warehouse]');
const shortName = form?.querySelector('[data-inventory-mapping-short-name]');
const storeUrl = @json(route('business.product-inventory.inventory-report.warehouse-mappings.store'));
const updateUrl = @json(route('business.product-inventory.inventory-report.warehouse-mappings.update', ['id' => '__ID__']));
if (!form || !method || !title || !provider || !warehouse || !shortName) return;
const suggest = () => {
if (shortName.dataset.edited === 'true') return;
const text = `${provider.value} ${warehouse.value}`;
let value = '';
const special = [
['代发仓', '代发仓'], ['京东云仓', '京东云仓-不看'], ['销退仓', '销退仓-不看'],
['客退', '客退仓-不看'], ['次品仓', '退货报废-不看'], ['数量校验仓', '数量校验仓-不看'],
['自动发货仓', '虚拟仓-不看'], ['京东京造', '京东京造-不看'], ['POP仓', '京东入仓-不看'],
['江西仓送货', '南昌樵舍仓'], ['运营C崔桥工厂仓', '崔桥工厂仓'], ['运营C横林二仓', '横林二仓'],
['运营C睢宁', '睢宁仓'], ['运营C新苏北', '新苏北仓'],
];
for (const [needle, result] of special) if (text.includes(needle)) { value = result; break; }
if (!value) {
value = warehouse.value.replace(/(主仓|进货仓)$/u, '').replace(/^(简尚-|京仓-|三方-|奇门-)/u, '');
const providerBase = provider.value.replace(/^(简尚-|京仓-|三方-|奇门-)/u, '');
if (value === providerBase || value.startsWith(providerBase)) value = providerBase;
}
shortName.value = value;
};
provider.addEventListener('input', suggest);
warehouse.addEventListener('input', suggest);
shortName.addEventListener('input', () => { shortName.dataset.edited = 'true'; });
dialog.querySelector('[data-inventory-mapping-cancel]')?.addEventListener('click', () => dialog.close());
document.querySelector('[data-inventory-mapping-create]')?.addEventListener('click', () => {
title.textContent = '新增仓库名称映射';
form.action = storeUrl;
method.value = 'POST';
provider.value = '';
warehouse.value = '';
shortName.value = '';
shortName.dataset.edited = 'false';
dialog.showModal();
provider.focus();
});
document.querySelectorAll('[data-inventory-mapping-edit]').forEach((button) => button.addEventListener('click', () => {
title.textContent = '修改仓库名称映射';
form.action = updateUrl.replace('__ID__', button.dataset.id);
method.value = 'PATCH';
provider.value = button.dataset.provider || '';
warehouse.value = button.dataset.warehouse || '';
shortName.value = button.dataset.shortName || '';
shortName.dataset.edited = 'true';
dialog.showModal();
provider.focus();
}));
dialog.addEventListener('click', (event) => { if (event.target === dialog) dialog.close(); });
});
document.querySelectorAll('[data-subject-row]').forEach((row) => {
const section = row.closest('[data-directory-page]');
const panel = section?.querySelector('[data-subject-details-panel]');
const body = panel?.querySelector('[data-subject-details-body]');
const name = panel?.querySelector('[data-subject-details-name]');
if (!panel || !body || !name) return;
const render = () => {
document.querySelectorAll('[data-subject-row]').forEach((item) => item.removeAttribute('aria-selected'));
row.setAttribute('aria-selected', 'true');
let details = [];
try { details = JSON.parse(row.dataset.subjectDetails || '[]'); } catch (_) { details = []; }
const entity = row.children[2]?.textContent.trim() || '当前主体';
name.textContent = entity;
panel.hidden = false;
if (!details.length) {
body.textContent = '暂无匹配的主体基础资料。';
return;
}
const labels = {
unified_social_credit_code: '统一社会信用代码', company_name: '公司名称', taxpayer_category: '纳税人类型',
company_status: '公司状态', subscribed_capital: '认缴注册资本(万元)', establishment_date: '注册时间',
legal_representative: '法人', shareholders: '股东', supervisor: '监事', financial_officer: '开票员',
registered_address: '注册地址', business_scope: '经营范围', basic_deposit_account_number: '基本存款账户信息', subscribed_at: '认缴时间',
};
body.innerHTML = details.map((detail) => `<dl class="directory-subject-details__record">${Object.entries(labels).map(([key, label]) => {
const value = detail[key];
const display = value === null || value === undefined || value === '' ? '—' : (key === 'basic_deposit_account_number' ? '已配置' : String(value));
return `<div><dt>${label}</dt><dd>${display.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;')}</dd></div>`;
}).join('')}</dl>`).join('');
};
row.addEventListener('click', render);
row.addEventListener('keydown', (event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); render(); } });
});
})();
</script>
@endonce