From 5ff4cbd7582eb39ce4a8305832514515a22752e2 Mon Sep 17 00:00:00 2001 From: yueyang <2495544247@qq.com> Date: Sun, 27 Sep 2026 10:49:51 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(small-payment-work-orders):=20?= =?UTF-8?q?=E7=AD=9B=E9=80=89=E4=B8=8B=E6=8B=89=E6=94=B9=E4=B8=BA=E5=8F=AF?= =?UTF-8?q?=E6=BB=9A=E5=8A=A8=E9=80=89=E9=A1=B9=E9=9D=A2=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原生 的面板由浏览器绘制,高度与滚动条无法控制, 店铺有 271 个选项时看不全也滑不动,因此改为自绘面板: - 面板 max-height: min(18rem, 50vh) + overflow: auto,选项在面板内滚动 - 打字即时过滤、方向键移动、回车选中、Esc 或点击外部关闭 - 选项与切换按钮均为 type=button,点击不会误提交表单 - 输入框保留 name/value,筛选提交行为不变 - 无匹配时显示空提示;样式对齐 .finance-table-field-picker__panel - 补 Feature 断言锁住可滚动契约,避免被改回 datalist --- .../small-payment-work-orders.blade.php | 384 +++++++++++++++++- .../SmallPaymentWorkOrderPageTest.php | 49 +++ 2 files changed, 421 insertions(+), 12 deletions(-) diff --git a/resources/views/business/pages/refunds/small-payment-work-orders.blade.php b/resources/views/business/pages/refunds/small-payment-work-orders.blade.php index 14efb903..3dfc6eb7 100644 --- a/resources/views/business/pages/refunds/small-payment-work-orders.blade.php +++ b/resources/views/business/pages/refunds/small-payment-work-orders.blade.php @@ -96,6 +96,97 @@ background: rgba(147, 95, 7, 0.06); color: var(--vip-warning, #935f07); } + + /* + * 筛选下拉:可滚动的选项面板。 + * 原生 的面板由浏览器绘制,高度/滚动条无法控制(选项多时看不全也滑不动), + * 因此改为自绘面板:固定最大高度 + 内部滚动,风格对齐 .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; }
@@ -138,12 +229,49 @@
- - - @foreach ($smallPaymentShops as $shopName) - +
+ + + +
@@ -164,12 +292,49 @@
- - - @foreach ($smallPaymentReasons as $reasonOption) - +
+ + + +
+ + diff --git a/tests/Feature/Business/SmallPaymentWorkOrderPageTest.php b/tests/Feature/Business/SmallPaymentWorkOrderPageTest.php index 51645dfe..2e1f8554 100644 --- a/tests/Feature/Business/SmallPaymentWorkOrderPageTest.php +++ b/tests/Feature/Business/SmallPaymentWorkOrderPageTest.php @@ -138,6 +138,55 @@ class SmallPaymentWorkOrderPageTest extends FeatureTestCase $this->assertStringContainsString('50.00', $content); } + /** + * 筛选下拉必须是自绘的可滚动面板:原生 的面板由浏览器绘制, + * 高度与滚动条无法控制,店铺有 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('
@@ -215,13 +373,49 @@ action="{{ route('business.pages.show', ['section' => 'financial-operations', 'page' => 'small-payment-work-orders']) }}" data-filter-contract="small-payment-work-orders.filters" > -
- - -
-
- - +
+ 完成日期 +
+ + + + + + + 至 + + + + + + +
@@ -630,7 +824,197 @@ }); } + 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 = '
' + + '' + + '' + viewYear + ' 年 ' + (viewMonth + 1) + ' 月' + + '' + + '
' + + '
' + + WEEK_LABELS.map(function (label) { + return '' + label + ''; + }).join('') + + '
' + + '
'; + + 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 += ''; + } + + panel.innerHTML = html + '
'; + } + + 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); })();
diff --git a/tests/Feature/Business/SmallPaymentWorkOrderPageTest.php b/tests/Feature/Business/SmallPaymentWorkOrderPageTest.php index 2e1f8554..93c15bda 100644 --- a/tests/Feature/Business/SmallPaymentWorkOrderPageTest.php +++ b/tests/Feature/Business/SmallPaymentWorkOrderPageTest.php @@ -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('支付宝已打款') -- 2.47.2