feat(财务台账): 动账明细账户简称支持输入联想 #462
@ -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 @@
|
||||
<form class="ledger-toolbar" method="GET" action="{{ $tabUrl('transactions') }}">
|
||||
<input type="hidden" name="tab" value="transactions">
|
||||
<div class="ledger-field" style="min-width:15rem"><label for="txns-q">关键词</label><input id="txns-q" name="txns_q" value="{{ request('txns_q') }}" placeholder="账户、明细、审批编号"></div>
|
||||
<div class="ledger-field"><label for="txns-account">账户简称</label><input id="txns-account" name="txns_account" list="ledger-account-list" value="{{ request('txns_account') }}" placeholder="输入或选择(含未匹配)"></div>
|
||||
<div class="ledger-field ledger-field--account-filter">
|
||||
<label for="txns-account">账户简称</label>
|
||||
<div class="ledger-filter-combobox" data-ledger-account-filter data-limit="60">
|
||||
<input id="txns-account" name="txns_account" type="search" value="{{ request('txns_account') }}" placeholder="输入文字自动联想" autocomplete="off" role="combobox" aria-expanded="false" aria-controls="txns-account-menu" aria-autocomplete="list" aria-haspopup="listbox" data-ledger-account-filter-input>
|
||||
<button class="ledger-filter-combobox__toggle" type="button" aria-label="展开账户简称选项" tabindex="-1" data-ledger-account-filter-toggle>
|
||||
<span class="material-icons-outlined" aria-hidden="true">expand_more</span>
|
||||
</button>
|
||||
<div class="ledger-filter-combobox__menu" id="txns-account-menu" role="listbox" aria-label="账户简称选项" data-ledger-account-filter-menu hidden></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ledger-field"><label for="txns-source">来源</label><select id="txns-source" name="txns_source"><option value="">全部</option>@foreach ($sourceLabels as $key => $label)<option value="{{ $key }}" @selected(request('txns_source') === $key)>{{ $label }}</option>@endforeach</select></div>
|
||||
<div class="ledger-field"><label for="txns-from">日期起</label><input id="txns-from" type="date" name="txns_from" value="{{ request('txns_from') }}"></div>
|
||||
<div class="ledger-field"><label for="txns-to">日期止</label><input id="txns-to" type="date" name="txns_to" value="{{ request('txns_to') }}"></div>
|
||||
@ -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) => {
|
||||
|
||||
@ -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('<option value="账户甲"', $html);
|
||||
$this->assertStringContainsString('<option value="账户乙"', $html);
|
||||
}
|
||||
|
||||
public function test_account_and_flow_headers_filter_and_sort_the_full_dataset(): void
|
||||
{
|
||||
$filtered = $this->loginAs()->get($this->pageUrl([
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user