小额打款工单:筛选下拉可滚动 + 完成日期改为账单页同款日历 #276

Merged
yueyang merged 2 commits from feature/yueyang-small-payment-filter-scroll into main 2026-09-27 13:45:18 +08:00
2 changed files with 821 additions and 27 deletions

View File

@ -36,10 +36,9 @@
default => '业务镜像尚未建立',
};
$lastSucceededAt = $smallPaymentSyncStatus['last_succeeded_at'] ?? null;
$datetimeLocalValue = static function (string $value): string {
$value = str_replace(' ', 'T', trim($value));
return substr(str_contains($value, 'T') ? $value : $value . 'T00:00', 0, 16);
// 日期控件只承载日期:时区无关,服务端会把纯日期补成整天半开区间(from 00:00:00 / to 次日 00:00:00)
$dateValue = static function (string $value): string {
return substr(str_replace(' ', 'T', trim($value)), 0, 10);
};
@endphp
@ -96,6 +95,256 @@
background: rgba(147, 95, 7, 0.06);
color: var(--vip-warning, #935f07);
}
/*
* 筛选下拉:可滚动的选项面板。
* 原生 <datalist> 的面板由浏览器绘制,高度/滚动条无法控制(选项多时看不全也滑不动),
* 因此改为自绘面板:固定最大高度 + 内部滚动,风格对齐 .finance-table-field-picker__panel。
*/
.upstream-data-combobox {
position: relative;
display: flex;
align-items: center;
width: 100%;
}
.upstream-data-combobox input { padding-right: 2.25rem; }
.upstream-data-combobox input::-webkit-search-cancel-button {
-webkit-appearance: none;
appearance: none;
}
.upstream-data-combobox__toggle {
position: absolute;
right: 0.25rem;
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.75rem;
height: 1.75rem;
padding: 0;
border: 0;
border-radius: 0.35rem;
background: transparent;
color: var(--vip-content-muted, #595959);
cursor: pointer;
}
.upstream-data-combobox__toggle:hover {
background: rgba(133, 102, 50, 0.12);
color: var(--vip-content-accent-dark, #856632);
}
.upstream-data-combobox__toggle .material-icons-outlined {
font-size: 1.125rem;
transition: transform 0.15s ease;
}
.upstream-data-combobox.is-open .upstream-data-combobox__toggle .material-icons-outlined {
transform: rotate(180deg);
}
.upstream-data-combobox__menu {
position: absolute;
top: calc(100% + 0.35rem);
left: 0;
right: 0;
z-index: 30;
max-height: min(18rem, 50vh);
overflow: auto;
padding: 0.25rem;
border: 1px solid var(--vip-border, #d8d2c7);
border-radius: 0.5rem;
background: var(--vip-surface, #ffffff);
box-shadow: 0 4px 8px rgba(23, 23, 23, 0.1);
overscroll-behavior: contain;
scrollbar-color: var(--vip-content-accent, #b9954f) #efede8;
scrollbar-width: thin;
}
.upstream-data-combobox__menu[hidden] { display: none; }
.upstream-data-combobox__option {
display: block;
width: 100%;
min-height: 1.75rem;
padding: 0.25rem 0.5rem;
border: 0;
border-radius: 0.35rem;
background: transparent;
color: var(--vip-content-ink, #171717);
cursor: pointer;
font: inherit;
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.upstream-data-combobox__option:hover,
.upstream-data-combobox__option.is-active {
background: rgba(133, 102, 50, 0.12);
color: var(--vip-content-accent-dark, #856632);
}
.upstream-data-combobox__option[aria-selected="true"] { font-weight: 600; }
.upstream-data-combobox__empty {
margin: 0;
padding: 0.5rem;
color: var(--vip-content-muted, #595959);
font-size: 0.75rem;
text-align: center;
}
.upstream-data-combobox__empty[hidden] { display: none; }
/*
* 日期选择器:对齐「淘系微信账户」账单页的日期控件
* (文本输入框 + 右侧日历按钮 + 中文月历弹层,选中/今天用品牌色)。
* 原来是 datetime-local,会显示成 "2026-08-29T00:00",与账单页不一致。
*/
.upstream-data-field__label {
color: var(--vip-content-muted, #595959);
font-size: 0.75rem;
font-weight: 500;
line-height: 1.333;
}
.upstream-data-field--dates { flex: 1 1 20rem; }
.upstream-data-date-range {
display: flex;
align-items: center;
gap: 0.5rem;
}
.upstream-data-date {
position: relative;
display: flex;
flex: 1 1 8.5rem;
min-width: 0;
}
.upstream-data-date input {
flex: 1 1 auto;
width: auto;
min-width: 0;
padding-right: 1.75rem;
border-radius: 0.5rem 0 0 0.5rem;
cursor: pointer;
}
.upstream-data-date__clear {
position: absolute;
top: 0;
right: 2.25rem;
bottom: 0;
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.5rem;
height: 1.5rem;
margin: auto 0;
padding: 0;
border: 0;
border-radius: 0.25rem;
background: transparent;
color: var(--vip-content-muted, #595959);
cursor: pointer;
}
.upstream-data-date__clear:hover {
background: rgba(133, 102, 50, 0.12);
color: var(--vip-content-accent-dark, #856632);
}
.upstream-data-date__clear[hidden] { display: none; }
.upstream-data-date__clear .material-icons-outlined { font-size: 0.9375rem; }
.upstream-data-date__pick {
display: inline-flex;
flex: 0 0 2.125rem;
align-items: center;
justify-content: center;
width: 2.125rem;
height: 2.125rem;
padding: 0;
border: 1px solid var(--vip-border, #d8d2c7);
border-left: 0;
border-radius: 0 0.5rem 0.5rem 0;
background: var(--vip-surface, #ffffff);
color: var(--vip-content-accent-dark, #856632);
cursor: pointer;
}
.upstream-data-date__pick:hover,
.upstream-data-date__pick:focus-visible {
background: #f7f0de;
color: #6a5129;
outline: none;
}
.upstream-data-date__pick .material-icons-outlined { font-size: 1.125rem; }
.upstream-data-date-range__sep {
color: var(--vip-content-muted, #595959);
font-weight: 400;
}
.upstream-data-calendar {
position: absolute;
top: calc(100% + 0.35rem);
left: 0;
z-index: 30;
width: 16.5rem;
padding: 0.5rem;
border: 1px solid var(--vip-border, #d8d2c7);
border-radius: 0.5rem;
background: var(--vip-surface, #ffffff);
box-shadow: 0 4px 8px rgba(23, 23, 23, 0.1);
}
.upstream-data-calendar[hidden] { display: none; }
.upstream-data-calendar__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.25rem;
margin-bottom: 0.25rem;
}
.upstream-data-calendar__title {
color: var(--vip-content-ink, #171717);
font-size: 0.8125rem;
font-weight: 600;
}
.upstream-data-calendar__nav {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.5rem;
height: 1.5rem;
padding: 0;
border: 0;
border-radius: 0.25rem;
background: transparent;
color: var(--vip-content-accent-dark, #856632);
cursor: pointer;
}
.upstream-data-calendar__nav:hover { background: #f7f0de; }
.upstream-data-calendar__nav .material-icons-outlined { font-size: 1.125rem; }
.upstream-data-calendar__week,
.upstream-data-calendar__grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 0.125rem;
}
.upstream-data-calendar__week span {
padding: 0.25rem 0;
color: var(--vip-content-muted, #595959);
font-size: 0.75rem;
text-align: center;
}
.upstream-data-calendar__day {
height: 1.75rem;
padding: 0;
border: 1px solid transparent;
border-radius: 0.25rem;
background: transparent;
color: var(--vip-content-ink, #171717);
cursor: pointer;
font: inherit;
font-size: 0.75rem;
font-variant-numeric: tabular-nums;
}
.upstream-data-calendar__day:hover { background: #f7f0de; }
.upstream-data-calendar__day.is-outside { color: #b9b3a8; }
.upstream-data-calendar__day.is-today { border-color: var(--vip-content-accent-dark, #856632); }
.upstream-data-calendar__day.is-selected {
border-color: var(--vip-content-accent-dark, #856632);
background: var(--vip-content-accent-dark, #856632);
color: #ffffff;
}
/* 结束日期的日历向右展开会顶出工具栏,改成向左 */
.upstream-data-date:last-of-type .upstream-data-calendar {
right: 0;
left: auto;
}
</style>
<div class="small-payment-sync" role="status" data-sync-state="{{ $syncCode }}">
@ -124,13 +373,49 @@
action="{{ route('business.pages.show', ['section' => 'financial-operations', 'page' => 'small-payment-work-orders']) }}"
data-filter-contract="small-payment-work-orders.filters"
>
<div class="upstream-data-field">
<label for="small-payment-from">完成开始时间</label>
<input id="small-payment-from" type="datetime-local" name="from" value="{{ $datetimeLocalValue($filters['from']) }}">
</div>
<div class="upstream-data-field">
<label for="small-payment-to">完成结束时间</label>
<input id="small-payment-to" type="datetime-local" name="to" value="{{ $datetimeLocalValue($filters['to']) }}">
<div class="upstream-data-field upstream-data-field--dates">
<span class="upstream-data-field__label">完成日期</span>
<div class="upstream-data-date-range">
<span class="upstream-data-date" data-date-picker>
<input
id="small-payment-from"
type="text"
name="from"
value="{{ $dateValue($filters['from']) }}"
readonly
autocomplete="off"
aria-label="完成开始日期"
data-date-input
>
<button type="button" class="upstream-data-date__clear" data-date-clear hidden aria-label="清除开始日期">
<span class="material-icons-outlined" aria-hidden="true">close</span>
</button>
<button type="button" class="upstream-data-date__pick" data-date-toggle aria-label="选择完成开始日期">
<span class="material-icons-outlined" aria-hidden="true">calendar_today</span>
</button>
<div class="upstream-data-calendar" role="dialog" aria-label="完成开始日期日历" data-date-calendar hidden></div>
</span>
<b class="upstream-data-date-range__sep">至</b>
<span class="upstream-data-date" data-date-picker>
<input
id="small-payment-to"
type="text"
name="to"
value="{{ $dateValue($filters['to']) }}"
readonly
autocomplete="off"
aria-label="完成结束日期"
data-date-input
>
<button type="button" class="upstream-data-date__clear" data-date-clear hidden aria-label="清除结束日期">
<span class="material-icons-outlined" aria-hidden="true">close</span>
</button>
<button type="button" class="upstream-data-date__pick" data-date-toggle aria-label="选择完成结束日期">
<span class="material-icons-outlined" aria-hidden="true">calendar_today</span>
</button>
<div class="upstream-data-calendar" role="dialog" aria-label="完成结束日期日历" data-date-calendar hidden></div>
</span>
</div>
</div>
<div class="upstream-data-field">
<label for="small-payment-work-order-no">工单编号</label>
@ -138,12 +423,49 @@
</div>
<div class="upstream-data-field">
<label for="small-payment-shop">店铺名称</label>
<input id="small-payment-shop" type="search" name="shop_name" value="{{ $filters['shop_name'] }}" list="small-payment-shop-list" autocomplete="off">
<datalist id="small-payment-shop-list">
@foreach ($smallPaymentShops as $shopName)
<option value="{{ $shopName }}">
@endforeach
</datalist>
<div class="upstream-data-combobox" data-filter-combobox>
<input
id="small-payment-shop"
type="search"
name="shop_name"
value="{{ $filters['shop_name'] }}"
autocomplete="off"
role="combobox"
aria-expanded="false"
aria-controls="small-payment-shop-menu"
aria-autocomplete="list"
data-combobox-input
>
<button
type="button"
class="upstream-data-combobox__toggle"
aria-label="展开店铺名称选项"
tabindex="-1"
data-combobox-toggle
>
<span class="material-icons-outlined" aria-hidden="true">expand_more</span>
</button>
<div
id="small-payment-shop-menu"
class="upstream-data-combobox__menu"
role="listbox"
aria-label="店铺名称选项"
data-combobox-menu
hidden
>
@foreach ($smallPaymentShops as $shopName)
<button
type="button"
class="upstream-data-combobox__option"
role="option"
aria-selected="{{ $filters['shop_name'] === $shopName ? 'true' : 'false' }}"
data-combobox-option
data-value="{{ $shopName }}"
>{{ $shopName }}</button>
@endforeach
<p class="upstream-data-combobox__empty" data-combobox-empty hidden>没有匹配的店铺名称</p>
</div>
</div>
</div>
<div class="upstream-data-field">
<label for="small-payment-transaction">支付宝批次号</label>
@ -164,12 +486,49 @@
</div>
<div class="upstream-data-field">
<label for="small-payment-reason">打款原因</label>
<input id="small-payment-reason" type="search" name="payment_reason" value="{{ $filters['payment_reason'] }}" list="small-payment-reason-list" autocomplete="off">
<datalist id="small-payment-reason-list">
@foreach ($smallPaymentReasons as $reasonOption)
<option value="{{ $reasonOption }}">
@endforeach
</datalist>
<div class="upstream-data-combobox" data-filter-combobox>
<input
id="small-payment-reason"
type="search"
name="payment_reason"
value="{{ $filters['payment_reason'] }}"
autocomplete="off"
role="combobox"
aria-expanded="false"
aria-controls="small-payment-reason-menu"
aria-autocomplete="list"
data-combobox-input
>
<button
type="button"
class="upstream-data-combobox__toggle"
aria-label="展开打款原因选项"
tabindex="-1"
data-combobox-toggle
>
<span class="material-icons-outlined" aria-hidden="true">expand_more</span>
</button>
<div
id="small-payment-reason-menu"
class="upstream-data-combobox__menu"
role="listbox"
aria-label="打款原因选项"
data-combobox-menu
hidden
>
@foreach ($smallPaymentReasons as $reasonOption)
<button
type="button"
class="upstream-data-combobox__option"
role="option"
aria-selected="{{ $filters['payment_reason'] === $reasonOption ? 'true' : 'false' }}"
data-combobox-option
data-value="{{ $reasonOption }}"
>{{ $reasonOption }}</button>
@endforeach
<p class="upstream-data-combobox__empty" data-combobox-empty hidden>没有匹配的打款原因</p>
</div>
</div>
</div>
<div class="upstream-data-actions">
<button class="upstream-data-button upstream-data-button--primary" type="submit">
@ -273,4 +632,389 @@
</div>
</div>
</div>
<script>
(function () {
'use strict';
var root = document.querySelector('[data-small-payment-work-orders="true"]');
if (!root) {
return;
}
function setupCombobox(combobox) {
var input = combobox.querySelector('[data-combobox-input]');
var menu = combobox.querySelector('[data-combobox-menu]');
var toggle = combobox.querySelector('[data-combobox-toggle]');
var empty = combobox.querySelector('[data-combobox-empty]');
if (!input || !menu) {
return;
}
var options = Array.prototype.slice.call(menu.querySelectorAll('[data-combobox-option]'));
options.forEach(function (option, index) {
option.id = menu.id + '-option-' + index;
});
var activeIndex = -1;
function isOpen() {
return !menu.hidden;
}
function visibleOptions() {
return options.filter(function (option) {
return !option.hidden;
});
}
function paint() {
options.forEach(function (option, index) {
var isActive = index === activeIndex;
option.classList.toggle('is-active', isActive);
option.setAttribute('aria-selected', option.dataset.value === input.value ? 'true' : 'false');
if (isActive) {
input.setAttribute('aria-activedescendant', option.id);
}
});
var active = options[activeIndex];
if (active) {
active.scrollIntoView({ block: 'nearest' });
} else {
input.removeAttribute('aria-activedescendant');
}
}
function applyFilter() {
var needle = input.value.trim().toLowerCase();
var matched = 0;
options.forEach(function (option) {
var text = String(option.dataset.value || '').toLowerCase();
var hit = needle === '' || text.indexOf(needle) > -1;
option.hidden = !hit;
if (hit) {
matched += 1;
}
});
if (empty) {
empty.hidden = matched !== 0;
}
var visible = visibleOptions();
if (activeIndex === -1 || options[activeIndex].hidden) {
activeIndex = visible.length ? options.indexOf(visible[0]) : -1;
}
paint();
}
function open() {
if (isOpen()) {
return;
}
menu.hidden = false;
combobox.classList.add('is-open');
input.setAttribute('aria-expanded', 'true');
applyFilter();
}
function close() {
if (!isOpen()) {
return;
}
menu.hidden = true;
combobox.classList.remove('is-open');
input.setAttribute('aria-expanded', 'false');
input.removeAttribute('aria-activedescendant');
activeIndex = -1;
options.forEach(function (option) {
option.classList.remove('is-active');
});
}
function move(step) {
var visible = visibleOptions();
if (!visible.length) {
return;
}
var current = visible.indexOf(options[activeIndex]);
var next = current === -1 ? (step > 0 ? 0 : visible.length - 1) : current + step;
if (next < 0) {
next = visible.length - 1;
}
if (next >= visible.length) {
next = 0;
}
activeIndex = options.indexOf(visible[next]);
paint();
}
function pick(option) {
input.value = String(option.dataset.value || option.textContent || '').trim();
close();
input.focus();
}
input.addEventListener('click', open);
input.addEventListener('input', function () {
open();
applyFilter();
});
input.addEventListener('keydown', function (event) {
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
event.preventDefault();
if (!isOpen()) {
open();
return;
}
move(event.key === 'ArrowDown' ? 1 : -1);
return;
}
if (event.key === 'Escape') {
close();
return;
}
// 回车:面板打开且有高亮项时先选中,再按一次才提交查询
if (event.key === 'Enter' && isOpen()) {
var active = options[activeIndex];
if (active && !active.hidden) {
event.preventDefault();
pick(active);
}
}
});
if (toggle) {
toggle.addEventListener('click', function () {
if (isOpen()) {
close();
} else {
open();
}
input.focus();
});
}
options.forEach(function (option) {
// 按下时阻止默认行为,避免输入框先失焦导致点击落空
option.addEventListener('mousedown', function (event) {
event.preventDefault();
});
option.addEventListener('mouseenter', function () {
activeIndex = options.indexOf(option);
paint();
});
option.addEventListener('click', function () {
pick(option);
});
});
document.addEventListener('click', function (event) {
if (!combobox.contains(event.target)) {
close();
}
});
}
var datePickerClosers = [];
function pad2(number) {
return (number < 10 ? '0' : '') + number;
}
function isoDate(date) {
return date.getFullYear() + '-' + pad2(date.getMonth() + 1) + '-' + pad2(date.getDate());
}
function parseIsoDate(text) {
var match = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(text || '').trim());
if (!match) {
return null;
}
var date = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
return date.getFullYear() === Number(match[1])
&& date.getMonth() === Number(match[2]) - 1
&& date.getDate() === Number(match[3])
? date
: null;
}
function sameDay(left, right) {
return left.getFullYear() === right.getFullYear()
&& left.getMonth() === right.getMonth()
&& left.getDate() === right.getDate();
}
var WEEK_LABELS = ['日', '一', '二', '三', '四', '五', '六'];
function setupDatePicker(picker) {
var input = picker.querySelector('[data-date-input]');
var panel = picker.querySelector('[data-date-calendar]');
var toggle = picker.querySelector('[data-date-toggle]');
var clear = picker.querySelector('[data-date-clear]');
if (!input || !panel || !toggle) {
return;
}
var today = new Date();
var selected = parseIsoDate(input.value);
var anchor = selected || today;
var viewYear = anchor.getFullYear();
var viewMonth = anchor.getMonth();
function isOpen() {
return !panel.hidden;
}
function syncClear() {
if (clear) {
clear.hidden = input.value === '';
}
}
function render() {
var monthStart = new Date(viewYear, viewMonth, 1);
var gridStart = new Date(viewYear, viewMonth, 1 - monthStart.getDay());
var html = '<div class="upstream-data-calendar__head">'
+ '<button type="button" class="upstream-data-calendar__nav" data-calendar-nav="-1" aria-label="上个月">'
+ '<span class="material-icons-outlined" aria-hidden="true">chevron_left</span></button>'
+ '<span class="upstream-data-calendar__title">' + viewYear + ' 年 ' + (viewMonth + 1) + ' 月</span>'
+ '<button type="button" class="upstream-data-calendar__nav" data-calendar-nav="1" aria-label="下个月">'
+ '<span class="material-icons-outlined" aria-hidden="true">chevron_right</span></button>'
+ '</div>'
+ '<div class="upstream-data-calendar__week">'
+ WEEK_LABELS.map(function (label) {
return '<span>' + label + '</span>';
}).join('')
+ '</div>'
+ '<div class="upstream-data-calendar__grid">';
for (var offset = 0; offset < 42; offset += 1) {
var day = new Date(gridStart.getFullYear(), gridStart.getMonth(), gridStart.getDate() + offset);
var classes = 'upstream-data-calendar__day';
if (day.getMonth() !== viewMonth) {
classes += ' is-outside';
}
if (sameDay(day, today)) {
classes += ' is-today';
}
if (selected && sameDay(day, selected)) {
classes += ' is-selected';
}
html += '<button type="button" class="' + classes + '" data-calendar-day="'
+ isoDate(day) + '">' + day.getDate() + '</button>';
}
panel.innerHTML = html + '</div>';
}
function close() {
if (!isOpen()) {
return;
}
panel.hidden = true;
toggle.setAttribute('aria-expanded', 'false');
}
function open() {
datePickerClosers.forEach(function (otherClose) {
if (otherClose !== close) {
otherClose();
}
});
if (isOpen()) {
return;
}
selected = parseIsoDate(input.value);
anchor = selected || new Date();
viewYear = anchor.getFullYear();
viewMonth = anchor.getMonth();
render();
panel.hidden = false;
toggle.setAttribute('aria-expanded', 'true');
}
function shiftMonth(step) {
anchor = new Date(viewYear, viewMonth + step, 1);
viewYear = anchor.getFullYear();
viewMonth = anchor.getMonth();
render();
}
datePickerClosers.push(close);
toggle.setAttribute('aria-expanded', 'false');
panel.addEventListener('click', function (event) {
var nav = event.target.closest('[data-calendar-nav]');
if (nav && panel.contains(nav)) {
shiftMonth(Number(nav.getAttribute('data-calendar-nav')));
return;
}
var day = event.target.closest('[data-calendar-day]');
if (day && panel.contains(day)) {
input.value = day.getAttribute('data-calendar-day');
selected = parseIsoDate(input.value);
syncClear();
close();
}
});
input.addEventListener('click', open);
input.addEventListener('keydown', function (event) {
if (event.key === 'Enter' || event.key === ' ' || event.key === 'ArrowDown') {
event.preventDefault();
open();
}
});
// Esc 在整个控件范围内生效:焦点可能已经落到日历里的日期按钮上
picker.addEventListener('keydown', function (event) {
if (event.key === 'Escape') {
close();
}
});
toggle.addEventListener('click', function () {
if (isOpen()) {
close();
} else {
open();
}
});
if (clear) {
clear.addEventListener('click', function () {
input.value = '';
selected = null;
syncClear();
close();
});
}
document.addEventListener('click', function (event) {
if (!picker.contains(event.target)) {
close();
}
});
syncClear();
}
root.querySelectorAll('[data-filter-combobox]').forEach(setupCombobox);
root.querySelectorAll('[data-date-picker]').forEach(setupDatePicker);
})();
</script>
</div>

View File

@ -84,10 +84,11 @@ class SmallPaymentWorkOrderPageTest extends FeatureTestCase
->assertSee('data-filter-contract="small-payment-work-orders.filters"', false)
->assertSee('data-sync-state="syncing"', false)
->assertSeeText('业务镜像同步中,当前显示最近成功数据')
->assertSeeText('完成开始时间')
->assertSeeText('完成结束时间')
->assertSee('value="2026-09-01T08:30"', false)
->assertSee('value="2026-09-03T17:45"', false)
->assertSeeText('完成日期')
->assertSee('value="2026-09-01"', false)
->assertSee('value="2026-09-03"', false)
->assertSee('data-date-picker', false)
->assertSee('data-date-toggle', false)
->assertSee('aria-label="聚水潭小额打款工单"', false)
->assertSeeText('GD-001')
->assertSeeText('支付宝已打款')
@ -138,6 +139,55 @@ class SmallPaymentWorkOrderPageTest extends FeatureTestCase
$this->assertStringContainsString('50.00', $content);
}
/**
* 筛选下拉必须是自绘的可滚动面板:原生 <datalist> 的面板由浏览器绘制,
* 高度与滚动条无法控制,店铺有 200+ 个选项时看不全也滑不动。
*/
public function test_filter_dropdowns_are_scrollable_option_panels(): void
{
$this->grantAdminPermission();
setting()->set('default.list_limit', 25);
$states = [
SmallPaymentWorkOrderMirrorRepository::WORK_ORDER_DATASET => [
'status' => 'succeeded', 'initial_completed_at' => '2026-09-01 00:00:00',
],
SmallPaymentWorkOrderMirrorRepository::PAYMENT_MAP_DATASET => [
'status' => 'succeeded', 'initial_completed_at' => '2026-09-01 00:00:00',
],
];
$repository = \Mockery::mock(SmallPaymentWorkOrderMirrorRepository::class);
$repository->shouldReceive('available')->once()->andReturnTrue();
$repository->shouldReceive('states')->once()->andReturn($states);
$repository->shouldReceive('syncStatus')->once()->with($states)->andReturn([
'code' => 'healthy', 'last_succeeded_at' => '2026-09-03 12:00:00',
]);
$repository->shouldReceive('ready')->once()->andReturnTrue();
$repository->shouldReceive('queryReady')->once()->andReturnTrue();
$repository->shouldReceive('paginate')->once()->andReturn([
'data' => [], 'total' => 0,
'page_payment_amount' => '0.00', 'filtered_payment_amount' => '0.00',
]);
$repository->shouldReceive('filterOptions')->once()->andReturn([
'shops' => ['京东-HX旗舰店101', '京东-HX旗舰店102'],
'payment_reasons' => ['买家原因/买家-不想要'],
]);
$this->app->instance(SmallPaymentWorkOrderMirrorRepository::class, $repository);
$this->loginAs()->get(route('business.pages.show', [
'section' => 'financial-operations', 'page' => 'small-payment-work-orders',
]))
->assertOk()
->assertSee('data-filter-combobox', false)
->assertSee('class="upstream-data-combobox__menu"', false)
// 唯一能锁住"可滚动"的常量:面板有最大高度且内部滚动
->assertSee('max-height: min(18rem, 50vh)', false)
->assertSee('data-combobox-option', false)
->assertSee('data-value="京东-HX旗舰店101"', false)
->assertSee('data-combobox-empty', false)
->assertDontSee('list="small-payment', false)
->assertDontSee('<datalist id=', false);
}
public function test_page_does_not_report_empty_before_both_mirrors_finish(): void
{
$this->grantAdminPermission();