feat(client): 库存规格/系列筛选改可搜索单选下拉、仓库单选,修复下拉过宽过高溢出
原型与代码同提交(design-first):下拉宽度锚定触发器宽 max(宽,220)、列表 264 封顶超出滚动、底部计数常驻。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bupi8Kdqkfx2N5acFsHTx5
This commit is contained in:
@@ -208,43 +208,43 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
|
||||
if (v != null && mounted) _applyStatusFilter(v);
|
||||
}
|
||||
|
||||
// ── 工具栏多选 chip(规格/系列/仓库,2026-07-07 从列头漏斗上移) ──
|
||||
Widget _multiChip(String label, List<String> options, Set<String> selected,
|
||||
ValueChanged<Set<String>> onChanged) {
|
||||
// ── 工具栏单选 chip(规格/系列/仓库,2026-08-28 从多选改单选,用户口径) ──
|
||||
// 状态仍存 Set<String>(沿用后端 setSpec/setSeries 列表口径 + 仓库客户端 contains),
|
||||
// 单选即「至多一项」:选中 → {v},× → {}。
|
||||
Widget _singleChip(String label, List<String> options, Set<String> selected,
|
||||
ValueChanged<Set<String>> onChanged,
|
||||
{String? searchHint}) {
|
||||
return Builder(
|
||||
builder: (chipCtx) => DsChip(
|
||||
label: label,
|
||||
value: selected.isEmpty
|
||||
? null
|
||||
: selected.length == 1
|
||||
? selected.first
|
||||
: '${selected.length} 项',
|
||||
onTap: () => _openMultiFilter(chipCtx,
|
||||
options: options, selected: selected, onChanged: onChanged),
|
||||
value: selected.isEmpty ? null : selected.first,
|
||||
onTap: () => _openSingleFilter(chipCtx,
|
||||
options: options,
|
||||
selected: selected,
|
||||
onChanged: onChanged,
|
||||
searchHint: searchHint),
|
||||
onClear: () => onChanged({}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ── 多选菜单(规格/系列/仓库,chip 锚定) ──
|
||||
void _openMultiFilter(
|
||||
// ── 单选菜单(规格/系列可搜索、仓库普通,chip 锚定) ──
|
||||
Future<void> _openSingleFilter(
|
||||
BuildContext anchorContext, {
|
||||
required List<String> options,
|
||||
required Set<String> selected,
|
||||
required ValueChanged<Set<String>> onChanged,
|
||||
}) {
|
||||
var cur = Set.of(selected);
|
||||
showDsMultiMenu<String>(
|
||||
String? searchHint, // 非空 → 可搜索单选下拉(规格/系列长名录)
|
||||
}) async {
|
||||
final v = await showDsMenu<String>(
|
||||
anchorContext,
|
||||
itemsBuilder: () => [
|
||||
searchHint: searchHint,
|
||||
items: [
|
||||
for (final o in options)
|
||||
DsMenuItem(value: o, label: o, selected: cur.contains(o)),
|
||||
DsMenuItem(value: o, label: o, selected: selected.contains(o)),
|
||||
],
|
||||
onToggle: (v) {
|
||||
cur.contains(v) ? cur.remove(v) : cur.add(v);
|
||||
onChanged(Set.of(cur));
|
||||
},
|
||||
);
|
||||
if (v != null && mounted) onChanged({v});
|
||||
}
|
||||
|
||||
// ── 窄屏筛选 sheet(原型移动无列头漏斗 → 详搜钮开底部 sheet)──
|
||||
@@ -937,21 +937,21 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
|
||||
children: [
|
||||
SizedBox(width: 260, child: searchField),
|
||||
const SizedBox(width: AppDims.sp2),
|
||||
_multiChip('规格', specOptions, _filterSpec, (v) {
|
||||
_singleChip('规格', specOptions, _filterSpec, (v) {
|
||||
setState(() => _filterSpec = v);
|
||||
ref
|
||||
.read(inventoryListProvider.notifier)
|
||||
.setSpec(v.toList());
|
||||
}),
|
||||
}, searchHint: '搜索规格…'),
|
||||
const SizedBox(width: AppDims.sp2),
|
||||
_multiChip('系列', seriesOptions, _filterSeries, (v) {
|
||||
_singleChip('系列', seriesOptions, _filterSeries, (v) {
|
||||
setState(() => _filterSeries = v);
|
||||
ref
|
||||
.read(inventoryListProvider.notifier)
|
||||
.setSeries(v.toList());
|
||||
}),
|
||||
}, searchHint: '搜索系列…'),
|
||||
const SizedBox(width: AppDims.sp2),
|
||||
_multiChip('仓库', warehouseOptions, _filterWarehouse,
|
||||
_singleChip('仓库', warehouseOptions, _filterWarehouse,
|
||||
(v) => setState(() => _filterWarehouse = v)),
|
||||
const SizedBox(width: AppDims.sp2),
|
||||
Builder(
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'dart:math' as math;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
|
||||
import '../../core/theme/app_tokens.dart';
|
||||
import '../../core/theme/context_tokens.dart';
|
||||
import '../../core/theme/app_dims.g.dart';
|
||||
|
||||
@@ -25,28 +26,41 @@ class DsMenuItem<T> {
|
||||
}
|
||||
|
||||
/// 单选菜单:点击项返回其 value 并关闭;点外部关闭返回 null。
|
||||
///
|
||||
/// [searchHint] 非空 → 升级为「可搜索单选」下拉(镜像原型 openSearchMenu / .combo-pop,
|
||||
/// 照 stock-out 客户筛选写法):顶部持久搜索框 + 边打边过滤 + 命中高亮 + 底部结果统计,
|
||||
/// 点选即返回并关闭。宽度锚定触发器宽(`max(触发器宽, 220)` 固定,对齐原型 openSearchMenu
|
||||
/// `w = max(r.width, 220)`)、列表 264 封顶,供规格/系列等长名录单选筛选复用。
|
||||
Future<T?> showDsMenu<T>(
|
||||
BuildContext anchorContext, {
|
||||
required List<DsMenuItem<T>> items,
|
||||
double minWidth = 168,
|
||||
String? searchHint,
|
||||
}) {
|
||||
final rect = _anchorRect(anchorContext);
|
||||
return Navigator.of(anchorContext).push<T>(_DsMenuRoute<T>(
|
||||
anchorRect: rect,
|
||||
minWidth: minWidth,
|
||||
builder: (ctx) => _DsMenuPanel(
|
||||
children: [
|
||||
for (final it in items)
|
||||
_DsMenuItemTile(
|
||||
item: it,
|
||||
onTap: () => Navigator.of(ctx).pop(it.value),
|
||||
minWidth: searchHint != null ? math.max(minWidth, 220) : minWidth,
|
||||
search: searchHint != null,
|
||||
builder: (ctx) => searchHint != null
|
||||
? _DsSearchSinglePanel<T>(
|
||||
items: items,
|
||||
hint: searchHint,
|
||||
onPick: (v) => Navigator.of(ctx).pop(v),
|
||||
)
|
||||
: _DsMenuPanel(
|
||||
children: [
|
||||
for (final it in items)
|
||||
_DsMenuItemTile(
|
||||
item: it,
|
||||
onTap: () => Navigator.of(ctx).pop(it.value),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
/// 多选菜单(列设置 / 多选筛选):点击项切换选中、菜单保持打开;点外部关闭。
|
||||
/// 多选菜单(列设置):点击项切换选中、菜单保持打开;点外部关闭。
|
||||
/// [itemsBuilder] 每次切换后重建以刷新 ✓ 态;[onToggle] 通知外部改状态。
|
||||
Future<void> showDsMultiMenu<T>(
|
||||
BuildContext anchorContext, {
|
||||
@@ -89,10 +103,12 @@ Rect _anchorRect(BuildContext anchorContext) {
|
||||
class _DsMenuRoute<T> extends PopupRoute<T> {
|
||||
final Rect anchorRect;
|
||||
final double minWidth;
|
||||
final bool search;
|
||||
final WidgetBuilder builder;
|
||||
_DsMenuRoute({
|
||||
required this.anchorRect,
|
||||
required this.minWidth,
|
||||
this.search = false,
|
||||
required this.builder,
|
||||
});
|
||||
|
||||
@@ -109,7 +125,7 @@ class _DsMenuRoute<T> extends PopupRoute<T> {
|
||||
Widget buildPage(BuildContext context, Animation<double> animation,
|
||||
Animation<double> secondaryAnimation) {
|
||||
return CustomSingleChildLayout(
|
||||
delegate: _DsMenuLayout(anchorRect, minWidth),
|
||||
delegate: _DsMenuLayout(anchorRect, minWidth, search),
|
||||
child: builder(context),
|
||||
);
|
||||
}
|
||||
@@ -118,15 +134,23 @@ class _DsMenuRoute<T> extends PopupRoute<T> {
|
||||
class _DsMenuLayout extends SingleChildLayoutDelegate {
|
||||
final Rect anchor;
|
||||
final double minWidth;
|
||||
_DsMenuLayout(this.anchor, this.minWidth);
|
||||
final bool search;
|
||||
_DsMenuLayout(this.anchor, this.minWidth, this.search);
|
||||
|
||||
@override
|
||||
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
|
||||
final maxW = math.max(minWidth, constraints.maxWidth - 16);
|
||||
final avail = constraints.maxWidth - 16;
|
||||
final maxH = math.max(0.0, constraints.maxHeight - 16);
|
||||
if (search) {
|
||||
// 可搜索下拉:宽度锚定触发器宽(对齐原型 openSearchMenu `w = max(r.width, 220)`),
|
||||
// tight 固定,避免 Expanded 搜索框撑满视口。触发器宽 → 弹层宽;窄 pill → 220。
|
||||
final w = math.min(math.max(anchor.width, minWidth), avail);
|
||||
return BoxConstraints(minWidth: w, maxWidth: w, maxHeight: maxH);
|
||||
}
|
||||
return BoxConstraints(
|
||||
minWidth: math.min(math.max(anchor.width, minWidth), maxW),
|
||||
maxWidth: maxW,
|
||||
maxHeight: math.max(0, constraints.maxHeight - 16),
|
||||
minWidth: math.min(math.max(anchor.width, minWidth), avail),
|
||||
maxWidth: avail,
|
||||
maxHeight: maxH,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -144,7 +168,9 @@ class _DsMenuLayout extends SingleChildLayoutDelegate {
|
||||
|
||||
@override
|
||||
bool shouldRelayout(_DsMenuLayout old) =>
|
||||
old.anchor != anchor || old.minWidth != minWidth;
|
||||
old.anchor != anchor ||
|
||||
old.minWidth != minWidth ||
|
||||
old.search != search;
|
||||
}
|
||||
|
||||
/// .menu:surface / border / r-md / sh-2(0 4px 14px shadow) / pad 6。
|
||||
@@ -224,3 +250,203 @@ class _DsMenuItemTile extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 可搜索单选面板(镜像原型 .combo-pop + openSearchMenu,照 stock-out 客户筛选):
|
||||
/// 顶部 .cp-search 持久搜索框 → .cp-list 列表(命中子串高亮、选中项尾部 ✓、点选即返回并关闭)
|
||||
/// → .cp-foot 结果统计(匹配 X 条 · 共 N 条)。供工具栏规格/系列等长名录单选筛选复用。
|
||||
class _DsSearchSinglePanel<T> extends StatefulWidget {
|
||||
final List<DsMenuItem<T>> items;
|
||||
final ValueChanged<T> onPick;
|
||||
final String hint;
|
||||
const _DsSearchSinglePanel({
|
||||
required this.items,
|
||||
required this.onPick,
|
||||
required this.hint,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_DsSearchSinglePanel<T>> createState() =>
|
||||
_DsSearchSinglePanelState<T>();
|
||||
}
|
||||
|
||||
class _DsSearchSinglePanelState<T> extends State<_DsSearchSinglePanel<T>> {
|
||||
final _ctrl = TextEditingController();
|
||||
final _focus = FocusNode();
|
||||
String _kw = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance
|
||||
.addPostFrameCallback((_) => _focus.requestFocus());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_ctrl.dispose();
|
||||
_focus.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = context.tokens;
|
||||
final all = widget.items;
|
||||
final q = _kw.trim().toLowerCase();
|
||||
final hits =
|
||||
q.isEmpty ? all : all.where((it) => it.label.toLowerCase().contains(q)).toList();
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: Container(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: BoxDecoration(
|
||||
color: t.surface,
|
||||
border: Border.all(color: t.border),
|
||||
borderRadius: BorderRadius.circular(AppDims.rMd),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: t.shadow, offset: const Offset(0, 4), blurRadius: 14),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// .cp-search
|
||||
Container(
|
||||
padding: const EdgeInsets.fromLTRB(11, 9, 11, 9),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(bottom: BorderSide(color: t.borderSubtle)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(LucideIcons.search, size: 14, color: t.faint),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _ctrl,
|
||||
focusNode: _focus,
|
||||
style: TextStyle(fontSize: AppDims.fsBody, color: t.text),
|
||||
cursorColor: t.primary,
|
||||
onChanged: (v) => setState(() => _kw = v),
|
||||
decoration: InputDecoration(
|
||||
isCollapsed: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
border: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
hintText: widget.hint,
|
||||
hintStyle: TextStyle(
|
||||
fontSize: AppDims.fsBody, color: t.faint),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// .cp-list(overflow auto,max-height:264 → 超出滚动、foot 常驻可见;空 → .cp-empty)
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 264),
|
||||
child: hits.isEmpty
|
||||
? Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Text('无匹配结果',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: AppDims.fsSm, color: t.muted)),
|
||||
)
|
||||
: ListView.builder(
|
||||
padding: const EdgeInsets.all(5),
|
||||
shrinkWrap: true,
|
||||
itemCount: hits.length,
|
||||
itemBuilder: (c, i) => _item(t, hits[i]),
|
||||
),
|
||||
),
|
||||
// .cp-foot
|
||||
Container(
|
||||
padding: const EdgeInsets.fromLTRB(12, 7, 12, 7),
|
||||
decoration: BoxDecoration(
|
||||
color: t.bg,
|
||||
border: Border(top: BorderSide(color: t.borderSubtle)),
|
||||
),
|
||||
child: _foot(t, hits.length, all.length, q.isNotEmpty),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// .cp-item:点选即回调 onPick(其内 Navigator.pop 关闭菜单)→ 单选。
|
||||
Widget _item(AppTokens t, DsMenuItem<T> it) {
|
||||
final fg = it.selected ? t.primary : t.text;
|
||||
return InkWell(
|
||||
onTap: () => widget.onPick(it.value),
|
||||
hoverColor: t.rowHover,
|
||||
borderRadius: BorderRadius.circular(AppDims.rSm),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: _highlight(t, it.label, fg, it.selected)),
|
||||
if (it.selected) ...[
|
||||
const SizedBox(width: 9),
|
||||
Icon(LucideIcons.check, size: 15, color: t.primary),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 命中子串高亮(.hl:primary fw700)
|
||||
Widget _highlight(AppTokens t, String name, Color fg, bool sel) {
|
||||
final base = TextStyle(
|
||||
fontSize: AppDims.fsBody,
|
||||
color: fg,
|
||||
fontWeight: sel ? FontWeight.w600 : FontWeight.w400);
|
||||
final kw = _kw.trim();
|
||||
final i =
|
||||
kw.isEmpty ? -1 : name.toLowerCase().indexOf(kw.toLowerCase());
|
||||
if (i < 0) {
|
||||
return Text(name,
|
||||
maxLines: 1, overflow: TextOverflow.ellipsis, style: base);
|
||||
}
|
||||
return RichText(
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
text: TextSpan(style: base, children: [
|
||||
TextSpan(text: name.substring(0, i)),
|
||||
TextSpan(
|
||||
text: name.substring(i, i + kw.length),
|
||||
style: base.copyWith(
|
||||
color: t.primary, fontWeight: FontWeight.w700)),
|
||||
TextSpan(text: name.substring(i + kw.length)),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
// .cp-foot:q 空 → 共 N 条;有关键字 → 匹配 X 条 · 共 N 条。数字 mono 加粗。
|
||||
Widget _foot(AppTokens t, int hits, int total, bool filtered) {
|
||||
final base = TextStyle(fontSize: AppDims.fsXs, color: t.muted);
|
||||
final b = base.copyWith(
|
||||
color: t.text,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontFamily: 'JetBrainsMono');
|
||||
return RichText(
|
||||
text: TextSpan(style: base, children: [
|
||||
if (filtered) ...[
|
||||
const TextSpan(text: '匹配 '),
|
||||
TextSpan(text: '$hits', style: b),
|
||||
const TextSpan(text: ' 条 · 共 '),
|
||||
TextSpan(text: '$total', style: b),
|
||||
const TextSpan(text: ' 条'),
|
||||
] else ...[
|
||||
const TextSpan(text: '共 '),
|
||||
TextSpan(text: '$total', style: b),
|
||||
const TextSpan(text: ' 条'),
|
||||
],
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,17 +126,17 @@ const ITEMS=[
|
||||
const STATUS=['全部','在售','库存','已售'];
|
||||
// 成本价/总价列 = 成本口径,仅管理员可见(非管理员两列隐藏、列设置菜单不出现,2026-07-06);原型=管理员形态
|
||||
const COLS=[{key:'product',label:'商品',fixed:true},{key:'spec',label:'规格'},{key:'series',label:'系列'},{key:'prodDate',label:'生产日期',sort:true},{key:'qty',label:'库存',num:true,sort:true},{key:'cost',label:'成本价',num:true,sort:true},{key:'price',label:'总价',num:true,sort:true},{key:'batch',label:'批次号'},{key:'inDate',label:'入库时间',sort:true},{key:'supplier',label:'供应商'},{key:'status',label:'状态'},{key:'act',label:'操作',fixed:true}];
|
||||
const state={q:'',code:'',status:'全部',page:1,perPage:10,hidden:new Set(),sortBy:null,sortAsc:true,spec:new Set(),series:new Set(),wh:new Set()};
|
||||
const state={q:'',code:'',status:'全部',page:1,perPage:10,hidden:new Set(),sortBy:null,sortAsc:true,spec:'',series:'',wh:''};
|
||||
|
||||
function setActive(k){ document.querySelectorAll('.nav[data-k]').forEach(e=>e.classList.toggle('active',e.dataset.k===k)); }
|
||||
function navTo(k){ setActive(k); const inv=document.querySelector('[data-view="inventory"]'), ph=document.querySelector('[data-view="placeholder"]'); if(k==='inventory'){inv.classList.add('active');ph.classList.remove('active');return;} inv.classList.remove('active');ph.classList.add('active'); const nav=NAVS.find(n=>n.k===k); document.getElementById('phTitle').textContent=nav.label; document.getElementById('phIcon').innerHTML=navSvg(nav.d); }
|
||||
|
||||
function visibleCols(){ return COLS.filter(c=>!state.hidden.has(c.key)); }
|
||||
function filtered(){ const rows=ITEMS.filter(it=>{ if(state.status!=='全部'&&it.status!==state.status)return false; if(state.spec.size&&!state.spec.has(it.spec))return false; if(state.series.size&&!state.series.has(it.series))return false; if(state.wh.size&&!state.wh.has(it.supplier))return false; if(state.q){const q=state.q.toLowerCase(); if(!it.name.toLowerCase().includes(q)&&!it.code.toLowerCase().includes(q))return false;} return true; }); if(state.sortBy&&SORT_VAL[state.sortBy]){const v=SORT_VAL[state.sortBy];rows.sort((a,b)=>{const x=v(a),y=v(b);return (x<y?-1:x>y?1:0)*(state.sortAsc?1:-1);});} return rows; }
|
||||
function filtered(){ const rows=ITEMS.filter(it=>{ if(state.status!=='全部'&&it.status!==state.status)return false; if(state.spec&&it.spec!==state.spec)return false; if(state.series&&it.series!==state.series)return false; if(state.wh&&it.supplier!==state.wh)return false; if(state.q){const q=state.q.toLowerCase(); if(!it.name.toLowerCase().includes(q)&&!it.code.toLowerCase().includes(q))return false;} return true; }); if(state.sortBy&&SORT_VAL[state.sortBy]){const v=SORT_VAL[state.sortBy];rows.sort((a,b)=>{const x=v(a),y=v(b);return (x<y?-1:x>y?1:0)*(state.sortAsc?1:-1);});} return rows; }
|
||||
function render(){
|
||||
document.getElementById('statusVal').textContent=state.status==='全部'?'':state.status;
|
||||
// 多选摘要(镜像 DsChip value):1 项=值本身,多项=「N 项」
|
||||
[['spec','chipSpec','specVal'],['series','chipSeries','seriesVal'],['wh','chipWh','whVal']].forEach(([k,cid,vid])=>{ const s=state[k]; document.getElementById(vid).textContent=!s.size?'':(s.size===1?[...s][0]:`${s.size} 项`); document.getElementById(cid).classList.toggle('on',!!s.size); });
|
||||
// 单选摘要(镜像 DsChip value):选中值本身,未选为空
|
||||
[['spec','chipSpec','specVal'],['series','chipSeries','seriesVal'],['wh','chipWh','whVal']].forEach(([k,cid,vid])=>{ const s=state[k]; document.getElementById(vid).textContent=s||''; document.getElementById(cid).classList.toggle('on',!!s); });
|
||||
document.getElementById('chipStatus').classList.toggle('on',state.status!=='全部');
|
||||
const cols=visibleCols();
|
||||
document.getElementById('thead').innerHTML='<tr>'+cols.map(c=>{ const cls=(c.num?'num ':'')+(c.key==='act'?'act ':''); if(c.filter){const on=state.status!=='全部'; return `<th class="${cls}"><span class="fh ${on?'filtered':''}" onclick="openFilter('${c.filter}',this)">${c.label} <svg class="funnel" viewBox="0 0 24 24"><use href="#i-ic22"/></svg></span></th>`;} if(c.sort){const on=state.sortBy===c.key; const ic=!on?'i-chevron-down':(state.sortAsc?'i-ic51':'i-chevron-down'); return `<th class="${cls}"><span class="sh ${on?'on':''}" onclick="toggleSort('${c.key}')">${c.label} <svg class="sort-ic" viewBox="0 0 24 24"><use href="#${ic}"/></svg></span></th>`;} return `<th class="${cls}">${c.label}</th>`; }).join('')+'</tr>';
|
||||
@@ -155,14 +155,19 @@ function filterStatus(s){ state.status=s; state.page=1; navTo('inventory'); rend
|
||||
// 列头排序:无→升→降→无 循环(真实实现为服务端全局排序)
|
||||
function toggleSort(k){ if(state.sortBy!==k){state.sortBy=k;state.sortAsc=true;} else if(state.sortAsc){state.sortAsc=false;} else {state.sortBy=null;state.sortAsc=true;} state.page=1; render(); }
|
||||
const SORT_VAL={qty:it=>it.qty,cost:it=>parseFloat(String(it.cost).replace(/[¥,]/g,''))||0,price:it=>parseFloat(String(it.price).replace(/[¥,]/g,''))||0,prodDate:it=>it.prodDate||'',inDate:it=>it.inDate||''};
|
||||
function clearFilters(){ state.q='';state.status='全部';state.spec.clear();state.series.clear();state.wh.clear();state.page=1; document.getElementById('searchInput').value=''; render(); }
|
||||
function clearFilters(){ state.q='';state.status='全部';state.spec='';state.series='';state.wh='';state.page=1; document.getElementById('searchInput').value=''; render(); }
|
||||
|
||||
function openFilter(kind,t){ openMenu(t,STATUS.map(v=>({v,label:v,sel:v===state.status})),v=>{ state.status=v; state.page=1; closeMenus(); render(); }); }
|
||||
// 维度筛选 chip(规格/系列/仓库,2026-07-07 从列头漏斗上移):多选,点选即生效、菜单保持打开(镜像 showDsMultiMenu)
|
||||
// 维度筛选 chip(规格/系列/仓库,2026-07-07 从列头漏斗上移):均为单选(2026-08-28 用户口径)。
|
||||
// 规格/系列名录长 → 可搜索单选下拉(openSearchMenu,照 stock-out 客户筛选写法);
|
||||
// 仓库名录短 → 普通单选下拉(openMenu,无搜索框)。
|
||||
const DIM_KEY={spec:'spec',series:'series',wh:'supplier'};
|
||||
function openDimFilter(dim,t){ const key=DIM_KEY[dim]; const opts=[...new Set(ITEMS.map(it=>it[key]).filter(Boolean))]; openMenu(t,opts.map(v=>({v,label:v,sel:state[dim].has(v)})),v=>{ state[dim].has(v)?state[dim].delete(v):state[dim].add(v); state.page=1; render(); openDimFilter(dim,t); }); }
|
||||
const DIM_PH={spec:'搜索规格…',series:'搜索系列…'};
|
||||
function openDimFilter(dim,t){ const key=DIM_KEY[dim]; const opts=[...new Set(ITEMS.map(it=>it[key]).filter(Boolean))];
|
||||
if(dim==='wh'){ openMenu(t,opts.map(v=>({v,label:v,sel:v===state.wh})),v=>{ state.wh=v; state.page=1; closeMenus(); render(); }); return; }
|
||||
openSearchMenu(t,opts.map(v=>({v,label:v,sel:v===state[dim]})),v=>{ state[dim]=v; state.page=1; closeMenus(); render(); },{placeholder:DIM_PH[dim]}); }
|
||||
// chip 尾部 × 单清(镜像 DsChip onClear:只清本维度,不展开菜单)
|
||||
function clearDim(dim,e){ e.stopPropagation(); state[dim].clear(); state.page=1; render(); }
|
||||
function clearDim(dim,e){ e.stopPropagation(); state[dim]=''; state.page=1; render(); }
|
||||
function clearStatus(e){ e.stopPropagation(); state.status='全部'; state.page=1; render(); }
|
||||
document.getElementById('chipStatus').onclick=e=>openFilter('status',e.currentTarget);
|
||||
const COL_KEY='jiu-inv-hidden';
|
||||
|
||||
Vendored
+1
-1
@@ -56,7 +56,7 @@
|
||||
|
||||
// 可搜索菜单:视觉承袭 .combo-pop(顶部 .cp-search 显式搜索框 + 边打边过滤 + 结果统计),
|
||||
// 供列表工具栏「供应商 / 客户」等长名录筛选复用(复用 atoms.css .combo-pop 族,零新增样式)。
|
||||
// items: [{ v, label, sel, small? }];opts: { placeholder, minW, cls }。
|
||||
// items: [{ v, label, sel, small? }];opts: { placeholder, minW, cls }。单选:点选后由 onPick 内自行 closeMenus。
|
||||
function openSearchMenu(t, items, onPick, opts) {
|
||||
closeMenus();
|
||||
opts = opts || {};
|
||||
|
||||
Reference in New Issue
Block a user