feat(发货数据): 供应商/店铺/仓库/快递公司筛选框自动补全
- 新增只读取值接口 shipping-data/suggest:按字段去重返回匹配候选(供应商/店铺名称/仓库/快递公司,快递取 express_name1/2 并集) - ShippingOrderRepository::distinctValues 去重查询 - 前端 typeahead:输入弹下拉,支持鼠标点选与上下键/回车选择 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014nvGZkg27YMEdhRy6EiUwf
This commit is contained in:
parent
3766fe4d4b
commit
b9e0835ef5
@ -824,6 +824,25 @@ class Pages extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
public function suggestShippingData(Request $request): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
$allowed = ['supplier_name', 'shop_name', 'warehouse_name', 'express_company'];
|
||||
$field = (string) $request->query('field', '');
|
||||
if (! in_array($field, $allowed, true)) {
|
||||
return response()->json(['items' => []]);
|
||||
}
|
||||
$keyword = mb_substr(trim((string) $request->query('q', '')), 0, 64);
|
||||
|
||||
try {
|
||||
$items = $this->shippingOrders->distinctValues($field, $keyword, 20);
|
||||
} catch (Throwable $exception) {
|
||||
report($exception);
|
||||
$items = [];
|
||||
}
|
||||
|
||||
return response()->json(['items' => array_values($items)]);
|
||||
}
|
||||
|
||||
/** @return array<string,string> */
|
||||
private function shippingDataFilters(Request $request): array
|
||||
{
|
||||
|
||||
@ -114,6 +114,47 @@ class ShippingOrderRepository
|
||||
return '%' . str_replace(['!', '%', '_'], ['!!', '!%', '!_'], trim($value)) . '%';
|
||||
}
|
||||
|
||||
/**
|
||||
* 取某字段去重候选值(供筛选框自动补全)。
|
||||
* @return array<int,string>
|
||||
*/
|
||||
public function distinctValues(string $field, string $keyword, int $limit = 20): array
|
||||
{
|
||||
$map = [
|
||||
'supplier_name' => ['supplier_name'],
|
||||
'shop_name' => ['shop_name'],
|
||||
'warehouse_name' => ['warehouse_name'],
|
||||
'express_company' => ['express_name1', 'express_name2'],
|
||||
];
|
||||
if (! isset($map[$field])) {
|
||||
return [];
|
||||
}
|
||||
$limit = min(max($limit, 1), 50);
|
||||
$like = $this->contains($keyword);
|
||||
$values = [];
|
||||
foreach ($map[$field] as $col) {
|
||||
$rows = $this->db->table('jst_oms_shipping')
|
||||
->selectRaw($col . ' as v')
|
||||
->whereRaw($col . " LIKE ? ESCAPE '!'", [$like])
|
||||
->whereNotNull($col)
|
||||
->where($col, '<>', '')
|
||||
->groupBy($col)
|
||||
->orderBy($col)
|
||||
->limit($limit)
|
||||
->pluck('v');
|
||||
foreach ($rows as $v) {
|
||||
$v = trim((string) $v);
|
||||
if ($v !== '') {
|
||||
$values[$v] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
$values = array_keys($values);
|
||||
sort($values, SORT_FLAG_CASE | SORT_STRING);
|
||||
|
||||
return array_slice($values, 0, $limit);
|
||||
}
|
||||
|
||||
/** 同步状态(供页面显示同步中/最近完成)。 */
|
||||
public function syncStatus(): array
|
||||
{
|
||||
|
||||
@ -85,6 +85,11 @@
|
||||
.shipping-data-page .sd-table tbody tr:hover { background: #fcfaf7; }
|
||||
.shipping-data-page .sd-mono { font-family: ui-monospace, Menlo, Consolas, monospace; font-size: .72rem; color: #4b3a24; }
|
||||
.shipping-data-page .sd-scroll { overflow-x: auto; max-width: 100%; }
|
||||
.shipping-data-page .sd-ac-field { position: relative; }
|
||||
.shipping-data-page .sd-ac-list { position: absolute; top: 100%; left: 0; right: 0; z-index: 40; margin-top: 2px; max-height: 15rem; overflow-y: auto; list-style: none; padding: 4px; background: #fff; border: 1px solid #d8d2c7; border-radius: .5rem; box-shadow: 0 8px 24px rgba(0,0,0,.10); }
|
||||
.shipping-data-page .sd-ac-item { padding: .35rem .5rem; font-size: .8rem; border-radius: .375rem; cursor: pointer; color: #29251f; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.shipping-data-page .sd-ac-item:hover, .shipping-data-page .sd-ac-item.is-active { background: #f3ece0; }
|
||||
.shipping-data-page .sd-ac-empty { padding: .35rem .5rem; font-size: .75rem; color: #9a9182; }
|
||||
</style>
|
||||
|
||||
<div class="flex flex-col gap-4 mb-5 sm:flex-row sm:items-start sm:justify-between">
|
||||
@ -103,14 +108,14 @@
|
||||
@endif
|
||||
|
||||
<div class="mb-5 rounded-lg border border-[#e7e0d5] bg-white p-4 shadow-sm">
|
||||
<form method="GET" class="flex flex-wrap items-end gap-3" aria-label="发货数据筛选">
|
||||
<form method="GET" class="flex flex-wrap items-end gap-3" aria-label="发货数据筛选" data-suggest-url="{{ route('business.courier-payables.shipping-data.suggest') }}">
|
||||
<label class="sd-filter sd-filter--wide flex flex-col gap-1 text-xs text-gray-500" for="shipping-express-no"><span>快递单号</span><input id="shipping-express-no" class="sd-control" type="text" name="express_no" value="{{ $filters['express_no'] ?? '' }}" placeholder="精确匹配"></label>
|
||||
<label class="sd-filter sd-filter--wide flex flex-col gap-1 text-xs text-gray-500" for="shipping-trade-no"><span>交易单号</span><input id="shipping-trade-no" class="sd-control" type="text" name="sourceno" value="{{ $filters['sourceno'] ?? '' }}" placeholder="精确匹配"></label>
|
||||
<label class="sd-filter flex flex-col gap-1 text-xs text-gray-500" for="shipping-barcode"><span>条形码</span><input id="shipping-barcode" class="sd-control" type="text" name="barcode" value="{{ $filters['barcode'] ?? '' }}" placeholder="条形码关键词"></label>
|
||||
<label class="sd-filter flex flex-col gap-1 text-xs text-gray-500" for="shipping-supplier"><span>供应商</span><input id="shipping-supplier" class="sd-control" type="text" name="supplier_name" value="{{ $filters['supplier_name'] ?? '' }}" placeholder="名称关键词"></label>
|
||||
<label class="sd-filter flex flex-col gap-1 text-xs text-gray-500" for="shipping-shop"><span>店铺名称</span><input id="shipping-shop" class="sd-control" type="text" name="shop_name" value="{{ $filters['shop_name'] ?? '' }}" placeholder="名称关键词"></label>
|
||||
<label class="sd-filter flex flex-col gap-1 text-xs text-gray-500" for="shipping-warehouse"><span>仓库</span><input id="shipping-warehouse" class="sd-control" type="text" name="warehouse_name" value="{{ $filters['warehouse_name'] ?? '' }}" placeholder="仓库关键词"></label>
|
||||
<label class="sd-filter flex flex-col gap-1 text-xs text-gray-500" for="shipping-courier"><span>快递公司</span><input id="shipping-courier" class="sd-control" type="text" name="express_company" value="{{ $filters['express_company'] ?? '' }}" placeholder="匹配快递名称1/2"></label>
|
||||
<label class="sd-filter flex flex-col gap-1 text-xs text-gray-500" for="shipping-supplier"><span>供应商</span><input id="shipping-supplier" class="sd-control" type="text" name="supplier_name" data-suggest="supplier_name" autocomplete="off" value="{{ $filters['supplier_name'] ?? '' }}" placeholder="名称关键词"></label>
|
||||
<label class="sd-filter flex flex-col gap-1 text-xs text-gray-500" for="shipping-shop"><span>店铺名称</span><input id="shipping-shop" class="sd-control" type="text" name="shop_name" data-suggest="shop_name" autocomplete="off" value="{{ $filters['shop_name'] ?? '' }}" placeholder="名称关键词"></label>
|
||||
<label class="sd-filter flex flex-col gap-1 text-xs text-gray-500" for="shipping-warehouse"><span>仓库</span><input id="shipping-warehouse" class="sd-control" type="text" name="warehouse_name" data-suggest="warehouse_name" autocomplete="off" value="{{ $filters['warehouse_name'] ?? '' }}" placeholder="仓库关键词"></label>
|
||||
<label class="sd-filter flex flex-col gap-1 text-xs text-gray-500" for="shipping-courier"><span>快递公司</span><input id="shipping-courier" class="sd-control" type="text" name="express_company" data-suggest="express_company" autocomplete="off" value="{{ $filters['express_company'] ?? '' }}" placeholder="匹配快递名称1/2"></label>
|
||||
<label class="sd-filter flex flex-col gap-1 text-xs text-gray-500" for="shipping-from"><span>发货时间起</span><span class="sd-date-picker"><input id="shipping-from" class="sd-control" type="date" name="ship_from" value="{{ $filters['ship_from'] ?? '' }}"><button type="button" data-date-picker-input="shipping-from" aria-label="选择发货开始日期" title="选择发货开始日期"><span class="material-icons-outlined" aria-hidden="true">calendar_today</span></button></span></label>
|
||||
<label class="sd-filter flex flex-col gap-1 text-xs text-gray-500" for="shipping-to"><span>发货时间止</span><span class="sd-date-picker"><input id="shipping-to" class="sd-control" type="date" name="ship_to" value="{{ $filters['ship_to'] ?? '' }}"><button type="button" data-date-picker-input="shipping-to" aria-label="选择发货结束日期" title="选择发货结束日期"><span class="material-icons-outlined" aria-hidden="true">calendar_today</span></button></span></label>
|
||||
<div class="flex gap-2">
|
||||
@ -163,5 +168,72 @@
|
||||
else input.click();
|
||||
});
|
||||
});
|
||||
|
||||
// 筛选框自动补全:输入关键词弹下拉,点选或键盘选择填入。
|
||||
(() => {
|
||||
const form = document.querySelector('.shipping-data-page form[aria-label="发货数据筛选"]');
|
||||
const url = form ? form.dataset.suggestUrl : '';
|
||||
if (!form || !url) return;
|
||||
|
||||
form.querySelectorAll('input[data-suggest]').forEach((input) => {
|
||||
const label = input.closest('label');
|
||||
if (!label) return;
|
||||
label.classList.add('sd-ac-field');
|
||||
|
||||
const list = document.createElement('ul');
|
||||
list.className = 'sd-ac-list';
|
||||
list.hidden = true;
|
||||
label.appendChild(list);
|
||||
|
||||
let timer = null;
|
||||
let active = -1;
|
||||
let seq = 0;
|
||||
|
||||
const hide = () => { list.hidden = true; list.innerHTML = ''; active = -1; };
|
||||
|
||||
const render = (items) => {
|
||||
list.innerHTML = '';
|
||||
active = -1;
|
||||
if (!items.length) { hide(); return; }
|
||||
items.forEach((it) => {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'sd-ac-item';
|
||||
li.textContent = it;
|
||||
li.title = it;
|
||||
li.addEventListener('mousedown', (e) => { e.preventDefault(); input.value = it; hide(); });
|
||||
list.appendChild(li);
|
||||
});
|
||||
list.hidden = false;
|
||||
};
|
||||
|
||||
const load = () => {
|
||||
const my = ++seq;
|
||||
const q = input.value.trim();
|
||||
const u = url + '?field=' + encodeURIComponent(input.dataset.suggest) + '&q=' + encodeURIComponent(q);
|
||||
fetch(u, { headers: { 'X-Requested-With': 'XMLHttpRequest' }, credentials: 'same-origin' })
|
||||
.then((r) => r.ok ? r.json() : { items: [] })
|
||||
.then((d) => { if (my === seq) render(Array.isArray(d.items) ? d.items : []); })
|
||||
.catch(() => { if (my === seq) hide(); });
|
||||
};
|
||||
|
||||
const schedule = (delay) => { clearTimeout(timer); timer = setTimeout(load, delay); };
|
||||
|
||||
input.addEventListener('input', () => schedule(200));
|
||||
input.addEventListener('focus', () => schedule(120));
|
||||
input.addEventListener('blur', () => setTimeout(hide, 150));
|
||||
input.addEventListener('keydown', (e) => {
|
||||
if (list.hidden) return;
|
||||
const items = list.querySelectorAll('.sd-ac-item');
|
||||
if (!items.length) return;
|
||||
if (e.key === 'ArrowDown') { e.preventDefault(); active = Math.min(active + 1, items.length - 1); }
|
||||
else if (e.key === 'ArrowUp') { e.preventDefault(); active = Math.max(active - 1, 0); }
|
||||
else if (e.key === 'Enter') { if (active >= 0) { e.preventDefault(); input.value = items[active].textContent; hide(); } return; }
|
||||
else if (e.key === 'Escape') { hide(); return; }
|
||||
else return;
|
||||
items.forEach((li, i) => li.classList.toggle('is-active', i === active));
|
||||
items[active].scrollIntoView({ block: 'nearest' });
|
||||
});
|
||||
});
|
||||
})();
|
||||
})();
|
||||
</script>
|
||||
|
||||
@ -147,6 +147,10 @@ Route::get('workspace/courier-payables/shipping-data/export', 'Business\Pages@ex
|
||||
->middleware('business.page:courier-payables,shipping-data')
|
||||
->name('business.courier-payables.shipping-data.export');
|
||||
|
||||
Route::get('workspace/courier-payables/shipping-data/suggest', 'Business\\Pages@suggestShippingData')
|
||||
->middleware('business.page:courier-payables,shipping-data')
|
||||
->name('business.courier-payables.shipping-data.suggest');
|
||||
|
||||
Route::get('workspace/courier-payables/third-party-warehouse-storage/export', 'Business\Pages@exportThirdPartyWarehouseStorage')
|
||||
->middleware('business.page:courier-payables,third-party-warehouse-storage')
|
||||
->name('business.courier-payables.third-party-warehouse-storage.export');
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user