From df1a084bd63e8277998c575cd70c98fe2b97b621 Mon Sep 17 00:00:00 2001 From: shanshi <3316401569@qq.com> Date: Thu, 22 Oct 2026 18:44:04 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E8=B4=A2=E5=8A=A1=E5=8F=B0=E8=B4=A6):=20?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E8=B4=A6=E6=88=B7=E7=AE=80=E7=A7=B0=E8=BE=93?= =?UTF-8?q?=E5=85=A5=E8=81=94=E6=83=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/financial-ledger/ledger.blade.php | 165 +++++++++++++++++- .../FinanceLedgerHeaderControlsTest.php | 22 +++ 2 files changed, 186 insertions(+), 1 deletion(-) diff --git a/resources/views/business/pages/financial-ledger/ledger.blade.php b/resources/views/business/pages/financial-ledger/ledger.blade.php index 97d03f2a..b306b6eb 100644 --- a/resources/views/business/pages/financial-ledger/ledger.blade.php +++ b/resources/views/business/pages/financial-ledger/ledger.blade.php @@ -172,6 +172,22 @@ .ledger-field label { color:var(--vip-content-muted, #595959); font-size:.7rem; font-weight:600; } .ledger-field input, .ledger-field select { height:2.15rem; padding:0 .6rem; border:1px solid var(--vip-border, #D8D2C7); border-radius:.45rem; background:#fff; color:var(--vip-content-ink, #171717); font:inherit; } .ledger-field input:focus, .ledger-field select:focus { border-color:var(--vip-content-accent-dark, #856632); outline:none; box-shadow:0 0 0 3px rgba(133,102,50,.16); } + .ledger-field--account-filter { min-width:16rem; } + .ledger-filter-combobox { position:relative; display:flex; align-items:center; width:100%; } + .ledger-filter-combobox input { width:100%; padding-right:2.25rem; } + .ledger-filter-combobox input::-webkit-search-cancel-button { appearance:none; -webkit-appearance:none; } + .ledger-filter-combobox__toggle { position:absolute; right:.25rem; display:inline-grid; place-items:center; width:1.65rem; height:1.65rem; padding:0; border:0; border-radius:.3rem; background:transparent; color:var(--vip-content-muted, #595959); cursor:pointer; } + .ledger-filter-combobox__toggle:hover, + .ledger-filter-combobox__toggle:focus-visible { background:var(--vip-content-accent-soft, #F7F0DE); color:var(--vip-content-accent-dark, #856632); outline:none; } + .ledger-filter-combobox__toggle .material-icons-outlined { font-size:1.05rem; transition:transform .15s ease; } + .ledger-filter-combobox.is-open .ledger-filter-combobox__toggle .material-icons-outlined { transform:rotate(180deg); } + .ledger-filter-combobox__menu { position:absolute; top:calc(100% + .35rem); right:0; left:0; z-index:35; max-height:min(18rem, 50vh); overflow:auto; padding:.25rem; border:1px solid var(--vip-border, #D8D2C7); border-radius:.5rem; background:#fff; box-shadow:0 5px 14px rgba(23,23,23,.13); overscroll-behavior:contain; scrollbar-color:var(--vip-content-accent, #B9954F) #efede8; scrollbar-width:thin; } + .ledger-filter-combobox__menu[hidden] { display:none !important; } + .ledger-filter-combobox__option { display:block; width:100%; min-height:1.8rem; padding:.32rem .5rem; overflow:hidden; border:0; border-radius:.35rem; background:transparent; color:var(--vip-content-ink, #171717); font:inherit; font-size:.72rem; text-align:left; text-overflow:ellipsis; white-space:nowrap; cursor:pointer; } + .ledger-filter-combobox__option:hover, + .ledger-filter-combobox__option.is-active { background:var(--vip-content-accent-soft, #F7F0DE); color:var(--vip-content-accent-dark, #856632); } + .ledger-filter-combobox__empty, + .ledger-filter-combobox__more { margin:.25rem 0 0; padding:.45rem .5rem; border-top:1px solid #e8e3da; color:var(--vip-content-muted, #595959); font-size:.68rem; line-height:1.4; text-align:center; } .ledger-actions { display:flex; gap:.45rem; padding-bottom:.05rem; } .ledger-edit-switch { position:relative; display:inline-flex; align-items:center; align-self:flex-end; gap:.45rem; height:2.15rem; padding:0 .65rem; border:1px solid var(--vip-border, #D8D2C7); border-radius:.45rem; background:#fff; color:var(--vip-content-muted, #595959); font-size:.72rem; font-weight:600; cursor:pointer; user-select:none; white-space:nowrap; } .ledger-edit-switch input { position:absolute; width:1px; height:1px; opacity:0; pointer-events:none; } @@ -591,7 +607,16 @@
-
+
+ +
+ + + +
+
@@ -810,6 +835,144 @@ })); if (window.location.hash === '#mapping') showTab('mapping', false); + const setupLedgerAccountFilter = () => { + const combobox = page.querySelector('[data-ledger-account-filter]'); + const input = combobox?.querySelector('[data-ledger-account-filter-input]'); + const menu = combobox?.querySelector('[data-ledger-account-filter-menu]'); + const toggle = combobox?.querySelector('[data-ledger-account-filter-toggle]'); + const optionsSource = page.querySelector('#ledger-account-list'); + if (!combobox || !input || !menu || !optionsSource) return; + + const limit = Math.max(1, Number(combobox.dataset.limit) || 60); + const seen = new Set(); + const options = []; + const addOption = (value) => { + const text = String(value || '').trim(); + if (!text || seen.has(text)) return; + seen.add(text); + options.push(text); + }; + addOption('未匹配'); + Array.from(optionsSource.options).forEach((option) => addOption(option.value)); + + let visibleValues = []; + let activeIndex = -1; + const isOpen = () => !menu.hidden; + const setActive = (index) => { + const buttons = Array.from(menu.querySelectorAll('[data-ledger-account-option]')); + activeIndex = buttons.length === 0 ? -1 : Math.min(Math.max(index, 0), buttons.length - 1); + buttons.forEach((button, buttonIndex) => { + const active = buttonIndex === activeIndex; + button.classList.toggle('is-active', active); + button.setAttribute('aria-selected', active ? 'true' : 'false'); + }); + const active = buttons[activeIndex]; + if (active) { + input.setAttribute('aria-activedescendant', active.id); + active.scrollIntoView({ block: 'nearest' }); + } else { + input.removeAttribute('aria-activedescendant'); + } + }; + const pick = (value) => { + input.value = value; + close(); + input.focus(); + }; + const render = () => { + const needle = input.value.trim().toLocaleLowerCase(); + const matches = options.filter((value) => !needle || value.toLocaleLowerCase().includes(needle)); + visibleValues = matches.slice(0, limit); + menu.replaceChildren(); + visibleValues.forEach((value, index) => { + const button = document.createElement('button'); + button.id = 'txns-account-option-' + index; + button.type = 'button'; + button.className = 'ledger-filter-combobox__option'; + button.setAttribute('role', 'option'); + button.setAttribute('aria-selected', 'false'); + button.dataset.ledgerAccountOption = ''; + button.dataset.value = value; + button.textContent = value; + button.addEventListener('mousedown', (event) => event.preventDefault()); + button.addEventListener('mouseenter', () => setActive(index)); + button.addEventListener('click', () => pick(value)); + menu.appendChild(button); + }); + activeIndex = -1; + if (visibleValues.length === 0) { + const empty = document.createElement('p'); + empty.className = 'ledger-filter-combobox__empty'; + empty.textContent = '没有匹配的账户简称'; + menu.appendChild(empty); + } else if (matches.length > limit) { + const more = document.createElement('p'); + more.className = 'ledger-filter-combobox__more'; + more.textContent = '仅显示前 ' + limit + ' 项,请继续输入缩小范围'; + menu.appendChild(more); + } + input.removeAttribute('aria-activedescendant'); + }; + const open = (shouldRender = true) => { + if (shouldRender || !isOpen()) render(); + menu.hidden = false; + combobox.classList.add('is-open'); + input.setAttribute('aria-expanded', 'true'); + }; + const close = () => { + menu.hidden = true; + combobox.classList.remove('is-open'); + input.setAttribute('aria-expanded', 'false'); + input.removeAttribute('aria-activedescendant'); + activeIndex = -1; + menu.querySelectorAll('[data-ledger-account-option]').forEach((button) => { + button.classList.remove('is-active'); + button.setAttribute('aria-selected', 'false'); + }); + }; + const move = (step) => { + if (!isOpen()) { + open(); + return; + } + if (!visibleValues.length) return; + let next = activeIndex === -1 + ? (step > 0 ? 0 : visibleValues.length - 1) + : activeIndex + step; + if (next < 0) next = visibleValues.length - 1; + if (next >= visibleValues.length) next = 0; + setActive(next); + }; + + input.addEventListener('focus', () => open()); + input.addEventListener('click', () => open()); + input.addEventListener('input', () => open(true)); + input.addEventListener('keydown', (event) => { + if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { + event.preventDefault(); + move(event.key === 'ArrowDown' ? 1 : -1); + return; + } + if (event.key === 'Escape' || event.key === 'Tab') { + close(); + return; + } + if (event.key === 'Enter' && isOpen() && activeIndex >= 0) { + event.preventDefault(); + pick(visibleValues[activeIndex]); + } + }); + toggle?.addEventListener('click', () => { + if (isOpen()) close(); + else open(); + input.focus(); + }); + document.addEventListener('click', (event) => { + if (!combobox.contains(event.target)) close(); + }); + }; + setupLedgerAccountFilter(); + const headerDetails = Array.from(page.querySelectorAll('[data-ledger-header-details]')); const closeHeaderMenus = (except = null) => { headerDetails.forEach((details) => { diff --git a/tests/Feature/Business/FinanceLedgerHeaderControlsTest.php b/tests/Feature/Business/FinanceLedgerHeaderControlsTest.php index 73ee19ce..67eb1a4f 100644 --- a/tests/Feature/Business/FinanceLedgerHeaderControlsTest.php +++ b/tests/Feature/Business/FinanceLedgerHeaderControlsTest.php @@ -142,6 +142,28 @@ class FinanceLedgerHeaderControlsTest extends FeatureTestCase } } + public function test_transaction_account_filter_provides_typeahead_suggestions(): void + { + $html = $this->loginAs()->get($this->pageUrl()) + ->assertOk() + ->assertSee('data-ledger-account-filter', false) + ->assertSee('data-limit="60"', false) + ->assertSee('id="txns-account"', false) + ->assertSee('role="combobox"', false) + ->assertSee('aria-autocomplete="list"', false) + ->assertSee('aria-controls="txns-account-menu"', false) + ->assertSee('aria-haspopup="listbox"', false) + ->assertSee('data-ledger-account-filter-toggle', false) + ->assertSee('data-ledger-account-filter-menu', false) + ->assertSee('max-height:min(18rem, 50vh)', false) + ->assertSee("input.addEventListener('input'", false) + ->assertDontSee('id="txns-account" name="txns_account" list=', false) + ->getContent(); + + $this->assertStringContainsString('