feat(client): 全列表下拉刷新 + 库存移动工具行移除
- DsTable 窄屏卡片流支持 onRefresh(RefreshIndicator,空态也可下拉)—— 等价桌面工具栏「刷新」;库存/入库/出库/往来/财务流水/用户/基础数据 全 tab 接线(基础数据下拉一并重拉五个 tab 数据源) - 库存移动端「盘点/表格视图/导出/列设置/刷新」工具行移除(2026-07-04 拍板:刷新改下拉手势、盘点入口在「我的」、其余桌面专属), 库存移动 golden 随更新 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@@ -560,6 +560,11 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
|
|||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
emptyText: '没有匹配的流水记录',
|
emptyText: '没有匹配的流水记录',
|
||||||
toolbar: toolbar,
|
toolbar: toolbar,
|
||||||
|
// 下拉刷新 = 桌面「刷新」(_reload 连带 KPI/汇总/趋势)
|
||||||
|
onRefresh: () async {
|
||||||
|
_reload();
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 600));
|
||||||
|
},
|
||||||
mobileCards: flows.map((r) => _flowCard(t, r)).toList(),
|
mobileCards: flows.map((r) => _flowCard(t, r)).toList(),
|
||||||
columns: const [
|
columns: const [
|
||||||
DsColumn('date', '日期'),
|
DsColumn('date', '日期'),
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||||
import '../../core/responsive/responsive.dart';
|
import '../../core/responsive/responsive.dart';
|
||||||
@@ -41,7 +40,6 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
|
|||||||
// 库存状态前端筛选(派生自 qty vs 安全库存):全部/在售/预警/缺货
|
// 库存状态前端筛选(派生自 qty vs 安全库存):全部/在售/预警/缺货
|
||||||
String _statusFilter = '全部';
|
String _statusFilter = '全部';
|
||||||
Set<String>? _hiddenCols; // null = 尚未载入本地存档
|
Set<String>? _hiddenCols; // null = 尚未载入本地存档
|
||||||
bool _mobileTableView = false; // 移动端:false=卡片列表,true=表格(横向滚动)
|
|
||||||
|
|
||||||
static const _screenId = 'inventory_list';
|
static const _screenId = 'inventory_list';
|
||||||
static const _statusOptions = ['全部', '在售', '预警', '缺货'];
|
static const _statusOptions = ['全部', '在售', '预警', '缺货'];
|
||||||
@@ -812,7 +810,6 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
|
|||||||
ref.read(inventoryListProvider.notifier).setPageSize(s),
|
ref.read(inventoryListProvider.notifier).setPageSize(s),
|
||||||
toolbar: Builder(builder: (ctx) {
|
toolbar: Builder(builder: (ctx) {
|
||||||
final isMobile = ctx.isMobile;
|
final isMobile = ctx.isMobile;
|
||||||
void doExport() => _exportInventory(filteredItems);
|
|
||||||
|
|
||||||
// 搜索合一(用户口径):名称/拼音/编码同框,回车触发
|
// 搜索合一(用户口径):名称/拼音/编码同框,回车触发
|
||||||
// (后端 keyword 本就 LIKE name/code/拼音/首字母)。
|
// (后端 keyword 本就 LIKE name/code/拼音/首字母)。
|
||||||
@@ -824,11 +821,10 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
|
|||||||
.setKeyword(v.trim()),
|
.setKeyword(v.trim()),
|
||||||
);
|
);
|
||||||
|
|
||||||
final canCheck = !WriteGuard.isReadonly(ref);
|
|
||||||
|
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
// 移动端:MSearchRow(原型 .m-search + 详搜钮)独占一行
|
// 移动端:MSearchRow(原型 .m-search + 详搜钮)独占一行
|
||||||
// + 紧凑图标操作行(含 表格/列表 切换)
|
//(盘点/导出/列设置/刷新工具行 2026-07-04 拍板移除,
|
||||||
|
// 刷新改下拉手势,盘点入口在「我的」)
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
@@ -842,62 +838,6 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
|
|||||||
onFilterTap: () => _openMobileFilterSheet(
|
onFilterTap: () => _openMobileFilterSheet(
|
||||||
specOptions, seriesOptions),
|
specOptions, seriesOptions),
|
||||||
),
|
),
|
||||||
const SizedBox(height: AppDims.sp2),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
if (canCheck)
|
|
||||||
WriteGuard(
|
|
||||||
child: OutlinedButton.icon(
|
|
||||||
onPressed: () =>
|
|
||||||
context.go('/inventory/check'),
|
|
||||||
icon: const Icon(
|
|
||||||
LucideIcons.clipboardCheck,
|
|
||||||
size: 16),
|
|
||||||
label: const Text('盘点'),
|
|
||||||
style: OutlinedButton.styleFrom(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 10, vertical: 6),
|
|
||||||
minimumSize: Size.zero,
|
|
||||||
tapTargetSize:
|
|
||||||
MaterialTapTargetSize.shrinkWrap,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Spacer(),
|
|
||||||
IconButton(
|
|
||||||
tooltip: _mobileTableView ? '卡片视图' : '表格视图',
|
|
||||||
icon: Icon(
|
|
||||||
_mobileTableView
|
|
||||||
? LucideIcons.layoutList
|
|
||||||
: LucideIcons.rows3,
|
|
||||||
size: 20),
|
|
||||||
onPressed: () => setState(() =>
|
|
||||||
_mobileTableView = !_mobileTableView),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
tooltip: '导出',
|
|
||||||
icon: const Icon(LucideIcons.download,
|
|
||||||
size: 20),
|
|
||||||
onPressed: doExport,
|
|
||||||
),
|
|
||||||
Builder(
|
|
||||||
builder: (bctx) => IconButton(
|
|
||||||
tooltip: '列设置',
|
|
||||||
icon: const Icon(LucideIcons.alignLeft,
|
|
||||||
size: 20),
|
|
||||||
onPressed: () => _openColMenu(bctx),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
tooltip: '刷新',
|
|
||||||
icon: const Icon(LucideIcons.refreshCw,
|
|
||||||
size: 20),
|
|
||||||
onPressed: () => ref
|
|
||||||
.read(inventoryListProvider.notifier)
|
|
||||||
.reload(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -912,9 +852,13 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
mobileCards: _mobileTableView
|
mobileCards: filteredItems.map(_inventoryCard).toList(),
|
||||||
? null
|
// 下拉刷新 = 桌面「刷新」(reload 保留旧内容叠进度)
|
||||||
: filteredItems.map(_inventoryCard).toList(),
|
onRefresh: () async {
|
||||||
|
ref.read(inventoryListProvider.notifier).reload();
|
||||||
|
await Future<void>.delayed(
|
||||||
|
const Duration(milliseconds: 600));
|
||||||
|
},
|
||||||
emptyText: '没有匹配的商品 · 试试调整筛选或搜索',
|
emptyText: '没有匹配的商品 · 试试调整筛选或搜索',
|
||||||
columns: visibleCols.map((c) {
|
columns: visibleCols.map((c) {
|
||||||
final Widget? header = switch (c.key) {
|
final Widget? header = switch (c.key) {
|
||||||
|
|||||||
@@ -147,6 +147,12 @@ class _PartnersScreenState extends ConsumerState<PartnersScreen> {
|
|||||||
pagerInfoText: '共 ${result.total} 个往来单位',
|
pagerInfoText: '共 ${result.total} 个往来单位',
|
||||||
emptyText: '没有匹配的往来单位 · 试试调整筛选或搜索',
|
emptyText: '没有匹配的往来单位 · 试试调整筛选或搜索',
|
||||||
toolbar: _toolbar(mobile, partners),
|
toolbar: _toolbar(mobile, partners),
|
||||||
|
// 下拉刷新 = 桌面「刷新」
|
||||||
|
onRefresh: () async {
|
||||||
|
ref.read(partnersScreenProvider.notifier).reload();
|
||||||
|
ref.invalidate(partnerBalancesProvider);
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 600));
|
||||||
|
},
|
||||||
mobileCards: partners.map((p) => _card(p, balances)).toList(),
|
mobileCards: partners.map((p) => _card(p, balances)).toList(),
|
||||||
columns: const [
|
columns: const [
|
||||||
DsColumn('name', '名称'),
|
DsColumn('name', '名称'),
|
||||||
|
|||||||
@@ -114,6 +114,16 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
|
|||||||
_ => '',
|
_ => '',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// 下拉刷新(全 tab 通用):档案/介绍/系列/规格/仓库 一并重拉。
|
||||||
|
Future<void> _pullRefresh() async {
|
||||||
|
ref.read(productListProvider.notifier).reload();
|
||||||
|
ref.invalidate(productDescriptionDocListProvider);
|
||||||
|
ref.read(productSeriesListProvider.notifier).reload();
|
||||||
|
ref.read(productSpecListProvider.notifier).reload();
|
||||||
|
ref.read(warehouseListProvider.notifier).reload();
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 600));
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final t = context.tokens;
|
final t = context.tokens;
|
||||||
@@ -370,6 +380,7 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
|
|||||||
.toList(),
|
.toList(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
onRefresh: _pullRefresh,
|
||||||
mobileCards: items
|
mobileCards: items
|
||||||
.map((p) => MobileListCard(
|
.map((p) => MobileListCard(
|
||||||
onTap: () => showProductEditorDrawer(context,
|
onTap: () => showProductEditorDrawer(context,
|
||||||
@@ -470,6 +481,7 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
|
|||||||
.toList(),
|
.toList(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
onRefresh: _pullRefresh,
|
||||||
mobileCards: filtered
|
mobileCards: filtered
|
||||||
.map((it) => MobileListCard(
|
.map((it) => MobileListCard(
|
||||||
title: Text(it.title),
|
title: Text(it.title),
|
||||||
@@ -695,6 +707,7 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
|
|||||||
onExport: onExport,
|
onExport: onExport,
|
||||||
mobileAdd: true,
|
mobileAdd: true,
|
||||||
),
|
),
|
||||||
|
onRefresh: _pullRefresh,
|
||||||
mobileCards: filtered
|
mobileCards: filtered
|
||||||
.map((r) => MobileListCard(
|
.map((r) => MobileListCard(
|
||||||
title: Text(r.name),
|
title: Text(r.name),
|
||||||
@@ -775,6 +788,7 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
|
|||||||
onChanged: () => setState(() {}),
|
onChanged: () => setState(() {}),
|
||||||
mobileAdd: true,
|
mobileAdd: true,
|
||||||
),
|
),
|
||||||
|
onRefresh: _pullRefresh,
|
||||||
mobileCards: filtered
|
mobileCards: filtered
|
||||||
.map((w) => MobileListCard(
|
.map((w) => MobileListCard(
|
||||||
title: Text(w.name),
|
title: Text(w.name),
|
||||||
|
|||||||
@@ -174,6 +174,8 @@ class _UsersScreenState extends ConsumerState<UsersScreen> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: DsTable(
|
child: DsTable(
|
||||||
pagerInfoText: '共 ${filtered.length} 名成员',
|
pagerInfoText: '共 ${filtered.length} 名成员',
|
||||||
|
// 下拉刷新 = 桌面「刷新」
|
||||||
|
onRefresh: () => ref.read(userListProvider.notifier).reload(),
|
||||||
emptyText: '没有匹配的成员',
|
emptyText: '没有匹配的成员',
|
||||||
toolbar: _toolbar(mobile),
|
toolbar: _toolbar(mobile),
|
||||||
columns: const [
|
columns: const [
|
||||||
|
|||||||
@@ -411,6 +411,11 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
|||||||
supplierOptions: supplierOptions,
|
supplierOptions: supplierOptions,
|
||||||
selectedSupplierId: selectedSupplierId,
|
selectedSupplierId: selectedSupplierId,
|
||||||
),
|
),
|
||||||
|
// 下拉刷新 = 桌面「刷新」
|
||||||
|
onRefresh: () async {
|
||||||
|
ref.read(stockInListProvider.notifier).reload();
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 600));
|
||||||
|
},
|
||||||
mobileCards:
|
mobileCards:
|
||||||
orders.map((o) => _orderCard(context, o)).toList(),
|
orders.map((o) => _orderCard(context, o)).toList(),
|
||||||
columns: _colDefs
|
columns: _colDefs
|
||||||
|
|||||||
@@ -406,6 +406,11 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
|
|||||||
],
|
],
|
||||||
))
|
))
|
||||||
.toList(),
|
.toList(),
|
||||||
|
// 下拉刷新 = 桌面「刷新」
|
||||||
|
onRefresh: () async {
|
||||||
|
ref.read(stockOutListProvider.notifier).reload();
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 600));
|
||||||
|
},
|
||||||
mobileCards: orders.map((o) => _orderCard(context, o)).toList(),
|
mobileCards: orders.map((o) => _orderCard(context, o)).toList(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -45,6 +45,9 @@ class DsTable extends StatefulWidget {
|
|||||||
// 仅文案分页(原型 partners/products 的 .pager 只有一行 span 文案,无翻页控件);
|
// 仅文案分页(原型 partners/products 的 .pager 只有一行 span 文案,无翻页控件);
|
||||||
// 与 total 互斥,total 为空且此值非空时生效。
|
// 与 total 互斥,total 为空且此值非空时生效。
|
||||||
final String? pagerInfoText;
|
final String? pagerInfoText;
|
||||||
|
|
||||||
|
/// 窄屏卡片流下拉刷新(等价桌面工具栏「刷新」,2026-07-04 拍板);null 不启用。
|
||||||
|
final Future<void> Function()? onRefresh;
|
||||||
// 内嵌收缩模式(多区块滚动页里的表格卡,如 devices/settings):
|
// 内嵌收缩模式(多区块滚动页里的表格卡,如 devices/settings):
|
||||||
// 不用 Expanded 撑满、表格不带内部滚动,高度随内容。
|
// 不用 Expanded 撑满、表格不带内部滚动,高度随内容。
|
||||||
final bool shrinkWrap;
|
final bool shrinkWrap;
|
||||||
@@ -62,6 +65,7 @@ class DsTable extends StatefulWidget {
|
|||||||
this.onPageSizeChanged,
|
this.onPageSizeChanged,
|
||||||
this.mobileCards,
|
this.mobileCards,
|
||||||
this.pagerInfoText,
|
this.pagerInfoText,
|
||||||
|
this.onRefresh,
|
||||||
this.shrinkWrap = false,
|
this.shrinkWrap = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -146,13 +150,27 @@ class _DsTableState extends State<DsTable> {
|
|||||||
|
|
||||||
Widget _buildMobile(dynamic t) {
|
Widget _buildMobile(dynamic t) {
|
||||||
final cards = widget.mobileCards!;
|
final cards = widget.mobileCards!;
|
||||||
if (cards.isEmpty) return _empty(t);
|
final Widget list = cards.isEmpty
|
||||||
return ListView.separated(
|
? (widget.onRefresh == null
|
||||||
padding: const EdgeInsets.all(12),
|
? _empty(t)
|
||||||
itemCount: cards.length,
|
// 空态也要可下拉:套一个总是可滚的 ListView
|
||||||
separatorBuilder: (_, __) => const SizedBox(height: 10),
|
: ListView(
|
||||||
itemBuilder: (_, i) => cards[i],
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
);
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 60),
|
||||||
|
child: _empty(t)),
|
||||||
|
],
|
||||||
|
))
|
||||||
|
: ListView.separated(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
|
itemCount: cards.length,
|
||||||
|
separatorBuilder: (_, __) => const SizedBox(height: 10),
|
||||||
|
itemBuilder: (_, i) => cards[i],
|
||||||
|
);
|
||||||
|
if (widget.onRefresh == null) return list;
|
||||||
|
return RefreshIndicator(onRefresh: widget.onRefresh!, child: list);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 收缩版卡片流(外层页面自带滚动)。
|
/// 收缩版卡片流(外层页面自带滚动)。
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 208 KiB After Width: | Height: | Size: 200 KiB |
|
Before Width: | Height: | Size: 204 KiB After Width: | Height: | Size: 197 KiB |
|
Before Width: | Height: | Size: 210 KiB After Width: | Height: | Size: 202 KiB |
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 134 KiB |
|
Before Width: | Height: | Size: 139 KiB After Width: | Height: | Size: 138 KiB |