From 9c75c8faadcf83f274e286181f527dd23ccd5d4e Mon Sep 17 00:00:00 2001 From: limeng <1131177064@qq.com> Date: Sat, 14 Nov 2026 09:37:05 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=8A=A5=E4=BB=B7=E5=8F=98=E4=BB=B7?= =?UTF-8?q?=E6=8C=89=E5=9C=B0=E5=8C=BA=E4=B8=8E=E9=87=8D=E9=87=8F=E5=8C=BA?= =?UTF-8?q?=E9=97=B4=E6=A8=AA=E5=90=91=E7=BC=96=E8=BE=91=20(#1169)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../quote-change-editor.blade.php | 138 +++++++++++++----- .../CourierQuoteChangeWorkflowTest.php | 64 +++++++- 2 files changed, 161 insertions(+), 41 deletions(-) diff --git a/resources/views/business/pages/courier-payables/quote-change-editor.blade.php b/resources/views/business/pages/courier-payables/quote-change-editor.blade.php index 9f538508..621234ef 100644 --- a/resources/views/business/pages/courier-payables/quote-change-editor.blade.php +++ b/resources/views/business/pages/courier-payables/quote-change-editor.blade.php @@ -11,7 +11,7 @@ --qc-border: var(--vip-border, #D8D2C7); margin: 24px 40px; color: var(--qc-ink); - font-family: "JetBrains Mono", "Microsoft YaHei", "PingFang SC", monospace; + font-family: "JetBrains Mono", "CaiWu CJK", "Microsoft YaHei", monospace; font-size: 13px; } .quote-change-workspace *, .quote-change-workspace *::before, .quote-change-workspace *::after { box-sizing: border-box; } @@ -50,25 +50,37 @@ .quote-change-source-meta dd { margin: 3px 0 0; font-size: 12.5px; font-variant-numeric: tabular-nums; } .quote-batch-tools { display: grid; grid-template-columns: minmax(160px, .8fr) minmax(130px, .6fr) auto; gap: 8px; align-items: end; } .quote-change-table-wrap { overflow: auto; max-height: 540px; border: 1px solid var(--qc-border); border-radius: 8px; } - .quote-change-table { width: 100%; border-collapse: collapse; font-size: 12.5px; } - .quote-change-table th { position: sticky; top: 0; z-index: 1; padding: 9px 10px; border-bottom: 1px solid var(--qc-border); background: var(--qc-gold-soft); color: var(--qc-ink); font-size: 12px; font-weight: 600; text-align: left; white-space: nowrap; } - .quote-change-table td { padding: 7px 10px; border-bottom: 1px solid #ECE8E0; vertical-align: middle; } + .quote-change-table { width: 100%; border-collapse: separate; border-spacing: 0; font-size: 12.5px; } + .quote-change-table thead th { position: sticky; top: 0; z-index: 2; min-width: 150px; padding: 10px; border-bottom: 1px solid var(--qc-border); background: var(--qc-gold-soft); color: var(--qc-ink); font-size: 12px; font-weight: 600; text-align: left; } + .quote-change-table td { min-width: 150px; padding: 9px 10px; border-bottom: 1px solid #ECE8E0; border-left: 1px solid #ECE8E0; vertical-align: top; background: #fff; } + .quote-change-table .quote-change-region { position: sticky; left: 0; z-index: 1; min-width: 125px; width: 125px; padding: 12px; border-bottom: 1px solid #ECE8E0; border-right: 1px solid var(--qc-border); background: #fff; text-align: left; white-space: nowrap; } + .quote-change-table thead .quote-change-region { z-index: 3; background: var(--qc-gold-soft); } + .quote-change-table tbody tr:last-child th { border-bottom: 0; } .quote-change-table tbody tr:last-child td { border-bottom: 0; } - .quote-change-table tbody tr.is-changed td { background: #FFFDF7; } - .quote-change-table__number { text-align: right !important; font-variant-numeric: tabular-nums; } - .quote-change-price-input { width: 108px; height: 30px; border: 1px solid var(--qc-border); border-radius: 6px; padding: 0 8px; text-align: right; font: inherit; font-variant-numeric: tabular-nums; } + .quote-change-table td.is-changed { background: #FFF8E8; box-shadow: inset 0 0 0 1px var(--qc-gold); } + .quote-change-column-label, .quote-change-cell-edit { display: flex; align-items: center; gap: 8px; } + .quote-change-column-label { line-height: 1.5; } + .quote-change-table input[type="checkbox"] { appearance: none; -webkit-appearance: none; flex: 0 0 15px; width: 15px; height: 15px; margin: 0; border: 1px solid var(--qc-gold); border-radius: 3px; background: #fff; cursor: pointer; } + .quote-change-table input[type="checkbox"]:checked { background: var(--qc-gold); box-shadow: inset 0 0 0 3px #fff; } + .quote-change-table input[type="checkbox"]:indeterminate { background: linear-gradient(var(--qc-gold),var(--qc-gold)) center / 9px 3px no-repeat #fff; } + .quote-change-table input[type="checkbox"]:focus-visible { outline: 2px solid var(--qc-gold); outline-offset: 2px; } + .quote-change-price-input { width: 100%; min-width: 0; height: 34px; border: 1px solid var(--qc-border); border-radius: 6px; background: #fff; padding: 0 8px; color: var(--qc-ink); text-align: right; font: inherit; font-variant-numeric: tabular-nums; } .quote-change-price-input:focus { border-color: var(--qc-gold); outline: 0; box-shadow: 0 0 0 2px rgba(133, 102, 50, .16); } - .quote-change-delta { font-weight: 600; font-variant-numeric: tabular-nums; } + .quote-change-cell-reference { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 4px 8px; margin-top: 5px; color: var(--qc-muted); font-size: 11px; font-variant-numeric: tabular-nums; } + .quote-change-delta { font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; } .quote-change-delta--up { color: #935F07; } .quote-change-delta--down { color: #53793E; } - .quote-change-note-input { width: 180px; height: 30px; border: 1px solid var(--qc-border); border-radius: 6px; padding: 0 8px; font: inherit; } - .quote-change-footer { position: sticky; bottom: 0; display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 16px; padding: 12px 16px; border: 1px solid var(--qc-border); border-radius: 12px; background: rgba(255, 255, 255, .96); } + .quote-change-cell-note { margin-top: 5px; color: var(--qc-muted); font-size: 11px; } + .quote-change-cell-note summary { cursor: pointer; } + .quote-change-cell-note summary:focus-visible { outline: 2px solid var(--qc-gold); } + .quote-change-note-input { width: 100%; height: 30px; margin-top: 5px; border: 1px solid var(--qc-border); border-radius: 6px; padding: 0 8px; color: var(--qc-ink); font: inherit; } + .quote-change-footer { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 16px; padding: 12px 16px; border: 1px solid var(--qc-border); border-radius: 12px; background: #fff; } .quote-change-footer__summary { color: var(--qc-muted); font-size: 12px; } .quote-change-footer__summary strong { color: var(--qc-ink); font-variant-numeric: tabular-nums; } .quote-change-footer__actions { display: flex; gap: 8px; } @media (max-width: 900px) { .quote-change-workspace { margin: 16px; } - .quote-change-form-grid, .quote-change-source-meta { grid-template-columns: 1fr; } + .quote-change-source, .quote-change-form-grid, .quote-change-source-meta { grid-template-columns: 1fr; } .quote-change-field--full { grid-column: auto; } .quote-batch-tools { grid-template-columns: 1fr; } .quote-change-footer { align-items: stretch; flex-direction: column; } @@ -133,22 +145,45 @@
- +
+

每个地区一行,各重量区间横向填写新价;下方对照原价和差额。勾选地区、区间列或单个价格可批量调整。

+ @php + $priceRows = collect($editableCells)->groupBy('source_row', true); + $priceColumns = collect($editableCells)->unique('source_column')->sortBy('source_column'); + @endphp
- - +
单元格地区/规则重量段/计费项原价格新价格差额备注
+ + + @foreach ($priceColumns as $column) + + @endforeach + - @foreach ($editableCells as $index => $cell) - - - - - - - - - + @foreach ($priceRows as $rowIndex => $rowCells) + @php($cellsByColumn = $rowCells->keyBy('source_column')) + + + @foreach ($priceColumns as $column) + @php($cell = $cellsByColumn->get($column['source_column'])) + @if ($cell) + @php($index = $rowCells->search(fn ($item) => $item['source_cell'] === $cell['source_cell'])) + + @else + + @endif + @endforeach @endforeach @@ -173,7 +208,9 @@ var root = document.querySelector('[data-quote-change-editor]'); if (!root || root.dataset.initialized === 'true') return; root.dataset.initialized = 'true'; - var rows = Array.from(root.querySelectorAll('[data-price-row]')); + var cells = Array.from(root.querySelectorAll('[data-price-cell]')); + var regionRows = Array.from(root.querySelectorAll('[data-region-row]')); + var columnSelectors = Array.from(root.querySelectorAll('[data-column-select]')); var count = root.querySelector('[data-changed-count]'); var effectiveDate = root.querySelector('[data-effective-date]'); var retrospectiveAlert = root.querySelector('[data-retrospective-alert]'); @@ -183,27 +220,48 @@ retrospectiveAlert.hidden = !isRetrospective; if (isRetrospective) retrospectiveAlert.querySelector('[data-retrospective-date]').textContent = effectiveDate.value; } - function refreshRow(row) { - var original = Number(row.dataset.original); - var input = row.querySelector('[data-new-price]'); + function refreshCell(cell) { + var original = Number(cell.dataset.original); + var input = cell.querySelector('[data-new-price]'); var next = Number(input.value); - var delta = Number.isFinite(next) ? next - original : 0; - var output = row.querySelector('[data-price-delta]'); - output.textContent = (delta > 0 ? '+' : '') + delta.toFixed(4); + var hasPrice = input.value !== '' && Number.isFinite(next); + var delta = hasPrice ? next - original : 0; + var output = cell.querySelector('[data-price-delta]'); + output.textContent = hasPrice ? (delta > 0 ? '+' : '') + delta.toFixed(4) : '—'; output.className = 'quote-change-delta' + (delta > 0 ? ' quote-change-delta--up' : delta < 0 ? ' quote-change-delta--down' : ''); - row.classList.toggle('is-changed', Math.abs(delta) > 0.00005); - if (Math.abs(delta) > 0.00005) row.querySelector('[data-row-select]').checked = true; + cell.classList.toggle('is-changed', hasPrice && Math.abs(delta) > 0.00005); } - function refreshAll() { rows.forEach(refreshRow); count.textContent = String(rows.filter(function (row) { return row.classList.contains('is-changed'); }).length); } - rows.forEach(function (row) { row.querySelector('[data-new-price]').addEventListener('input', refreshAll); }); + function selected(cell) { return cell.querySelector('[data-cell-select]').checked; } + function syncSelector(selector, group) { + var checked = group.filter(selected).length; + selector.checked = group.length > 0 && checked === group.length; + selector.indeterminate = checked > 0 && checked < group.length; + } + function syncSelection() { + syncSelector(root.querySelector('[data-select-all]'), cells); + regionRows.forEach(function (row) { syncSelector(row.querySelector('[data-region-select]'), Array.from(row.querySelectorAll('[data-price-cell]'))); }); + columnSelectors.forEach(function (selector) { syncSelector(selector, cells.filter(function (cell) { return cell.dataset.column === selector.dataset.columnSelect; })); }); + } + function refreshAll() { cells.forEach(refreshCell); count.textContent = String(cells.filter(function (cell) { return cell.classList.contains('is-changed'); }).length); syncSelection(); } + cells.forEach(function (cell) { + cell.querySelector('[data-new-price]').addEventListener('input', function () { + if (this.value !== '' && Number(this.value) !== Number(cell.dataset.original)) cell.querySelector('[data-cell-select]').checked = true; + refreshAll(); + }); + cell.querySelector('[data-cell-select]').addEventListener('change', syncSelection); + }); if (effectiveDate) effectiveDate.addEventListener('change', refreshRetrospective); - root.querySelector('[data-select-all]').addEventListener('change', function (event) { rows.forEach(function (row) { row.querySelector('[data-row-select]').checked = event.target.checked; }); }); + function selectGroup(group, checked) { group.forEach(function (cell) { cell.querySelector('[data-cell-select]').checked = checked; }); syncSelection(); } + root.querySelector('[data-select-all]').addEventListener('change', function () { selectGroup(cells, this.checked); }); + regionRows.forEach(function (row) { row.querySelector('[data-region-select]').addEventListener('change', function () { selectGroup(Array.from(row.querySelectorAll('[data-price-cell]')), this.checked); }); }); + columnSelectors.forEach(function (selector) { selector.addEventListener('change', function () { selectGroup(cells.filter(function (cell) { return cell.dataset.column === selector.dataset.columnSelect; }), this.checked); }); }); root.querySelector('[data-batch-apply]').addEventListener('click', function () { - var value = Number(root.querySelector('[data-batch-value]').value); - if (!Number.isFinite(value)) { root.querySelector('[data-batch-value]').focus(); return; } + var valueInput = root.querySelector('[data-batch-value]'); + var value = Number(valueInput.value); + if (valueInput.value.trim() === '' || !Number.isFinite(value)) { valueInput.focus(); return; } var mode = root.querySelector('[data-batch-mode]').value; - rows.filter(function (row) { return row.querySelector('[data-row-select]').checked; }).forEach(function (row) { - var input = row.querySelector('[data-new-price]'); + cells.filter(selected).forEach(function (cell) { + var input = cell.querySelector('[data-new-price]'); var current = Number(input.value); var result = mode === 'add' ? current + value : mode === 'percent' ? current * (1 + value / 100) : value; input.value = Math.max(0, result).toFixed(4); diff --git a/tests/Feature/Business/CourierQuoteChangeWorkflowTest.php b/tests/Feature/Business/CourierQuoteChangeWorkflowTest.php index 512eea86..80547f0e 100644 --- a/tests/Feature/Business/CourierQuoteChangeWorkflowTest.php +++ b/tests/Feature/Business/CourierQuoteChangeWorkflowTest.php @@ -79,7 +79,7 @@ class CourierQuoteChangeWorkflowTest extends FeatureTestCase ->assertSeeText('批量方式') ->assertSeeText('原价格') ->assertSeeText('新价格') - ->assertSee('data-price-row', false) + ->assertSee('data-region-row', false) ->assertSee('name="items[0][new_amount]"', false) ->assertSee('data-effective-date', false) ->assertDontSee('type="date" min=', false) @@ -87,6 +87,68 @@ class CourierQuoteChangeWorkflowTest extends FeatureTestCase ->assertSeeText('保存草稿'); } + public function test_horizontal_editor_saves_the_rendered_cell_coordinates_and_restores_draft_values(): void + { + file_put_contents($this->fixturePath, json_encode([ + 'sourcePath' => 'C:/private/报价单合并.xlsx', + 'sheets' => [[ + 'index' => 0, + 'name' => '新北中通报价', + 'address' => 'A1:D3', + 'values' => [ + ['省份', '0-3公斤面单费/元', '3公斤以上面单费/元', '0-3公斤费率'], + ['江苏省', 0, 1.2, 0.45], + ['安徽省', 4, 1, null], + ], + 'formulas' => [[], ['', '', '=0.6*2'], []], + ]], + ], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR)); + + $response = $this->loginAs()->get(route('business.courier-quote-changes.create', ['source_quote_id' => 'QB-001']))->assertOk(); + $document = new \DOMDocument(); + @$document->loadHTML('' . $response->getContent()); + $xpath = new \DOMXPath($document); + $this->assertSame(2, $xpath->query('//tr[@data-region-row]')->length); + $this->assertSame(3, $xpath->query('//input[@data-column-select]')->length); + $this->assertSame(2, $xpath->query('//tr[@data-region-row="1"]/td[@data-price-cell]')->length); + $this->assertSame(2, $xpath->query('//tr[@data-region-row="2"]/td[@data-price-cell]')->length); + + // Submit the names/coordinates actually rendered in the rearranged form. + $fields = []; + foreach ($xpath->query('//form[@data-quote-change-form]//input[@name]') as $input) { + $fields[] = urlencode($input->getAttribute('name')) . '=' . urlencode($input->getAttribute('value')); + } + parse_str(implode('&', $fields), $payload); + $this->assertSame(['B2', 'D2', 'B3', 'C3'], array_column($payload['items'], 'source_cell')); + $this->assertSame('0.0000', $payload['items'][0]['new_amount']); + $payload['change_type'] = '费率调整'; + $payload['effective_date'] = now()->addDay()->toDateString(); + $payload['summary'] = '江苏和安徽按区间调整。'; + $payload['items'][1]['new_amount'] = '0.5678'; + $payload['items'][1]['note'] = '江苏费率调整'; + $payload['items'][3]['new_amount'] = '0'; + + $this->post(route('business.courier-quote-changes.store'), $payload)->assertRedirect(); + $change = CourierQuoteChange::with('items')->firstOrFail(); + $items = $change->items->keyBy('source_cell'); + $this->assertCount(2, $items); + $this->assertSame('江苏省', $items['D2']->region); + $this->assertSame('0.4500', $items['D2']->original_amount); + $this->assertSame('0.5678', $items['D2']->new_amount); + $this->assertSame('江苏费率调整', $items['D2']->note); + $this->assertSame('安徽省', $items['C3']->region); + $this->assertSame('0.0000', $items['C3']->new_amount); + $this->assertEquals(0.45, app(QuoteManagementSnapshot::class)->load()['quotes'][0]['values'][1][3]); + + $draft = $this->get(route('business.courier-quote-changes.edit', $change))->assertOk(); + @$document->loadHTML('' . $draft->getContent()); + $xpath = new \DOMXPath($document); + $this->assertSame('0.5678', $xpath->query('//input[@name="items[1][new_amount]"]')->item(0)->getAttribute('value')); + $this->assertSame('江苏费率调整', $xpath->query('//input[@name="items[1][note]"]')->item(0)->getAttribute('value')); + $this->assertTrue($xpath->query('//input[@name="items[1][note]"]/parent::details')->item(0)->hasAttribute('open')); + $this->assertSame('0.0000', $xpath->query('//input[@name="items[3][new_amount]"]')->item(0)->getAttribute('value')); + } + public function test_historical_effective_date_can_be_saved_and_submitted_for_retrospective_recalculation(): void { $payload = $this->validPayload();
{{ $cell['source_cell'] }}{{ $cell['region'] ?: '—' }}{{ $cell['weight_band'] ?: ($cell['charge_item'] ?: '—') }}{{ rtrim(rtrim($cell['original_amount'], '0'), '.') }}0.0000
+
+ + +
+
原 {{ rtrim(rtrim($cell['original_amount'], '0'), '.') }}0.0000
+
备注
+ @foreach (['source_cell', 'source_row', 'source_column', 'region', 'weight_band', 'charge_item', 'original_amount', 'unit'] as $field) + + @endforeach +
—