From 732d3680fac2f0f9ac7a9ec483c5b9cc96feb1c5 Mon Sep 17 00:00:00 2001 From: limeng <1131177064@qq.com> Date: Wed, 23 Sep 2026 17:29:57 +0800 Subject: [PATCH] feat: make courier picker searchable (#253) --- .../agreement-signing.blade.php | 293 +++++++++++++++++- .../Business/AgreementSigningPageTest.php | 6 + 2 files changed, 290 insertions(+), 9 deletions(-) diff --git a/resources/views/business/pages/courier-payables/agreement-signing.blade.php b/resources/views/business/pages/courier-payables/agreement-signing.blade.php index 219c2dc9..d97fda09 100644 --- a/resources/views/business/pages/courier-payables/agreement-signing.blade.php +++ b/resources/views/business/pages/courier-payables/agreement-signing.blade.php @@ -249,6 +249,63 @@ color: var(--agreement-muted); } + .agreement-courier-search-wrap { + position: relative; + } + + .agreement-courier-native-select--enhanced { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + clip-path: inset(50%); + border: 0; + } + + .agreement-courier-options { + position: absolute; + z-index: 20; + top: calc(100% + 4px); + right: 0; + left: 0; + max-height: 16rem; + overflow: auto; + border: 1px solid var(--agreement-border); + border-radius: 8px; + background: var(--agreement-surface); + box-shadow: 0 4px 8px rgba(23, 23, 23, 0.1); + } + + .agreement-courier-option { + min-height: 34px; + padding: 0.5rem 0.65rem; + color: var(--agreement-ink); + cursor: pointer; + } + + .agreement-courier-option:hover, + .agreement-courier-option.is-active { + background: var(--agreement-accent-soft); + color: var(--agreement-accent); + } + + .agreement-courier-options-status { + padding: 0.5rem 0.65rem; + border-top: 1px solid var(--agreement-border); + color: var(--agreement-muted); + font-size: 0.75rem; + } + + .agreement-courier-native-select--enhanced:focus-visible, + .agreement-courier-search-wrap .agreement-control:focus-visible { + outline: 0; + box-shadow: 0 0 0 3px rgba(133, 102, 50, 0.2); + } + .agreement-search { position: relative; } @@ -1368,15 +1425,21 @@
- +
+ + +
@@ -1920,6 +1983,18 @@ var subjectNameInput = root.querySelector('[data-subject-name]'); var subjectCodeInput = root.querySelector('[data-subject-code]'); var courierSelect = root.querySelector('[data-courier-select]'); + var courierSearchWrap = root.querySelector('[data-courier-search-wrap]'); + var courierSearch = root.querySelector('[data-courier-search]'); + var courierOptionsList = root.querySelector('[data-courier-options]'); + var courierControl = root.querySelector('[data-courier-control]'); + var courierLabel = root.querySelector('label[for="agreement-courier"]'); + var courierOptions = courierSelect ? Array.prototype.filter.call(courierSelect.options, function (option) { + return option.value !== '' && !option.disabled; + }).map(function (option) { + return { value: option.value, account: option.dataset.esignAccount || '' }; + }) : []; + var visibleCourierIndexes = []; + var activeCourierIndex = -1; var esignAccountInput = root.querySelector('input[name="esign_account"]'); var accountSignerTypeField = root.querySelector('[data-account-signer-type-field]'); var accountSignerTypeInputs = Array.prototype.slice.call(root.querySelectorAll('[data-account-signer-type]')); @@ -2259,6 +2334,197 @@ } } + function closeCourierOptions() { + if (!courierSearch || !courierOptionsList) { + return; + } + + courierOptionsList.hidden = true; + courierSearch.setAttribute('aria-expanded', 'false'); + courierSearch.removeAttribute('aria-activedescendant'); + activeCourierIndex = -1; + } + + function renderCourierOptions(query) { + if (!courierSearch || !courierOptionsList) { + return; + } + + var normalizedQuery = String(query || '').trim().toLocaleLowerCase(); + var matches = []; + + courierOptions.forEach(function (option, index) { + if (normalizedQuery === '' || option.value.toLocaleLowerCase().indexOf(normalizedQuery) !== -1) { + matches.push(index); + } + }); + + visibleCourierIndexes = matches.slice(0, 80); + courierOptionsList.innerHTML = ''; + + if (visibleCourierIndexes.length === 0) { + var empty = document.createElement('div'); + empty.className = 'agreement-courier-options-status'; + empty.setAttribute('role', 'status'); + empty.textContent = normalizedQuery === '' ? '暂无可选快递资料' : '没有匹配的快递名称'; + courierOptionsList.appendChild(empty); + } else { + visibleCourierIndexes.forEach(function (optionIndex) { + var option = courierOptions[optionIndex]; + var item = document.createElement('div'); + item.className = 'agreement-courier-option'; + item.id = 'agreement-courier-option-' + optionIndex; + item.setAttribute('role', 'option'); + item.setAttribute('aria-selected', courierSelect.value === option.value ? 'true' : 'false'); + item.dataset.courierOption = String(optionIndex); + item.textContent = option.value; + courierOptionsList.appendChild(item); + }); + + if (matches.length > visibleCourierIndexes.length) { + var status = document.createElement('div'); + status.className = 'agreement-courier-options-status'; + status.setAttribute('role', 'status'); + status.textContent = '匹配到 ' + matches.length + ' 项,当前显示前 80 项;继续输入可缩小范围'; + courierOptionsList.appendChild(status); + } + } + + courierOptionsList.hidden = false; + courierSearch.setAttribute('aria-expanded', 'true'); + activeCourierIndex = -1; + courierSearch.removeAttribute('aria-activedescendant'); + } + + function setActiveCourierOption(position) { + if (!courierOptionsList || visibleCourierIndexes.length === 0) { + return; + } + + activeCourierIndex = (position + visibleCourierIndexes.length) % visibleCourierIndexes.length; + var optionIndex = visibleCourierIndexes[activeCourierIndex]; + var optionElement = courierOptionsList.querySelector('[data-courier-option="' + optionIndex + '"]'); + courierOptionsList.querySelectorAll('.agreement-courier-option').forEach(function (item) { + item.classList.toggle('is-active', item === optionElement); + }); + courierSearch.setAttribute('aria-activedescendant', 'agreement-courier-option-' + optionIndex); + optionElement && optionElement.scrollIntoView({ block: 'nearest' }); + } + + function syncCourierSelection() { + if (!courierSelect || !courierSearch) { + return; + } + + var enteredName = courierSearch.value.trim(); + var option = courierOptions.find(function (item) { + return item.value.toLocaleLowerCase() === enteredName.toLocaleLowerCase(); + }); + + if (option) { + courierSearch.value = option.value; + courierSelect.value = option.value; + courierSearch.setCustomValidity(''); + } else { + courierSelect.value = ''; + courierSearch.setCustomValidity(enteredName === '' ? '请选择快递公司' : '请从快递资料中选择快递公司'); + } + } + + function chooseCourierOption(index) { + if (!courierSearch || !courierSelect || !courierOptions[index]) { + return; + } + + courierSearch.value = courierOptions[index].value; + syncCourierSelection(); + closeCourierOptions(); + syncEsignAccount(); + refreshPreview(); + } + + function initializeCourierSearch() { + if (!courierSelect || !courierSearchWrap || !courierSearch || !courierOptionsList) { + return; + } + + courierSearchWrap.hidden = false; + courierSelect.classList.add('agreement-courier-native-select--enhanced'); + courierSelect.setAttribute('aria-hidden', 'true'); + courierSelect.tabIndex = -1; + courierSelect.required = false; + courierSearch.value = courierSelect.value; + courierSearch.required = true; + if (courierLabel) { + courierLabel.htmlFor = courierSearch.id; + } + syncCourierSelection(); + + courierSearch.addEventListener('focus', function () { + courierSearch.select(); + renderCourierOptions(''); + }); + courierSearch.addEventListener('input', function () { + syncCourierSelection(); + renderCourierOptions(courierSearch.value); + syncEsignAccount(); + refreshPreview(); + }); + courierSearch.addEventListener('keydown', function (event) { + if (event.key === 'ArrowDown') { + event.preventDefault(); + if (courierOptionsList.hidden) { + renderCourierOptions(courierSearch.value); + } + setActiveCourierOption(activeCourierIndex + 1); + } else if (event.key === 'ArrowUp') { + event.preventDefault(); + if (courierOptionsList.hidden) { + renderCourierOptions(courierSearch.value); + } + setActiveCourierOption(activeCourierIndex < 0 ? visibleCourierIndexes.length - 1 : activeCourierIndex - 1); + } else if (event.key === 'Enter' && !courierOptionsList.hidden) { + event.preventDefault(); + var selectedIndex = activeCourierIndex >= 0 + ? visibleCourierIndexes[activeCourierIndex] + : visibleCourierIndexes.find(function (optionIndex) { + return courierOptions[optionIndex].value.toLocaleLowerCase() === courierSearch.value.trim().toLocaleLowerCase(); + }); + if (selectedIndex === undefined && visibleCourierIndexes.length === 1) { + selectedIndex = visibleCourierIndexes[0]; + } + if (selectedIndex !== undefined) { + chooseCourierOption(selectedIndex); + } + } else if (event.key === 'Escape') { + closeCourierOptions(); + } + }); + courierSearch.addEventListener('blur', function () { + window.setTimeout(function () { + if (courierControl && !courierControl.contains(document.activeElement)) { + closeCourierOptions(); + } + }, 0); + }); + courierOptionsList.addEventListener('mousedown', function (event) { + if (event.target.closest('[data-courier-option]')) { + event.preventDefault(); + } + }); + courierOptionsList.addEventListener('click', function (event) { + var optionElement = event.target.closest('[data-courier-option]'); + if (optionElement) { + chooseCourierOption(Number(optionElement.dataset.courierOption)); + } + }); + root.addEventListener('click', function (event) { + if (courierControl && !courierControl.contains(event.target)) { + closeCourierOptions(); + } + }); + } + function setText(selector, value) { root.querySelectorAll(selector).forEach(function (target) { target.textContent = value; @@ -2718,6 +2984,10 @@ if (courierSelect) { courierSelect.addEventListener('change', function () { + if (courierSearch) { + courierSearch.value = courierSelect.value; + syncCourierSelection(); + } syncEsignAccount(); refreshPreview(); }); @@ -2755,6 +3025,10 @@ }); if (courierOption) { courierSelect.value = option.dataset.carrier; + if (courierSearch) { + courierSearch.value = option.dataset.carrier; + syncCourierSelection(); + } syncEsignAccount(); } } @@ -2829,6 +3103,7 @@ var initialUrl = new URL(window.location.href); var initialView = initialUrl.searchParams.get('agreement_view') || 'tasks'; + initializeCourierSearch(); renderSubjectSelect(); renderSubjectList(); showTab(initialView, { updateUrl: false }); diff --git a/tests/Feature/Business/AgreementSigningPageTest.php b/tests/Feature/Business/AgreementSigningPageTest.php index 4e777ca7..1643fc1a 100644 --- a/tests/Feature/Business/AgreementSigningPageTest.php +++ b/tests/Feature/Business/AgreementSigningPageTest.php @@ -63,6 +63,10 @@ class AgreementSigningPageTest extends FeatureTestCase ->assertDontSee('data-agreement-step-panel', false) ->assertSeeText('发起签章请求') ->assertSeeText('快递公司') + ->assertSee('data-courier-search', false) + ->assertSee('placeholder="输入快递名称搜索"', false) + ->assertSee('aria-haspopup="listbox"', false) + ->assertSee('data-courier-options', false) ->assertSeeText('文件类型') ->assertSeeText('收款账户证明') ->assertSeeText('TPL-ACCOUNT-CERT-V2') @@ -118,6 +122,8 @@ class AgreementSigningPageTest extends FeatureTestCase 'agreement_view' => 'create', ])) ->assertOk() + ->assertSee('data-courier-search', false) + ->assertSee('data-courier-select required', false) ->assertSee('', false) ->assertSee('', false) ->assertDontSee('