Merge remote-tracking branch 'origin/main' into feature/zhoushengchen-reverse-logistics-quotes

This commit is contained in:
zhoushengchen 2026-09-23 17:33:07 +08:00
commit d98aba6d77
2 changed files with 290 additions and 9 deletions

View File

@ -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 @@
<div class="agreement-form-grid">
<div class="agreement-field">
<label for="agreement-courier">快递公司 <span class="agreement-required" aria-hidden="true">*</span></label>
<select class="agreement-control" id="agreement-courier" name="courier_name" data-courier-select required>
<option value="">请选择</option>
@if ($agreementCourierNames->isEmpty())
<option value="" disabled>暂无可选快递资料</option>
@endif
@foreach ($agreementCourierNames as $courierName)
<option value="{{ $courierName }}" data-esign-account="{{ $agreementCourierAccounts[$courierName] ?? '' }}">{{ $courierName }}</option>
@endforeach
</select>
<div class="agreement-courier-search-wrap" data-courier-control>
<select class="agreement-control" id="agreement-courier" name="courier_name" data-courier-select required>
<option value="">请选择</option>
@if ($agreementCourierNames->isEmpty())
<option value="" disabled>暂无可选快递资料</option>
@endif
@foreach ($agreementCourierNames as $courierName)
<option value="{{ $courierName }}" data-esign-account="{{ $agreementCourierAccounts[$courierName] ?? '' }}">{{ $courierName }}</option>
@endforeach
</select>
<div class="agreement-courier-search-wrap" data-courier-search-wrap hidden>
<input class="agreement-control" id="agreement-courier-search" type="search" placeholder="输入快递名称搜索" autocomplete="off" role="combobox" aria-autocomplete="list" aria-haspopup="listbox" aria-expanded="false" aria-controls="agreement-courier-options" data-courier-search>
<div class="agreement-courier-options" id="agreement-courier-options" role="listbox" aria-label="快递资料" hidden data-courier-options></div>
</div>
</div>
</div>
<div class="agreement-field">
<label for="agreement-document-type">文件类型 <span class="agreement-required" aria-hidden="true">*</span></label>
@ -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 });

View File

@ -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('<option value="顺丰" data-esign-account="13800000001">顺丰</option>', false)
->assertSee('<option value="韵达" data-esign-account="">韵达</option>', false)
->assertDontSee('<option value="新北中通"', false);