CaiWuXiTong/resources/views/business/pages/courier-payables/third-party-warehouse-storage.blade.php

520 lines
32 KiB
PHP

@php
$rows = $thirdPartyWarehouseRows ?? null;
$rawFilters = is_array($thirdPartyWarehouseFilters ?? null) ? $thirdPartyWarehouseFilters : [];
$dimension = in_array(($rawFilters['dimension'] ?? 'month'), ['month', 'day'], true)
? $rawFilters['dimension']
: 'month';
$period = trim((string) ($rawFilters['period'] ?? (
$dimension === 'day' ? ($rawFilters['date'] ?? '') : ($rawFilters['month'] ?? '')
)));
$filters = [
'dimension' => $dimension,
'period' => $period,
'sku_code' => trim((string) ($rawFilters['sku_code'] ?? '')),
'product_name' => trim((string) ($rawFilters['product_name'] ?? '')),
'supplier' => trim((string) ($rawFilters['supplier'] ?? '')),
'category' => trim((string) ($rawFilters['category'] ?? '')),
];
$summary = $thirdPartyWarehouseSummary ?? [];
$sync = array_replace([
'code' => 'unavailable',
'last_succeeded_at' => null,
'lag_minutes' => null,
'message' => null,
], $thirdPartyWarehouseSync ?? []);
$error = trim((string) ($thirdPartyWarehouseError ?? ''));
$pending = (bool) ($thirdPartyWarehousePending ?? false);
$exportUrl = trim((string) ($thirdPartyWarehouseExportUrl ?? ''));
$columns = [
['key' => 'owner', 'label' => '货主'],
['key' => 'date', 'label' => '日期'],
['key' => 'sku_code', 'label' => '商品编码', 'type' => 'code'],
['key' => 'image_url', 'label' => '图片', 'type' => 'image'],
['key' => 'product_name', 'label' => '商品名称', 'type' => 'wide'],
['key' => 'short_name', 'label' => '商品简称'],
['key' => 'supplier', 'label' => '供应商', 'type' => 'wide'],
['key' => 'category', 'label' => '分类'],
['key' => 'virtual_category', 'label' => '虚拟分类'],
['key' => 'supplier_style_no', 'label' => '供应商款号', 'type' => 'code'],
['key' => 'color_spec', 'label' => '颜色规格'],
['key' => 'brand', 'label' => '品牌'],
['key' => 'unit', 'label' => '单位'],
['key' => 'opening_qty', 'label' => '期初数量', 'type' => 'number'],
['key' => 'closing_qty', 'label' => '期末数量', 'type' => 'number'],
['key' => 'processing_in', 'label' => '加工进仓', 'type' => 'number'],
['key' => 'processing_out', 'label' => '加工出仓', 'type' => 'number'],
['key' => 'purchase_in', 'label' => '采购进仓', 'type' => 'number'],
['key' => 'purchase_return', 'label' => '采购退货', 'type' => 'number'],
['key' => 'other_in', 'label' => '其它进仓', 'type' => 'number'],
['key' => 'other_return', 'label' => '其它退货', 'type' => 'number'],
['key' => 'sales_out', 'label' => '销售出仓', 'type' => 'number'],
['key' => 'sales_return', 'label' => '销售退货', 'type' => 'number'],
['key' => 'stocktake', 'label' => '盘点', 'type' => 'number'],
['key' => 'transfer_out', 'label' => '调拨出', 'type' => 'number'],
['key' => 'transfer_in', 'label' => '调拨入', 'type' => 'number'],
['key' => 'opening_aux', 'label' => '期初', 'type' => 'number'],
['key' => 'other_out', 'label' => '其它出库', 'type' => 'number'],
];
$statusLabels = [
'healthy' => '同步正常',
'fresh' => '同步正常',
'syncing' => '同步中,显示最近数据',
'reconciling' => '全量校准中,显示最近数据',
'stale' => '同步延迟,显示最近数据',
'delayed' => '同步延迟,显示最近数据',
'failed' => '同步失败,显示最近数据',
'initializing' => '首次同步中',
'dependency_pending' => '商品资料同步中',
'pending' => '等待首次同步',
'initial_failed' => '首次同步失败',
'unavailable' => '镜像不可用',
];
$statusCode = (string) ($sync['code'] ?? 'unavailable');
$statusLabel = $statusLabels[$statusCode] ?? '同步状态未知';
$statusTone = match ($statusCode) {
'failed', 'initial_failed', 'unavailable' => 'danger',
'stale', 'delayed', 'initializing', 'dependency_pending', 'pending' => 'warning',
'syncing', 'reconciling' => 'info',
default => 'success',
};
$rowTotal = is_object($rows) && method_exists($rows, 'total')
? (int) $rows->total()
: (is_countable($rows) ? count($rows) : 0);
$rowCount = is_object($rows) && method_exists($rows, 'count')
? (int) $rows->count()
: (is_countable($rows) ? count($rows) : 0);
$summaryReady = ! $pending && collect(['count', 'opening_qty', 'closing_qty'])
->every(static fn (string $key): bool => array_key_exists($key, $summary));
$showingLastVersion = $rowTotal > 0 && ($error !== '' || in_array($statusCode, [
'syncing', 'reconciling', 'stale', 'delayed', 'failed',
], true));
$canExport = $exportUrl !== '' && $rowTotal > 0 && ! $pending;
$activeFilters = array_filter(
$filters,
static fn (mixed $value): bool => $value !== null && $value !== ''
);
if (is_object($rows) && method_exists($rows, 'appends')) {
$rows->appends($activeFilters);
}
$fallbackExportUrl = $exportUrl;
if ($exportUrl !== '' && $activeFilters !== []) {
$fallbackExportUrl .= (str_contains($exportUrl, '?') ? '&' : '?') . http_build_query($activeFilters);
}
$text = static function (mixed $value): string {
if ($value === null || $value === '') {
return '-';
}
if (is_bool($value)) {
return $value ? '是' : '否';
}
if (is_scalar($value)) {
return (string) $value;
}
return json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '-';
};
$number = static function (mixed $value) use ($text): string {
if ($value === null || $value === '') {
return '-';
}
if (! is_numeric($value)) {
return $text($value);
}
return rtrim(rtrim(number_format((float) $value, 4, '.', ','), '0'), '.');
};
$money = static function (mixed $value) use ($text): string {
if ($value === null || $value === '') {
return '-';
}
return is_numeric($value) ? number_format((float) $value, 2, '.', ',') : $text($value);
};
$imageUrl = static function (mixed $value): ?string {
if (is_array($value)) {
$value = $value['url'] ?? $value['src'] ?? reset($value);
}
if (! is_string($value)) {
return null;
}
$candidate = trim($value);
if (str_starts_with($candidate, '[') || str_starts_with($candidate, '{')) {
$decoded = json_decode($candidate, true);
if (is_array($decoded)) {
$candidate = trim((string) ($decoded['url'] ?? $decoded['src'] ?? reset($decoded)));
}
}
$scheme = strtolower((string) parse_url($candidate, PHP_URL_SCHEME));
return filter_var($candidate, FILTER_VALIDATE_URL) && in_array($scheme, ['http', 'https'], true)
? $candidate
: null;
};
$lagMinutes = is_numeric($sync['lag_minutes'] ?? null) ? max(0, (int) $sync['lag_minutes']) : null;
$lagLabel = $lagMinutes === null
? null
: ($lagMinutes < 60 ? $lagMinutes . ' 分钟' : floor($lagMinutes / 60) . ' 小时 ' . ($lagMinutes % 60) . ' 分钟');
@endphp
<div class="third-party-warehouse-page" data-finance-table-surface>
<style>
.third-party-warehouse-page {
--warehouse-border: var(--vip-border, #D8D2C7);
--warehouse-ink: var(--vip-content-ink, #171717);
--warehouse-muted: var(--vip-content-muted, #595959);
--warehouse-accent: var(--vip-content-accent-dark, #856632);
--warehouse-soft: var(--vip-content-accent-soft, #F7F0DE);
display: flex;
flex-direction: column;
gap: .75rem;
margin: 1.5rem 0;
color: var(--warehouse-ink);
font-size: .8125rem;
}
.third-party-warehouse-status { display:flex; flex-wrap:wrap; align-items:center; justify-content:space-between; gap:.75rem 1rem; min-height:2.625rem; padding:.5rem .75rem; border:1px solid var(--warehouse-border); border-radius:.5rem; background:var(--warehouse-soft); }
.third-party-warehouse-status__primary,.third-party-warehouse-status__meta { display:flex; flex-wrap:wrap; align-items:center; gap:.5rem .75rem; }
.third-party-warehouse-status__primary { font-weight:600; }
.third-party-warehouse-status__meta { color:var(--warehouse-muted); font-size:.75rem; }
.third-party-warehouse-status__dot { width:.5rem; height:.5rem; flex:0 0 auto; border-radius:50%; background:var(--vip-success,#53793E); }
.third-party-warehouse-status__dot.is-warning { background:var(--vip-warning,#935F07); }
.third-party-warehouse-status__dot.is-danger { background:var(--vip-danger,#B80000); }
.third-party-warehouse-status__dot.is-info { background:var(--vip-info,#006395); }
.third-party-warehouse-alert { display:flex; align-items:flex-start; gap:.5rem; padding:.625rem .75rem; border:1px solid rgba(184,0,0,.35); border-radius:.5rem; background:#fff5f5; color:var(--vip-danger,#B80000); }
.third-party-warehouse-notice { display:flex; align-items:flex-start; gap:.5rem; padding:.625rem .75rem; border:1px solid rgba(147,95,7,.35); border-radius:.5rem; background:#fffaf0; color:var(--vip-warning,#935F07); }
.third-party-warehouse-alert .material-icons-outlined,.third-party-warehouse-notice .material-icons-outlined { margin-top:.05rem; font-size:1.1rem; }
.third-party-warehouse-filters { display:flex; flex-wrap:wrap; align-items:flex-end; gap:.65rem; padding:.75rem; border:1px solid var(--warehouse-border); border-radius:.5rem; background:var(--vip-surface,#fff); }
.third-party-warehouse-field { display:flex; min-width:8.75rem; flex:1 1 10rem; flex-direction:column; gap:.3rem; }
.third-party-warehouse-field:first-of-type { flex:0 1 9.5rem; }
.third-party-warehouse-field label { color:var(--warehouse-muted); font-size:.75rem; font-weight:600; }
.third-party-warehouse-field input { width:100%; height:2.125rem; padding:0 .625rem; border:1px solid var(--warehouse-border); border-radius:.5rem; background:#fff; color:var(--warehouse-ink); font:inherit; }
.third-party-warehouse-field input:focus { border-color:var(--warehouse-accent); outline:none; box-shadow:0 0 0 3px rgba(133,102,50,.2); }
.third-party-warehouse-dimension { display:flex; min-width:10.75rem; flex:0 0 auto; flex-direction:column; gap:.3rem; margin:0; padding:0; border:0; }
.third-party-warehouse-dimension__label { color:var(--warehouse-muted); font-size:.75rem; font-weight:600; }
.third-party-warehouse-segmented { display:inline-grid; height:2.125rem; grid-template-columns:repeat(2,1fr); overflow:hidden; border:1px solid var(--warehouse-border); border-radius:.5rem; background:#fff; }
.third-party-warehouse-segmented label { position:relative; display:flex; min-width:5rem; align-items:center; justify-content:center; padding:0 .65rem; color:var(--warehouse-muted); cursor:pointer; font-size:.75rem; font-weight:600; }
.third-party-warehouse-segmented label + label { border-left:1px solid var(--warehouse-border); }
.third-party-warehouse-segmented input { position:absolute; width:1px; height:1px; overflow:hidden; opacity:0; pointer-events:none; }
.third-party-warehouse-segmented label:has(input:checked) { background:var(--warehouse-accent); color:#fff; }
.third-party-warehouse-segmented label:has(input:focus-visible) { outline:2px solid var(--warehouse-accent); outline-offset:-3px; }
.third-party-warehouse-actions { display:flex; flex:1 1 29rem; flex-wrap:wrap; align-items:center; justify-content:flex-end; gap:.4rem; }
.third-party-warehouse-button,.third-party-warehouse-actions [data-finance-server-export] { display:inline-flex; height:2.125rem; align-items:center; justify-content:center; gap:.3rem; padding:0 .7rem; border:1px solid var(--warehouse-border); border-radius:.5rem; background:#fff; color:var(--warehouse-ink); cursor:pointer; font:inherit; font-weight:600; line-height:1; text-decoration:none; white-space:nowrap; }
.third-party-warehouse-button:hover,.third-party-warehouse-actions [data-finance-server-export]:hover { border-color:var(--warehouse-accent); background:var(--warehouse-soft); }
.third-party-warehouse-button:focus-visible,.third-party-warehouse-actions [data-finance-server-export]:focus-visible { border-color:var(--warehouse-accent); outline:none; box-shadow:0 0 0 3px rgba(133,102,50,.2); }
.third-party-warehouse-button.is-primary { border-color:var(--warehouse-accent); background:var(--warehouse-accent); color:#fff; }
.third-party-warehouse-button.is-primary:hover { background:#6A5129; }
.third-party-warehouse-button.is-disabled,.third-party-warehouse-actions [data-finance-server-export][disabled] { opacity:.5; cursor:not-allowed; pointer-events:none; }
.third-party-warehouse-button .material-icons-outlined,.third-party-warehouse-actions .material-icons-outlined { font-size:1rem; }
.third-party-warehouse-actions [data-finance-server-export] { display:none; }
.third-party-warehouse-page[data-finance-enhanced] .third-party-warehouse-actions [data-finance-server-export] { display:inline-flex; }
.third-party-warehouse-page .finance-table-field-picker__panel { right:0; left:auto; }
.third-party-warehouse-summary { display:grid; grid-template-columns:repeat(3,minmax(8rem,1fr)); overflow:hidden; border:1px solid var(--warehouse-border); border-radius:.5rem; background:#fff; }
.third-party-warehouse-summary__item { min-width:0; padding:.625rem .75rem; border-right:1px solid var(--warehouse-border); }
.third-party-warehouse-summary__item:last-child { border-right:0; }
.third-party-warehouse-summary__label { display:block; color:var(--warehouse-muted); font-size:.7rem; }
.third-party-warehouse-summary__value { display:block; margin-top:.15rem; overflow:hidden; color:#4B3A24; font-size:.9375rem; font-variant-numeric:tabular-nums; font-weight:700; text-overflow:ellipsis; white-space:nowrap; }
.third-party-warehouse-table-panel { overflow:hidden; border:1px solid var(--warehouse-border); border-radius:.5rem; background:#fff; }
.third-party-warehouse-table-scroll { width:100%; height:min(56vh,42rem); min-height:16rem; overflow:auto; overscroll-behavior:contain; scrollbar-color:var(--vip-content-accent,#B9954F) #EFEDE8; scrollbar-gutter:stable both-edges; scrollbar-width:thin; }
.third-party-warehouse-table-scroll::-webkit-scrollbar { width:.5rem; height:.5rem; }
.third-party-warehouse-table-scroll::-webkit-scrollbar-thumb { border-radius:.25rem; background:var(--vip-content-accent,#B9954F); }
.third-party-warehouse-table-scroll::-webkit-scrollbar-track { background:#EFEDE8; }
.third-party-warehouse-table { width:max-content; min-width:100%; border-collapse:separate; border-spacing:0; font-size:.78125rem; }
.third-party-warehouse-table th,.third-party-warehouse-table td { min-width:7.25rem; max-width:15rem; padding:.5rem .625rem; border-right:1px solid var(--warehouse-border); border-bottom:1px solid #EEEAE3; background:#fff; line-height:1.45; text-align:left; vertical-align:middle; white-space:nowrap; }
.third-party-warehouse-table th { position:sticky; top:0; z-index:4; border-bottom-color:var(--warehouse-border); background:var(--warehouse-soft); color:#4F4A43; font-size:.75rem; font-weight:600; }
.third-party-warehouse-table tbody tr:hover td { background:#FCFAF5; }
.third-party-warehouse-table .is-number { text-align:right; font-variant-numeric:tabular-nums; }
.third-party-warehouse-table .is-wide { min-width:14rem; max-width:20rem; overflow:hidden; text-overflow:ellipsis; }
.third-party-warehouse-table .is-code { min-width:10rem; font-variant-numeric:tabular-nums; }
.third-party-warehouse-table .is-image { min-width:4.75rem; width:4.75rem; text-align:center; }
.third-party-warehouse-thumb { display:block; width:2.5rem; height:2.5rem; margin:auto; border:1px solid var(--warehouse-border); border-radius:.25rem; background:#F8F6F1; object-fit:cover; }
.third-party-warehouse-empty { height:15rem; color:var(--warehouse-muted); text-align:center !important; white-space:normal !important; }
.third-party-warehouse-pagination { display:flex; min-height:2.75rem; align-items:center; justify-content:space-between; gap:.75rem; padding:.5rem .75rem; border-top:1px solid var(--warehouse-border); color:var(--warehouse-muted); font-size:.75rem; }
.third-party-warehouse-pagination__controls { display:flex; align-items:center; gap:.4rem; }
@media (max-width: 960px) {
.third-party-warehouse-summary { grid-template-columns:repeat(2,minmax(0,1fr)); }
.third-party-warehouse-summary__item { border-bottom:1px solid var(--warehouse-border); }
.third-party-warehouse-actions { justify-content:flex-start; }
}
@media (max-width: 640px) {
.third-party-warehouse-field { flex-basis:100%; }
.third-party-warehouse-actions { flex-basis:100%; }
.third-party-warehouse-summary { grid-template-columns:1fr; }
.third-party-warehouse-summary__item { border-right:0; }
.third-party-warehouse-table-scroll { height:52vh; min-height:14rem; }
.third-party-warehouse-pagination { align-items:flex-start; flex-direction:column; }
.third-party-warehouse-page .finance-table-field-picker__panel { right:auto; left:0; }
}
</style>
<div class="third-party-warehouse-status" role="status" aria-live="polite">
<div class="third-party-warehouse-status__primary">
<span @class(['third-party-warehouse-status__dot', 'is-' . $statusTone]) aria-hidden="true"></span>
<span>{{ $statusLabel }}</span>
@if ($showingLastVersion)
<span>· 当前展示最近一次成功同步的数据</span>
@endif
</div>
<div class="third-party-warehouse-status__meta">
<span>三方-南昌樵舍仓 · 查询维度:商品 + 按天</span>
<span>每日 00:00 自动同步</span>
@if (! empty($sync['last_succeeded_at']))
<span>最近成功同步 {{ $sync['last_succeeded_at'] }}</span>
@endif
@if ($lagLabel !== null)
<span>延迟 {{ $lagLabel }}</span>
@endif
@if ($error === '' && ! empty($sync['message']))
<span>{{ $sync['message'] }}</span>
@endif
</div>
</div>
@if ($error !== '')
<div class="third-party-warehouse-alert" role="alert">
<span class="material-icons-outlined" aria-hidden="true">error_outline</span>
<span>
@if ($showingLastVersion)
本次同步或读取异常,已保留上一版数据:{{ $error }}
@elseif ($pending)
首次同步未完成:{{ $error }}
@else
数据暂时无法读取:{{ $error }}
@endif
</span>
</div>
@endif
@if ($pending)
<div class="third-party-warehouse-notice" role="status">
<span class="material-icons-outlined" aria-hidden="true">schedule</span>
<span>首次同步尚未完成,当前没有可展示的正式镜像数据;同步成功后页面会自动使用镜像数据。</span>
</div>
@endif
<form method="GET" class="third-party-warehouse-filters" aria-label="三方仓进销存筛选" data-finance-controls>
<fieldset class="third-party-warehouse-dimension">
<legend class="third-party-warehouse-dimension__label">查询维度</legend>
<div class="third-party-warehouse-segmented" data-warehouse-dimension>
<label>
<input type="radio" name="dimension" value="month" @checked($dimension === 'month')>
<span>按月</span>
</label>
<label>
<input type="radio" name="dimension" value="day" @checked($dimension === 'day')>
<span>按天</span>
</label>
</div>
</fieldset>
<div class="third-party-warehouse-field">
<label for="warehouse-period" data-warehouse-period-label>{{ $dimension === 'day' ? '日期' : '月份' }}</label>
<input
id="warehouse-period"
type="{{ $dimension === 'day' ? 'date' : 'month' }}"
name="period"
value="{{ $filters['period'] }}"
data-warehouse-period
>
</div>
<div class="third-party-warehouse-field">
<label for="warehouse-sku-code">商品编码</label>
<input id="warehouse-sku-code" type="search" name="sku_code" value="{{ $filters['sku_code'] }}" placeholder="输入商品编码">
</div>
<div class="third-party-warehouse-field">
<label for="warehouse-product-name">商品名称</label>
<input id="warehouse-product-name" type="search" name="product_name" value="{{ $filters['product_name'] }}" placeholder="支持关键词筛选">
</div>
<div class="third-party-warehouse-field">
<label for="warehouse-supplier">供应商</label>
<input id="warehouse-supplier" type="search" name="supplier" value="{{ $filters['supplier'] }}" placeholder="支持关键词筛选">
</div>
<div class="third-party-warehouse-field">
<label for="warehouse-category">分类</label>
<input id="warehouse-category" type="search" name="category" value="{{ $filters['category'] }}" placeholder="支持关键词筛选">
</div>
<div class="third-party-warehouse-actions">
<button class="third-party-warehouse-button is-primary" type="submit">
<span class="material-icons-outlined" aria-hidden="true">search</span><span>筛选</span>
</button>
<a class="third-party-warehouse-button" href="{{ url()->current() }}">
<span class="material-icons-outlined" aria-hidden="true">restart_alt</span><span>重置</span>
</a>
<a
@class(['third-party-warehouse-button', 'finance-table-fallback-export', 'is-disabled' => ! $canExport])
data-finance-fallback-export
href="{{ $canExport ? $fallbackExportUrl : '#' }}"
@if (! $canExport) aria-disabled="true" @endif
>
<span class="material-icons-outlined" aria-hidden="true">download</span><span>导出筛选结果</span>
</a>
<button type="button" data-finance-server-export="visible" @disabled(! $canExport)>
<span class="material-icons-outlined" aria-hidden="true">download</span><span>导出显示列</span>
</button>
<button type="button" data-finance-server-export="all" @disabled(! $canExport)>
<span class="material-icons-outlined" aria-hidden="true">download</span><span>导出全部列</span>
</button>
<div class="finance-table-field-picker" data-finance-field-picker>
<button type="button" class="third-party-warehouse-button" aria-expanded="false">
<span class="material-icons-outlined" aria-hidden="true">view_column</span><span>列设置</span>
</button>
<div class="finance-table-field-picker__panel" role="group" aria-label="选择展示字段" hidden></div>
</div>
</div>
</form>
<div class="third-party-warehouse-summary" aria-label="当前筛选汇总">
@foreach ([
['key' => 'count', 'label' => '明细记录数', 'type' => 'count'],
['key' => 'opening_qty', 'label' => '期初数量合计', 'type' => 'number'],
['key' => 'closing_qty', 'label' => '期末数量合计', 'type' => 'number'],
] as $metric)
@php
$metricValue = $summaryReady ? ($summary[$metric['key']] ?? null) : null;
$metricDisplay = match ($metric['type']) {
'count' => $metricValue === null ? '—' : number_format((int) $metricValue),
default => $metricValue === null ? '—' : $number($metricValue),
};
@endphp
<div class="third-party-warehouse-summary__item">
<span class="third-party-warehouse-summary__label">{{ $metric['label'] }}</span>
<span class="third-party-warehouse-summary__value">{{ $metricDisplay }}</span>
</div>
@endforeach
</div>
<div class="third-party-warehouse-table-panel">
<div class="third-party-warehouse-table-scroll finance-table-scroll-owner">
<table
class="third-party-warehouse-table"
aria-label="三方仓进销存"
data-finance-table
@if ($exportUrl !== '') data-finance-export-base="{{ $exportUrl }}" @endif
data-finance-export-query="{{ json_encode($activeFilters, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) }}"
data-finance-export-disabled="{{ $canExport ? 'false' : 'true' }}"
>
<thead>
<tr>
@foreach ($columns as $column)
<th
scope="col"
data-finance-column-key="{{ $column['key'] }}"
data-export-field="{{ $column['key'] }}"
@class([
'is-number' => in_array($column['type'] ?? null, ['number', 'money'], true),
'is-image' => ($column['type'] ?? null) === 'image',
'is-wide' => ($column['type'] ?? null) === 'wide',
'is-code' => ($column['type'] ?? null) === 'code',
])
>{{ $column['label'] }}</th>
@endforeach
</tr>
</thead>
<tbody>
@forelse ($rows ?? [] as $row)
<tr>
@foreach ($columns as $column)
@php
$value = data_get($row, $column['key']);
$type = $column['type'] ?? 'text';
$displayValue = match ($type) {
'number' => $number($value),
'money' => $money($value),
default => $text($value),
};
@endphp
<td
@class([
'is-number' => in_array($type, ['number', 'money'], true),
'is-image' => $type === 'image',
'is-wide' => $type === 'wide',
'is-code' => $type === 'code',
])
@if ($type === 'wide' && $displayValue !== '-') title="{{ $displayValue }}" @endif
@if ($type === 'image') data-export-value="{{ $text($value) }}" @endif
>
@if ($type === 'image')
@php($safeImageUrl = $imageUrl($value))
@if ($safeImageUrl !== null)
<a href="{{ $safeImageUrl }}" target="_blank" rel="noopener noreferrer" aria-label="查看{{ $text(data_get($row, 'product_name')) }}图片">
<img class="third-party-warehouse-thumb" src="{{ $safeImageUrl }}" alt="" width="40" height="40" loading="lazy" decoding="async" referrerpolicy="no-referrer">
</a>
@else
-
@endif
@else
{{ $displayValue }}
@endif
</td>
@endforeach
</tr>
@empty
<tr class="is-empty">
<td class="third-party-warehouse-empty" colspan="{{ count($columns) }}">
@if ($pending)
首次同步尚未完成,当前不会显示空白数据冒充正式结果。
@elseif ($error !== '')
镜像数据读取失败,当前没有可展示的数据。
@elseif (in_array($statusCode, ['initial_failed', 'unavailable'], true))
镜像尚未准备好,请等待同步恢复后再查看。
@else
当前期间及筛选条件下没有数据,请调整条件或重置筛选。
@endif
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<div class="third-party-warehouse-pagination">
<span>共 {{ number_format($rowTotal) }} 条记录,本页 {{ number_format($rowCount) }} 条</span>
@if (is_object($rows) && method_exists($rows, 'currentPage'))
<div class="third-party-warehouse-pagination__controls" aria-label="三方仓进销存分页">
<a
@class(['third-party-warehouse-button', 'is-disabled' => $rows->onFirstPage()])
href="{{ $rows->previousPageUrl() ?: '#' }}"
aria-label="上一页"
@if ($rows->onFirstPage()) aria-disabled="true" @endif
><span class="material-icons-outlined" aria-hidden="true">chevron_left</span></a>
<span>第 {{ $rows->currentPage() }} / {{ max($rows->lastPage(), 1) }} 页</span>
<a
@class(['third-party-warehouse-button', 'is-disabled' => ! $rows->hasMorePages()])
href="{{ $rows->nextPageUrl() ?: '#' }}"
aria-label="下一页"
@if (! $rows->hasMorePages()) aria-disabled="true" @endif
><span class="material-icons-outlined" aria-hidden="true">chevron_right</span></a>
</div>
@endif
</div>
</div>
<script>
(() => {
const page = document.currentScript.closest('.third-party-warehouse-page');
const period = page?.querySelector('[data-warehouse-period]');
const periodLabel = page?.querySelector('[data-warehouse-period-label]');
if (!period || !periodLabel) return;
page.querySelectorAll('[data-warehouse-dimension] input[name="dimension"]').forEach((control) => {
control.addEventListener('change', () => {
const nextType = control.value === 'day' ? 'date' : 'month';
if (period.type !== nextType) period.value = '';
period.type = nextType;
periodLabel.textContent = nextType === 'date' ? '日期' : '月份';
});
});
})();
</script>
</div>