feat(client): 库存表格列头排序 UI——DsSortHeader 点击循环 升/降/无

- 新组件 DsSortHeader(原型 thead .sh 对照,L1 已登记):未排序淡箭头暗示
  可排,激活 primary + 方向箭头(↑升/↓降)
- 库存屏 5 列(库存/成本价/总价/生产日期/入库时间)接线,排序走服务端
  全局生效(跨分页),点击循环 无→升→降→无
- 原型 inventory.html COLS sort:true + toggleSort 交互 + atoms .sh 样式登记
- 桌面库存 golden 重录 ×3(列头带排序箭头)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
This commit is contained in:
wangjia
2026-07-07 16:45:39 +08:00
parent e012f6e02b
commit c7f36b2d72
11 changed files with 117 additions and 4 deletions
@@ -31,6 +31,8 @@ class InventoryListNotifier extends AsyncNotifier<PageResult<Inventory>> {
String _keyword = '';
String _code = ''; // 商品编码独立筛选(原型 codeInput)
String _status = ''; // 三态筛选(空=后端默认排除已售)
String _sortBy = ''; // 列头排序(空=默认 id 倒序)
bool _sortAsc = true;
List<String> _series = [];
List<String> _spec = [];
PageResult<Inventory>? _cache;
@@ -55,6 +57,8 @@ class InventoryListNotifier extends AsyncNotifier<PageResult<Inventory>> {
keyword: _keyword.isEmpty ? null : _keyword,
code: _code.isEmpty ? null : _code,
status: _status.isEmpty ? null : _status,
sortBy: _sortBy.isEmpty ? null : _sortBy,
sortAsc: _sortAsc,
series: _series.isEmpty ? null : _series,
spec: _spec.isEmpty ? null : _spec,
page: _page,
@@ -97,6 +101,13 @@ class InventoryListNotifier extends AsyncNotifier<PageResult<Inventory>> {
reload();
}
void setSort(String sortBy, bool asc) {
_sortBy = sortBy;
_sortAsc = asc;
_page = 1;
reload();
}
void setSeries(List<String> series) {
_series = series;
_page = 1;
@@ -17,6 +17,8 @@ class InventoryRepository {
List<String>? series,
List<String>? spec,
String? status, // 三态筛选:stock/on_sale/sold(逗号可多值);空=后端默认(排除已售)
String? sortBy, // 列头排序:qty/cost/price/prod_date/in_time(后端白名单)
bool sortAsc = true,
int page = 1,
int pageSize = 50,
}) async {
@@ -31,6 +33,10 @@ class InventoryRepository {
if (series != null && series.isNotEmpty) 'series': series.join(','),
if (spec != null && spec.isNotEmpty) 'spec': spec.join(','),
if (status != null && status.isNotEmpty) 'status': status,
if (sortBy != null && sortBy.isNotEmpty) ...{
'sort_by': sortBy,
'sort_dir': sortAsc ? 'asc' : 'desc',
},
};
final resp = await _client.get('/inventory', params: params);
return PageResult.fromJson(
@@ -141,6 +141,34 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
.setStatus(_statusParam[label] ?? '');
}
// ── 列头排序(点击循环 无→升→降→无,服务端全局排序) ──
String _sortBy = ''; // 列 key(屏内),空=未排序
bool _sortAsc = true;
static const _sortParam = {
'qty': 'qty',
'cost': 'cost',
'price': 'price',
'prodDate': 'prod_date',
'inTime': 'in_time',
};
void _toggleSort(String key) {
setState(() {
if (_sortBy != key) {
_sortBy = key;
_sortAsc = true;
} else if (_sortAsc) {
_sortAsc = false;
} else {
_sortBy = '';
_sortAsc = true;
}
});
ref
.read(inventoryListProvider.notifier)
.setSort(_sortBy.isEmpty ? '' : _sortParam[_sortBy]!, _sortAsc);
}
// 成本仅管理员可见(与出库同口径):非管理员隐藏成本价/总价列、卡片单价、货值 KPI。
bool get _isAdmin => ref.watch(isAdminProvider);
bool _colAllowed(ColDef c) =>
@@ -939,6 +967,17 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
filtered: _statusFilter != '全部',
onOpen: _openStatusMenu,
),
// 可排序列(原型 COLS sort:true,服务端全局排序)
'qty' ||
'cost' ||
'price' ||
'prodDate' ||
'inTime' =>
DsSortHeader(
label: c.label,
ascending: _sortBy == c.key ? _sortAsc : null,
onTap: () => _toggleSort(c.key),
),
_ => null,
};
return DsColumn(c.key, c.label,
+44
View File
@@ -479,6 +479,50 @@ class _DsTableState extends State<DsTable> {
}
}
/// 原型 thead th .sh 排序列头(2026-07-07 登记):label + 方向箭头。
/// 未排序 = 淡 chevron-down 暗示可排;激活整体 primaryasc=↑ / desc=↓。
/// 点击由调用方循环 无→升→降→无(服务端全局排序)。
class DsSortHeader extends StatelessWidget {
final String label;
/// null = 未按本列排序;true = 升序;false = 降序
final bool? ascending;
final VoidCallback onTap;
const DsSortHeader(
{super.key, required this.label, this.ascending, required this.onTap});
@override
Widget build(BuildContext context) {
final t = context.tokens;
final on = ascending != null;
final color = on ? t.primary : t.muted;
return InkWell(
onTap: onTap,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(label,
style: TextStyle(
fontSize: AppDims.fsSm,
fontWeight: FontWeight.w600,
color: color)),
const SizedBox(width: 4),
Opacity(
opacity: on ? 1 : .45,
child: Icon(
ascending == true
? LucideIcons.chevronUp
: LucideIcons.chevronDown,
size: 12,
color: color,
),
),
],
),
);
}
}
/// 原型 thead th .fh 漏斗列头:label + funnel(12, sw1.6),有筛选值时整体 primary。
/// 点击回调携带列头自身 BuildContext(供 showDsMenu 锚定)。
class DsFilterHeader extends StatelessWidget {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 416 KiB

After

Width:  |  Height:  |  Size: 414 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 410 KiB

After

Width:  |  Height:  |  Size: 409 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 417 KiB

After

Width:  |  Height:  |  Size: 415 KiB

@@ -52,6 +52,8 @@ class _FakeInvRepo implements InventoryRepository {
List<String>? series,
List<String>? spec,
String? status,
String? sortBy,
bool sortAsc = true,
int page = 1,
int pageSize = 50,
}) async =>
@@ -100,6 +100,8 @@ class _FakeInventoryRepository extends InventoryRepository {
List<String>? series,
List<String>? spec,
String? status,
String? sortBy,
bool sortAsc = true,
int page = 1,
int pageSize = 50,
}) async {