feat: 普通商品展示完整价格变更记录
This commit is contained in:
parent
edf51109fe
commit
d0e7bfbcf7
@ -177,7 +177,7 @@ class ProductCatalogMirrorRepository
|
||||
|
||||
$this->attachPurchasePrices($rows, $purchasePriceSyncStatus['initialized']);
|
||||
$this->attachPhysicalFields($rows);
|
||||
$this->attachPriceChanges($rows, $priceChangeSyncStatus['initialized']);
|
||||
$this->attachPriceChanges($rows, $priceChangeSyncStatus['initialized'], false);
|
||||
$this->attachWorkPlatformPrices($rows, $workPlatformPriceSyncStatus['initialized']);
|
||||
foreach ($rows as $row) {
|
||||
yield $row;
|
||||
@ -307,11 +307,11 @@ class ProductCatalogMirrorRepository
|
||||
->orderByDesc('effectTime')
|
||||
->orderByDesc('Id')
|
||||
->get(['skuCode', 'purchasePrice', 'effectTime'])
|
||||
->unique('skuCode')
|
||||
->keyBy('skuCode');
|
||||
->unique(fn (object $price): string => $this->normalizeSku($price->skuCode ?? null))
|
||||
->keyBy(fn (object $price): string => $this->normalizeSku($price->skuCode ?? null));
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$price = $candidates->get(trim((string) ($row->sku_id ?? '')));
|
||||
$price = $candidates->get($this->normalizeSku($row->sku_id ?? null));
|
||||
if ($price === null) {
|
||||
continue;
|
||||
}
|
||||
@ -325,17 +325,20 @@ class ProductCatalogMirrorRepository
|
||||
|
||||
/**
|
||||
* 变价记录与采购单价一样是补充来源:即使镜像尚未就绪,普通商品行仍必须
|
||||
* 完整显示,四个变价字段保持为空,由页面统一说明同步状态。
|
||||
* 完整显示,变价字段保持为空,由页面统一说明同步状态。分页页面附带该
|
||||
* SKU 的全部定价/变价历史;大批量导出只附带最近一次变价,避免放大内存。
|
||||
*
|
||||
* @param list<object> $rows
|
||||
*/
|
||||
private function attachPriceChanges(array $rows, bool $initialized): void
|
||||
private function attachPriceChanges(array $rows, bool $initialized, bool $includeHistory = true): void
|
||||
{
|
||||
foreach ($rows as $row) {
|
||||
$row->price_change_before_price = null;
|
||||
$row->price_change_after_price = null;
|
||||
$row->price_change_diff_amount = null;
|
||||
$row->price_change_effect_time = null;
|
||||
$row->price_change_history = [];
|
||||
$row->price_change_history_count = 0;
|
||||
}
|
||||
|
||||
if ($rows === [] || ! $initialized) {
|
||||
@ -353,23 +356,53 @@ class ProductCatalogMirrorRepository
|
||||
return;
|
||||
}
|
||||
|
||||
$candidates = $this->connection->table($this->priceChangeTable())
|
||||
$query = $this->connection->table($this->priceChangeTable())
|
||||
->whereIn('skucode', $skuIds)
|
||||
->where('priceType', '1')
|
||||
->whereIn('priceType', $includeHistory ? ['0', '1'] : ['1'])
|
||||
->orderByDesc('effectStartTime')
|
||||
->orderByDesc('Id')
|
||||
->get([
|
||||
'skucode',
|
||||
'originalPrice',
|
||||
'afterPrice',
|
||||
'diffPrice',
|
||||
'effectStartTime',
|
||||
])
|
||||
->unique('skucode')
|
||||
->keyBy('skucode');
|
||||
->orderByDesc('Id');
|
||||
$candidates = $query->get([
|
||||
'Id',
|
||||
'skucode',
|
||||
'priceType',
|
||||
'originalPrice',
|
||||
'afterPrice',
|
||||
'diffPrice',
|
||||
'effectStartTime',
|
||||
'effectEndTime',
|
||||
'createTime',
|
||||
])
|
||||
->groupBy(fn (object $change): string => $this->normalizeSku($change->skucode ?? null));
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$change = $candidates->get(trim((string) ($row->sku_id ?? '')));
|
||||
$history = $candidates->get($this->normalizeSku($row->sku_id ?? null), collect())->values();
|
||||
|
||||
if ($includeHistory) {
|
||||
$row->price_change_history = $history->map(static function (object $change): array {
|
||||
$type = trim((string) ($change->priceType ?? ''));
|
||||
|
||||
return [
|
||||
'id' => $change->Id ?? null,
|
||||
'record_type' => $type,
|
||||
'record_type_label' => match ($type) {
|
||||
'0' => '定价',
|
||||
'1' => '变价',
|
||||
default => $type === '' ? '-' : $type,
|
||||
},
|
||||
'before_price' => $change->originalPrice ?? null,
|
||||
'after_price' => $change->afterPrice ?? null,
|
||||
'diff_amount' => $change->diffPrice ?? null,
|
||||
'effect_start_time' => $change->effectStartTime ?? null,
|
||||
'effect_end_time' => $change->effectEndTime ?? null,
|
||||
'created_at' => $change->createTime ?? null,
|
||||
];
|
||||
})->all();
|
||||
$row->price_change_history_count = count($row->price_change_history);
|
||||
}
|
||||
|
||||
$change = $history->first(
|
||||
static fn (object $candidate): bool => trim((string) ($candidate->priceType ?? '')) === '1',
|
||||
);
|
||||
if ($change === null) {
|
||||
continue;
|
||||
}
|
||||
@ -469,6 +502,11 @@ class ProductCatalogMirrorRepository
|
||||
);
|
||||
}
|
||||
|
||||
private function normalizeSku(mixed $value): string
|
||||
{
|
||||
return mb_strtolower(trim((string) $value), 'UTF-8');
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $filters */
|
||||
private function filteredQuery(array $filters): Builder
|
||||
{
|
||||
|
||||
@ -230,7 +230,7 @@
|
||||
|
||||
.product-catalog-table {
|
||||
width: 100%;
|
||||
min-width: 3180px;
|
||||
min-width: 3280px;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.78125rem;
|
||||
}
|
||||
@ -288,6 +288,49 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.product-catalog-history-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.product-catalog-icon-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
border: 1px solid var(--vip-content-accent-dark, #856632);
|
||||
border-radius: 50%;
|
||||
background: var(--vip-content-accent-dark, #856632);
|
||||
color: #ffffff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.product-catalog-icon-button:hover {
|
||||
background: #6a5129;
|
||||
}
|
||||
|
||||
.product-catalog-icon-button:focus-visible {
|
||||
box-shadow: 0 0 0 3px rgba(133, 102, 50, 0.20);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.product-catalog-icon-button:disabled {
|
||||
border-color: var(--vip-border, #d8d2c7);
|
||||
background: #efede8;
|
||||
color: var(--catalog-muted);
|
||||
cursor: not-allowed;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.product-catalog-history-count {
|
||||
color: var(--catalog-muted);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.product-catalog-table .is-clamped {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@ -365,6 +408,161 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
body.product-price-history-open {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.product-price-history-modal[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.product-price-history-modal {
|
||||
position: fixed;
|
||||
z-index: 1100;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.product-price-history-modal__backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(5, 5, 6, 0.54);
|
||||
}
|
||||
|
||||
.product-price-history-modal__panel {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: min(1240px, calc(100vw - 32px));
|
||||
max-height: min(760px, calc(100vh - 48px));
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--vip-border, #d8d2c7);
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 4px 8px rgba(23, 23, 23, 0.10);
|
||||
}
|
||||
|
||||
.product-price-history-modal__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid var(--vip-border, #d8d2c7);
|
||||
}
|
||||
|
||||
.product-price-history-modal__heading {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.product-price-history-modal__title {
|
||||
margin: 0;
|
||||
color: var(--catalog-text);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.product-price-history-modal__subtitle {
|
||||
margin: 4px 0 0;
|
||||
overflow: hidden;
|
||||
color: var(--catalog-muted);
|
||||
font-size: 0.75rem;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.product-price-history-modal__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.product-price-history-modal__summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 0;
|
||||
border-bottom: 1px solid var(--vip-border, #d8d2c7);
|
||||
background: var(--vip-content-accent-soft, #f7f0de);
|
||||
}
|
||||
|
||||
.product-price-history-modal__metric {
|
||||
min-width: 0;
|
||||
padding: 10px 16px;
|
||||
border-right: 1px solid var(--vip-border, #d8d2c7);
|
||||
}
|
||||
|
||||
.product-price-history-modal__metric:last-child {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.product-price-history-modal__metric-label {
|
||||
display: block;
|
||||
margin-bottom: 3px;
|
||||
color: var(--catalog-muted);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.product-price-history-modal__metric-value {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
color: var(--catalog-text);
|
||||
font-size: 0.8125rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-weight: 600;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.product-price-history-modal__table-wrap {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.product-price-history-table {
|
||||
width: 100%;
|
||||
min-width: 980px;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.78125rem;
|
||||
}
|
||||
|
||||
.product-price-history-table th,
|
||||
.product-price-history-table td {
|
||||
padding: 9px 10px;
|
||||
border-bottom: 1px solid #eeeae3;
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.product-price-history-table th {
|
||||
position: sticky;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
background: #f5f3ee;
|
||||
color: #4f4a43;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.product-price-history-table td.is-numeric {
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.product-price-history-table td.is-positive {
|
||||
color: var(--vip-warning, #935f07);
|
||||
}
|
||||
|
||||
.product-price-history-table td.is-negative {
|
||||
color: var(--vip-success, #53793e);
|
||||
}
|
||||
|
||||
@media (max-width: 1500px) {
|
||||
.product-catalog-filters {
|
||||
grid-template-columns: repeat(4, minmax(140px, 1fr));
|
||||
@ -398,6 +596,36 @@
|
||||
.product-catalog-actions {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.product-price-history-modal {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.product-price-history-modal__panel {
|
||||
width: calc(100vw - 16px);
|
||||
max-height: calc(100vh - 16px);
|
||||
}
|
||||
|
||||
.product-price-history-modal__header {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.product-price-history-modal__actions .product-catalog-button span:last-child {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.product-price-history-modal__summary {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.product-price-history-modal__metric {
|
||||
border-right: 0;
|
||||
border-bottom: 1px solid var(--vip-border, #d8d2c7);
|
||||
}
|
||||
|
||||
.product-price-history-modal__metric:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -568,6 +796,7 @@
|
||||
<th class="is-numeric">变价后价格</th>
|
||||
<th class="is-numeric">差额</th>
|
||||
<th>变价时间</th>
|
||||
<th>变价记录</th>
|
||||
<th>数据来源</th>
|
||||
<th class="is-numeric">市场价</th>
|
||||
<th class="is-numeric">重量(kg)</th>
|
||||
@ -621,6 +850,32 @@
|
||||
@endforeach
|
||||
<td class="is-numeric">{{ isset($row->price_change_diff_amount) && $row->price_change_diff_amount !== null ? number_format((float) $row->price_change_diff_amount, 2) : '-' }}</td>
|
||||
<td class="is-code">{{ !empty($row->price_change_effect_time) ? \Illuminate\Support\Carbon::parse($row->price_change_effect_time)->format('Y-m-d') : '-' }}</td>
|
||||
@php
|
||||
$priceChangeHistory = $row->price_change_history ?? [];
|
||||
$priceChangeHistoryCount = (int) ($row->price_change_history_count ?? count($priceChangeHistory));
|
||||
$priceChangeHistoryId = 'product-price-history-' . (int) ($row->autoid ?? 0);
|
||||
@endphp
|
||||
<td>
|
||||
<div class="product-catalog-history-action">
|
||||
<button
|
||||
type="button"
|
||||
class="product-catalog-icon-button"
|
||||
data-price-history-trigger
|
||||
data-history-target="{{ $priceChangeHistoryId }}"
|
||||
data-sku-code="{{ $row->sku_id ?? '' }}"
|
||||
data-product-name="{{ $row->name ?? '' }}"
|
||||
data-cost-price="{{ $row->cost_price ?? '' }}"
|
||||
data-work-platform-price="{{ $row->work_platform_price ?? '' }}"
|
||||
aria-label="查看 {{ $row->sku_id ?? '当前商品' }} 的价格变更明细"
|
||||
title="{{ $priceChangeHistoryCount > 0 ? '查看价格变更明细' : '当前商品暂无价格变更记录' }}"
|
||||
@disabled($priceChangeHistoryCount === 0)
|
||||
>
|
||||
<span class="material-icons-outlined text-base" aria-hidden="true">history</span>
|
||||
</button>
|
||||
<span class="product-catalog-history-count">{{ $priceChangeHistoryCount }} 条</span>
|
||||
<script id="{{ $priceChangeHistoryId }}" type="application/json">@json($priceChangeHistory, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT)</script>
|
||||
</div>
|
||||
</td>
|
||||
<td class="product-catalog-source-list">
|
||||
@forelse ($row->data_sources ?? [] as $source)
|
||||
<span class="product-catalog-source-item">{{ $source }}</span>
|
||||
@ -650,7 +905,7 @@
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="33" class="product-catalog-empty">
|
||||
<td colspan="34" class="product-catalog-empty">
|
||||
<span class="material-icons-outlined text-3xl block mb-2" aria-hidden="true">inventory_2</span>
|
||||
@if ($productCatalogError)
|
||||
数据暂时不可用。
|
||||
@ -690,4 +945,213 @@
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="product-price-history-modal" data-price-history-modal hidden>
|
||||
<div class="product-price-history-modal__backdrop" data-price-history-close aria-hidden="true"></div>
|
||||
<section
|
||||
class="product-price-history-modal__panel"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="product-price-history-title"
|
||||
>
|
||||
<header class="product-price-history-modal__header">
|
||||
<div class="product-price-history-modal__heading">
|
||||
<h2 id="product-price-history-title" class="product-price-history-modal__title">价格变更明细</h2>
|
||||
<p class="product-price-history-modal__subtitle" data-price-history-subtitle></p>
|
||||
</div>
|
||||
<div class="product-price-history-modal__actions">
|
||||
<button type="button" class="product-catalog-button" data-price-history-export>
|
||||
<span class="material-icons-outlined text-base" aria-hidden="true">download</span>
|
||||
<span>导出变价记录</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="product-catalog-icon-button"
|
||||
data-price-history-close
|
||||
aria-label="关闭价格变更明细"
|
||||
title="关闭"
|
||||
>
|
||||
<span class="material-icons-outlined text-base" aria-hidden="true">close</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="product-price-history-modal__summary">
|
||||
<div class="product-price-history-modal__metric">
|
||||
<span class="product-price-history-modal__metric-label">商品编码</span>
|
||||
<span class="product-price-history-modal__metric-value" data-price-history-sku>-</span>
|
||||
</div>
|
||||
<div class="product-price-history-modal__metric">
|
||||
<span class="product-price-history-modal__metric-label">当前成本价</span>
|
||||
<span class="product-price-history-modal__metric-value" data-price-history-cost>-</span>
|
||||
</div>
|
||||
<div class="product-price-history-modal__metric">
|
||||
<span class="product-price-history-modal__metric-label">当前工作平台价格</span>
|
||||
<span class="product-price-history-modal__metric-value" data-price-history-platform>-</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="product-price-history-modal__table-wrap">
|
||||
<table class="product-price-history-table" aria-label="当前商品价格变更明细">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>价格类型</th>
|
||||
<th class="is-numeric">变价前价格</th>
|
||||
<th class="is-numeric">变价后价格</th>
|
||||
<th class="is-numeric">差价</th>
|
||||
<th>生效开始日期</th>
|
||||
<th>生效结束日期</th>
|
||||
<th>记录时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody data-price-history-body></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
const page = document.querySelector('[data-product-catalog-page]');
|
||||
if (!page || page.dataset.priceHistoryBound === 'true') {
|
||||
return;
|
||||
}
|
||||
|
||||
page.dataset.priceHistoryBound = 'true';
|
||||
const modal = page.querySelector('[data-price-history-modal]');
|
||||
const tableBody = page.querySelector('[data-price-history-body]');
|
||||
const exportButton = page.querySelector('[data-price-history-export]');
|
||||
const closeButton = modal?.querySelector('button[data-price-history-close]');
|
||||
const state = { history: [], sku: '', productName: '', trigger: null };
|
||||
|
||||
if (!modal || !tableBody || !exportButton || !closeButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
const displayValue = (value) => value === null || value === undefined || value === '' ? '-' : String(value);
|
||||
const formatMoney = (value) => {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return '-';
|
||||
}
|
||||
|
||||
const numeric = Number(value);
|
||||
return Number.isFinite(numeric)
|
||||
? numeric.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
: '-';
|
||||
};
|
||||
const formatDate = (value, withTime = false) => {
|
||||
if (!value) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
const normalized = String(value).replace('T', ' ');
|
||||
return normalized.slice(0, withTime ? 19 : 10);
|
||||
};
|
||||
const appendCell = (row, value, className = '') => {
|
||||
const cell = document.createElement('td');
|
||||
cell.textContent = displayValue(value);
|
||||
if (className) {
|
||||
cell.className = className;
|
||||
}
|
||||
row.appendChild(cell);
|
||||
};
|
||||
|
||||
const renderHistory = () => {
|
||||
tableBody.replaceChildren();
|
||||
state.history.forEach((item) => {
|
||||
const row = document.createElement('tr');
|
||||
appendCell(row, item.record_type_label);
|
||||
appendCell(row, formatMoney(item.before_price), 'is-numeric');
|
||||
appendCell(row, formatMoney(item.after_price), 'is-numeric');
|
||||
|
||||
const difference = Number(item.diff_amount);
|
||||
const differenceClass = Number.isFinite(difference) && difference !== 0
|
||||
? `is-numeric ${difference > 0 ? 'is-positive' : 'is-negative'}`
|
||||
: 'is-numeric';
|
||||
appendCell(row, formatMoney(item.diff_amount), differenceClass);
|
||||
appendCell(row, formatDate(item.effect_start_time));
|
||||
appendCell(row, formatDate(item.effect_end_time));
|
||||
appendCell(row, formatDate(item.created_at, true));
|
||||
tableBody.appendChild(row);
|
||||
});
|
||||
};
|
||||
|
||||
const openModal = (trigger) => {
|
||||
const source = document.getElementById(trigger.dataset.historyTarget || '');
|
||||
if (!source) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
state.history = JSON.parse(source.textContent || '[]');
|
||||
} catch (error) {
|
||||
state.history = [];
|
||||
}
|
||||
if (state.history.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.sku = trigger.dataset.skuCode || '';
|
||||
state.productName = trigger.dataset.productName || '';
|
||||
state.trigger = trigger;
|
||||
page.querySelector('[data-price-history-subtitle]').textContent = `${state.productName || '未命名商品'} · ${state.history.length} 条记录`;
|
||||
page.querySelector('[data-price-history-sku]').textContent = displayValue(state.sku);
|
||||
page.querySelector('[data-price-history-cost]').textContent = formatMoney(trigger.dataset.costPrice);
|
||||
page.querySelector('[data-price-history-platform]').textContent = formatMoney(trigger.dataset.workPlatformPrice);
|
||||
renderHistory();
|
||||
modal.hidden = false;
|
||||
document.body.classList.add('product-price-history-open');
|
||||
closeButton.focus();
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
modal.hidden = true;
|
||||
document.body.classList.remove('product-price-history-open');
|
||||
state.trigger?.focus();
|
||||
};
|
||||
|
||||
const csvCell = (value) => `"${displayValue(value).replaceAll('"', '""')}"`;
|
||||
const exportHistory = () => {
|
||||
const headers = ['价格类型', '变价前价格', '变价后价格', '差价', '生效开始日期', '生效结束日期', '记录时间'];
|
||||
const lines = [headers.map(csvCell).join(',')];
|
||||
state.history.forEach((item) => {
|
||||
lines.push([
|
||||
item.record_type_label,
|
||||
item.before_price,
|
||||
item.after_price,
|
||||
item.diff_amount,
|
||||
formatDate(item.effect_start_time),
|
||||
formatDate(item.effect_end_time),
|
||||
formatDate(item.created_at, true),
|
||||
].map(csvCell).join(','));
|
||||
});
|
||||
|
||||
const blob = new Blob(['\uFEFF', lines.join('\r\n')], { type: 'text/csv;charset=utf-8' });
|
||||
const link = document.createElement('a');
|
||||
const safeSku = (state.sku || '商品').replace(/[\\/:*?"<>|]/g, '_');
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = `${safeSku}_价格变更明细.csv`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
URL.revokeObjectURL(link.href);
|
||||
};
|
||||
|
||||
page.addEventListener('click', (event) => {
|
||||
const trigger = event.target.closest('[data-price-history-trigger]');
|
||||
if (trigger && !trigger.disabled) {
|
||||
openModal(trigger);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.target.closest('[data-price-history-close]')) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
exportButton.addEventListener('click', exportHistory);
|
||||
modal.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
@ -65,6 +65,31 @@ class ProductCatalogPageTest extends FeatureTestCase
|
||||
'price_change_after_price' => 475,
|
||||
'price_change_diff_amount' => -15,
|
||||
'price_change_effect_time' => '2026-08-11 00:00:00',
|
||||
'price_change_history' => [
|
||||
[
|
||||
'id' => 12,
|
||||
'record_type' => '1',
|
||||
'record_type_label' => '变价',
|
||||
'before_price' => 490,
|
||||
'after_price' => 475,
|
||||
'diff_amount' => -15,
|
||||
'effect_start_time' => '2026-08-11 00:00:00',
|
||||
'effect_end_time' => null,
|
||||
'created_at' => '2026-08-10 09:30:00',
|
||||
],
|
||||
[
|
||||
'id' => 3,
|
||||
'record_type' => '0',
|
||||
'record_type_label' => '定价',
|
||||
'before_price' => null,
|
||||
'after_price' => 490,
|
||||
'diff_amount' => null,
|
||||
'effect_start_time' => '2026-01-01 00:00:00',
|
||||
'effect_end_time' => '2026-08-10 00:00:00',
|
||||
'created_at' => '2025-12-31 08:00:00',
|
||||
],
|
||||
],
|
||||
'price_change_history_count' => 2,
|
||||
'purchase_order_unit_price' => 65,
|
||||
'purchase_order_finished_at' => '2026-08-20 11:03:05',
|
||||
'purchase_order_id' => 827380,
|
||||
@ -140,6 +165,17 @@ class ProductCatalogPageTest extends FeatureTestCase
|
||||
->assertSeeText('变价后价格')
|
||||
->assertSeeText('差额')
|
||||
->assertSeeText('-15.00')
|
||||
->assertSeeText('变价记录')
|
||||
->assertSeeText('2 条')
|
||||
->assertSee('data-price-history-trigger', false)
|
||||
->assertSeeText('价格变更明细')
|
||||
->assertSeeText('当前成本价')
|
||||
->assertSeeText('当前工作平台价格')
|
||||
->assertSeeText('导出变价记录')
|
||||
->assertSeeText('生效结束日期')
|
||||
->assertSeeText('记录时间')
|
||||
->assertSee('"record_type_label":"变价"', false)
|
||||
->assertSee('"record_type_label":"定价"', false)
|
||||
->assertSeeText('数据来源')
|
||||
->assertSeeText('商品及库存管理(普通商品资料)')
|
||||
->assertSeeText('1688代发单价')
|
||||
|
||||
@ -263,17 +263,29 @@ class ProductCatalogMirrorRepositoryTest extends TestCase
|
||||
$this->assertSame('pending', $result['work_platform_price_sync_status']['code']);
|
||||
}
|
||||
|
||||
public function test_it_attaches_the_latest_price_type_one_change_details(): void
|
||||
public function test_it_attaches_all_price_history_and_keeps_the_latest_change_on_the_main_row(): void
|
||||
{
|
||||
$this->markReady(1);
|
||||
$this->insertProduct(1, ['sku_id' => 'SKU-CHANGE']);
|
||||
$this->createPriceChangeTables(true);
|
||||
$this->insertPriceChange(9, [
|
||||
'skucode' => 'SKU-CHANGE',
|
||||
'priceType' => 0,
|
||||
'originalPrice' => null,
|
||||
'afterPrice' => 500,
|
||||
'diffPrice' => null,
|
||||
'effectStartTime' => '2026-08-01 00:00:00',
|
||||
'effectEndTime' => '2026-08-17 00:00:00',
|
||||
'createTime' => '2026-07-31 09:00:00',
|
||||
]);
|
||||
$this->insertPriceChange(10, [
|
||||
'skucode' => 'SKU-CHANGE',
|
||||
'originalPrice' => 500,
|
||||
'afterPrice' => 480,
|
||||
'diffPrice' => -20,
|
||||
'effectStartTime' => '2026-08-18 00:00:00',
|
||||
'effectEndTime' => '2026-08-19 00:00:00',
|
||||
'createTime' => '2026-08-17 09:00:00',
|
||||
]);
|
||||
$this->insertPriceChange(11, [
|
||||
'skucode' => 'SKU-CHANGE',
|
||||
@ -281,6 +293,7 @@ class ProductCatalogMirrorRepositoryTest extends TestCase
|
||||
'afterPrice' => 475,
|
||||
'diffPrice' => -5,
|
||||
'effectStartTime' => '2026-08-20 00:00:00',
|
||||
'createTime' => '2026-08-19 09:00:00',
|
||||
]);
|
||||
$this->insertPriceChange(12, [
|
||||
'skucode' => 'SKU-CHANGE',
|
||||
@ -298,6 +311,13 @@ class ProductCatalogMirrorRepositoryTest extends TestCase
|
||||
$this->assertSame('475', (string) $row->price_change_after_price);
|
||||
$this->assertSame('-5', (string) $row->price_change_diff_amount);
|
||||
$this->assertStringStartsWith('2026-08-20', (string) $row->price_change_effect_time);
|
||||
$this->assertSame(3, $row->price_change_history_count);
|
||||
$this->assertSame(['变价', '变价', '定价'], array_column($row->price_change_history, 'record_type_label'));
|
||||
$this->assertSame('480', (string) $row->price_change_history[0]['before_price']);
|
||||
$this->assertSame('475', (string) $row->price_change_history[0]['after_price']);
|
||||
$this->assertSame('-5', (string) $row->price_change_history[0]['diff_amount']);
|
||||
$this->assertStringStartsWith('2026-08-19', (string) $row->price_change_history[0]['created_at']);
|
||||
$this->assertStringStartsWith('2026-08-19', (string) $row->price_change_history[1]['effect_end_time']);
|
||||
$this->assertTrue($result['price_change_sync_status']['initialized']);
|
||||
}
|
||||
|
||||
@ -314,6 +334,8 @@ class ProductCatalogMirrorRepositoryTest extends TestCase
|
||||
$this->assertNull($row->price_change_after_price);
|
||||
$this->assertNull($row->price_change_diff_amount);
|
||||
$this->assertNull($row->price_change_effect_time);
|
||||
$this->assertSame([], $row->price_change_history);
|
||||
$this->assertSame(0, $row->price_change_history_count);
|
||||
$this->assertFalse($result['price_change_sync_status']['initialized']);
|
||||
$this->assertSame('pending', $result['price_change_sync_status']['code']);
|
||||
}
|
||||
@ -559,6 +581,8 @@ class ProductCatalogMirrorRepositoryTest extends TestCase
|
||||
$table->decimal('afterPrice', 18, 2)->nullable();
|
||||
$table->decimal('diffPrice', 18, 2)->nullable();
|
||||
$table->dateTime('effectStartTime')->nullable();
|
||||
$table->dateTime('effectEndTime')->nullable();
|
||||
$table->dateTime('createTime')->nullable();
|
||||
});
|
||||
DB::connection('business_mirror')->table('business_mirror_table_states')->insert([
|
||||
'dataset_key' => ProductCatalogMirrorRepository::PRICE_CHANGE_DATASET_KEY,
|
||||
@ -579,6 +603,8 @@ class ProductCatalogMirrorRepositoryTest extends TestCase
|
||||
'afterPrice' => 50,
|
||||
'diffPrice' => -5,
|
||||
'effectStartTime' => '2026-08-01 00:00:00',
|
||||
'effectEndTime' => null,
|
||||
'createTime' => '2026-07-31 09:00:00',
|
||||
], $overrides));
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user