小额打款工单:筛选下拉可滚动 + 完成日期改为账单页同款日历 #276
@ -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>
|
||||
|
||||
@ -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();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user