feat(client): 财务应收应付汇总增加往来单位搜索(两端,原型同步)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@@ -27,6 +27,7 @@ import '../../widgets/ds/ds_bar_chart.dart';
|
|||||||
import '../../widgets/ds/ds_kpi.dart';
|
import '../../widgets/ds/ds_kpi.dart';
|
||||||
import '../../widgets/ds/ds_table.dart';
|
import '../../widgets/ds/ds_table.dart';
|
||||||
import '../../widgets/ds/m_kpi_grid.dart';
|
import '../../widgets/ds/m_kpi_grid.dart';
|
||||||
|
import '../../widgets/ds/m_search_row.dart';
|
||||||
import '../../widgets/ds/m_sheet.dart';
|
import '../../widgets/ds/m_sheet.dart';
|
||||||
import '../../widgets/ds/status_icon_map.dart';
|
import '../../widgets/ds/status_icon_map.dart';
|
||||||
import '../../widgets/finance_entry_dialog.dart';
|
import '../../widgets/finance_entry_dialog.dart';
|
||||||
@@ -56,6 +57,10 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
|
|||||||
DateTimeRange? _customRange;
|
DateTimeRange? _customRange;
|
||||||
// 窄屏三分段(原型 m-finance .seg:应收 / 应付 / 流水)
|
// 窄屏三分段(原型 m-finance .seg:应收 / 应付 / 流水)
|
||||||
int _mSeg = 0;
|
int _mSeg = 0;
|
||||||
|
// 应收/应付汇总卡「往来单位」搜索(本地即时过滤——financePartnerRowsProvider
|
||||||
|
// 全量加载非分页,搜索口径=显示口径;桌面 DsSearchBox / 移动 MSearchRow 共用)
|
||||||
|
final _summaryKwCtrl = TextEditingController();
|
||||||
|
String _summaryKw = '';
|
||||||
|
|
||||||
static const _flowChips = ['全部', '应收', '应付', '收款', '付款'];
|
static const _flowChips = ['全部', '应收', '应付', '收款', '付款'];
|
||||||
static const _chipToType = {
|
static const _chipToType = {
|
||||||
@@ -102,7 +107,8 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
|
|||||||
case '自定义':
|
case '自定义':
|
||||||
final picked = await showDateRangeDropdown(
|
final picked = await showDateRangeDropdown(
|
||||||
anchorContext,
|
anchorContext,
|
||||||
initial: _customRange ??
|
initial:
|
||||||
|
_customRange ??
|
||||||
DateTimeRange(start: DateTime(now.year, now.month, 1), end: now),
|
DateTimeRange(start: DateTime(now.year, now.month, 1), end: now),
|
||||||
);
|
);
|
||||||
if (picked == null) return;
|
if (picked == null) return;
|
||||||
@@ -121,6 +127,21 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
|
|||||||
ref.read(financeListProvider.notifier).setPartner(partnerId);
|
ref.read(financeListProvider.notifier).setPartner(partnerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _setSummaryKw(String kw) => setState(() => _summaryKw = kw);
|
||||||
|
|
||||||
|
/// 汇总行本地过滤(往来单位名 contains,大小写不敏感)。
|
||||||
|
List<PartnerFinanceRow> _filterSummaryRows(List<PartnerFinanceRow> rows) {
|
||||||
|
final kw = _summaryKw.trim().toLowerCase();
|
||||||
|
if (kw.isEmpty) return rows;
|
||||||
|
return rows.where((r) => r.name.toLowerCase().contains(kw)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_summaryKwCtrl.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
void _reload() {
|
void _reload() {
|
||||||
ref.read(financeListProvider.notifier).reload();
|
ref.read(financeListProvider.notifier).reload();
|
||||||
ref.invalidate(financePartnerRowsProvider);
|
ref.invalidate(financePartnerRowsProvider);
|
||||||
@@ -130,9 +151,8 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── 金额格式 ──
|
// ── 金额格式 ──
|
||||||
String _yuan(double v) => '¥${NumberFormat.decimalPattern().format(
|
String _yuan(double v) =>
|
||||||
v == v.roundToDouble() ? v.round() : v,
|
'¥${NumberFormat.decimalPattern().format(v == v.roundToDouble() ? v.round() : v)}';
|
||||||
)}';
|
|
||||||
|
|
||||||
/// KPI 大数:¥186万 / ¥48.6万(<1万显示整数元)。
|
/// KPI 大数:¥186万 / ¥48.6万(<1万显示整数元)。
|
||||||
String _yuanWan(double v) {
|
String _yuanWan(double v) {
|
||||||
@@ -179,19 +199,32 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
|
|||||||
child: DsSeg(
|
child: DsSeg(
|
||||||
items: const ['应收', '应付', '流水'],
|
items: const ['应收', '应付', '流水'],
|
||||||
index: _mSeg,
|
index: _mSeg,
|
||||||
onChanged: (i) => setState(() => _mSeg = i)),
|
onChanged: (i) => setState(() => _mSeg = i),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
// 应收/应付分段头部往来单位搜索(同口径本地过滤);「流水」分段
|
||||||
|
// 沿用上方时间行「往来单位」chip → sheet 单选,不重复放置。
|
||||||
|
if (_mSeg != 2) ...[
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
MSearchRow(
|
||||||
|
controller: _summaryKwCtrl,
|
||||||
|
hint: '搜索往来单位',
|
||||||
|
onChanged: _setSummaryKw,
|
||||||
|
),
|
||||||
|
],
|
||||||
..._mSegBody(t, flows),
|
..._mSegBody(t, flows),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return Stack(children: [
|
return Stack(
|
||||||
|
children: [
|
||||||
content,
|
content,
|
||||||
if (flowsAsync.isLoading)
|
if (flowsAsync.isLoading)
|
||||||
const Positioned.fill(child: DsLoadingScrim()),
|
const Positioned.fill(child: DsLoadingScrim()),
|
||||||
]);
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final content = Container(
|
final content = Container(
|
||||||
@@ -209,7 +242,17 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
|
|||||||
_secHead(t, '收支趋势', '近 6 个月 · 单位 万元'),
|
_secHead(t, '收支趋势', '近 6 个月 · 单位 万元'),
|
||||||
_trendChart(),
|
_trendChart(),
|
||||||
const SizedBox(height: 22),
|
const SizedBox(height: 22),
|
||||||
_secHead(t, '应收 / 应付 汇总', '按往来单位 · 点击行查看流水'),
|
_secHead(
|
||||||
|
t,
|
||||||
|
'应收 / 应付 汇总',
|
||||||
|
'按往来单位 · 点击行查看流水',
|
||||||
|
trailing: DsSearchBox(
|
||||||
|
controller: _summaryKwCtrl,
|
||||||
|
hint: '搜索往来单位',
|
||||||
|
width: 220,
|
||||||
|
onChanged: _setSummaryKw,
|
||||||
|
),
|
||||||
|
),
|
||||||
_summaryTable(t, mobile),
|
_summaryTable(t, mobile),
|
||||||
const SizedBox(height: 22),
|
const SizedBox(height: 22),
|
||||||
_secHead(t, '收支流水', _flowSub(flows)),
|
_secHead(t, '收支流水', _flowSub(flows)),
|
||||||
@@ -218,10 +261,13 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return Stack(children: [
|
return Stack(
|
||||||
|
children: [
|
||||||
content,
|
content,
|
||||||
if (flowsAsync.isLoading) const Positioned.fill(child: DsLoadingScrim()),
|
if (flowsAsync.isLoading)
|
||||||
]);
|
const Positioned.fill(child: DsLoadingScrim()),
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── head ──
|
// ── head ──
|
||||||
@@ -231,32 +277,42 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
|
|||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
Text('财务管理',
|
Text(
|
||||||
|
'财务管理',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: AppDims.fsH1,
|
fontSize: AppDims.fsH1,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
color: t.heading)),
|
color: t.heading,
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(width: AppDims.sp3),
|
const SizedBox(width: AppDims.sp3),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 2),
|
padding: const EdgeInsets.only(bottom: 2),
|
||||||
child: Text('应收/应付概览 · $_rangeLabel',
|
child: Text(
|
||||||
|
'应收/应付概览 · $_rangeLabel',
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: TextStyle(fontSize: AppDims.fsSm, color: t.muted)),
|
style: TextStyle(fontSize: AppDims.fsSm, color: t.muted),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (!mobile) ...[
|
if (!mobile) ...[
|
||||||
DsButton('刷新', icon: LucideIcons.refreshCw, onPressed: _reload),
|
DsButton('刷新', icon: LucideIcons.refreshCw, onPressed: _reload),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
DsButton('导出报表',
|
DsButton(
|
||||||
icon: LucideIcons.download, onPressed: () => _export(flows)),
|
'导出报表',
|
||||||
|
icon: LucideIcons.download,
|
||||||
|
onPressed: () => _export(flows),
|
||||||
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
WriteGuard(
|
WriteGuard(
|
||||||
child: DsButton('登记收支',
|
child: DsButton(
|
||||||
|
'登记收支',
|
||||||
icon: LucideIcons.plus,
|
icon: LucideIcons.plus,
|
||||||
variant: DsBtnVariant.primary,
|
variant: DsBtnVariant.primary,
|
||||||
onPressed: () => showFinanceEntryDialog(context)),
|
onPressed: () => showFinanceEntryDialog(context),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -269,7 +325,8 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
|
|||||||
filename: '财务流水',
|
filename: '财务流水',
|
||||||
headers: ['日期', '类型', '往来单位', '金额', '关联单据', '状态', '备注'],
|
headers: ['日期', '类型', '往来单位', '金额', '关联单据', '状态', '备注'],
|
||||||
rows: flows
|
rows: flows
|
||||||
.map((r) => [
|
.map(
|
||||||
|
(r) => [
|
||||||
(r.recordDate ?? '').split('T').first,
|
(r.recordDate ?? '').split('T').first,
|
||||||
r.typeLabel,
|
r.typeLabel,
|
||||||
r.partnerName ?? '',
|
r.partnerName ?? '',
|
||||||
@@ -277,7 +334,8 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
|
|||||||
r.refType != null ? '${r.docTitle}#${r.refId ?? ''}' : '',
|
r.refType != null ? '${r.docTitle}#${r.refId ?? ''}' : '',
|
||||||
r.status == 'open' ? '未结清' : '已结清',
|
r.status == 'open' ? '未结清' : '已结清',
|
||||||
r.remark ?? '',
|
r.remark ?? '',
|
||||||
])
|
],
|
||||||
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -289,18 +347,23 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
|
|||||||
final partnerName = mobile
|
final partnerName = mobile
|
||||||
? _partnerNameById(
|
? _partnerNameById(
|
||||||
ref.watch(allPartnersProvider).valueOrNull?.data ?? const [],
|
ref.watch(allPartnersProvider).valueOrNull?.data ?? const [],
|
||||||
_partnerId)
|
_partnerId,
|
||||||
|
)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
final children = <Widget>[
|
final children = <Widget>[
|
||||||
Text('时间范围', style: TextStyle(fontSize: AppDims.fsSm, color: t.muted)),
|
Text(
|
||||||
|
'时间范围',
|
||||||
|
style: TextStyle(fontSize: AppDims.fsSm, color: t.muted),
|
||||||
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
for (final r in const ['本月', '本季']) ...[
|
for (final r in const ['本月', '本季']) ...[
|
||||||
DsChip(
|
DsChip(
|
||||||
label: r,
|
label: r,
|
||||||
selected: _range == r,
|
selected: _range == r,
|
||||||
caret: false,
|
caret: false,
|
||||||
onTap: () => _applyRange(r, context)),
|
onTap: () => _applyRange(r, context),
|
||||||
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
],
|
],
|
||||||
Builder(
|
Builder(
|
||||||
@@ -309,7 +372,8 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
|
|||||||
builder: (anchorCtx) => DsChip(
|
builder: (anchorCtx) => DsChip(
|
||||||
label: '自定义',
|
label: '自定义',
|
||||||
selected: _range == '自定义',
|
selected: _range == '自定义',
|
||||||
onTap: () => _applyRange('自定义', anchorCtx)),
|
onTap: () => _applyRange('自定义', anchorCtx),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (mobile) ...[
|
if (mobile) ...[
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
@@ -317,7 +381,8 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
|
|||||||
label: '往来单位',
|
label: '往来单位',
|
||||||
value: partnerName,
|
value: partnerName,
|
||||||
onTap: _openPartnerSheet,
|
onTap: _openPartnerSheet,
|
||||||
onClear: () => _setPartner(null)),
|
onClear: () => _setPartner(null),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -325,11 +390,13 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
|
|||||||
children.addAll([
|
children.addAll([
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
WriteGuard(
|
WriteGuard(
|
||||||
child: DsButton('登记',
|
child: DsButton(
|
||||||
|
'登记',
|
||||||
small: true,
|
small: true,
|
||||||
icon: LucideIcons.plus,
|
icon: LucideIcons.plus,
|
||||||
variant: DsBtnVariant.primary,
|
variant: DsBtnVariant.primary,
|
||||||
onPressed: () => showFinanceEntryDialog(context)),
|
onPressed: () => showFinanceEntryDialog(context),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
// 窄屏内容(label+4 chip+登记)常超宽:整行横向可滚(照 _flowTable 移动
|
// 窄屏内容(label+4 chip+登记)常超宽:整行横向可滚(照 _flowTable 移动
|
||||||
@@ -337,7 +404,9 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
|
|||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 18),
|
padding: const EdgeInsets.only(bottom: 18),
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
scrollDirection: Axis.horizontal, child: Row(children: children)),
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: Row(children: children),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -357,14 +426,17 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
|
|||||||
|
|
||||||
/// 移动端「往来单位」chip → 底部 sheet 可搜索单选(D6,对齐桌面 ComboSearchField)。
|
/// 移动端「往来单位」chip → 底部 sheet 可搜索单选(D6,对齐桌面 ComboSearchField)。
|
||||||
Future<void> _openPartnerSheet() async {
|
Future<void> _openPartnerSheet() async {
|
||||||
final partners = ref.read(allPartnersProvider).valueOrNull?.data ?? const [];
|
final partners =
|
||||||
|
ref.read(allPartnersProvider).valueOrNull?.data ?? const [];
|
||||||
final options = [
|
final options = [
|
||||||
for (final p in partners) OptionItem(id: p.id, name: p.name, code: p.code),
|
for (final p in partners)
|
||||||
|
OptionItem(id: p.id, name: p.name, code: p.code),
|
||||||
];
|
];
|
||||||
final sel = await showMSheet<int?>(
|
final sel = await showMSheet<int?>(
|
||||||
context,
|
context,
|
||||||
title: '往来单位',
|
title: '往来单位',
|
||||||
builder: (_) => _PartnerSheetBody(options: options, selectedId: _partnerId),
|
builder: (_) =>
|
||||||
|
_PartnerSheetBody(options: options, selectedId: _partnerId),
|
||||||
);
|
);
|
||||||
if (sel == null || !mounted) return;
|
if (sel == null || !mounted) return;
|
||||||
// -1 = 清除选择(照 SearchableOptionField 的既有约定)
|
// -1 = 清除选择(照 SearchableOptionField 的既有约定)
|
||||||
@@ -430,12 +502,14 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
|
|||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
child: IntrinsicHeight(
|
child: IntrinsicHeight(
|
||||||
child: Row(children: [
|
child: Row(
|
||||||
|
children: [
|
||||||
for (var i = 0; i < cards.length; i++) ...[
|
for (var i = 0; i < cards.length; i++) ...[
|
||||||
if (i > 0) const SizedBox(width: AppDims.sp3),
|
if (i > 0) const SizedBox(width: AppDims.sp3),
|
||||||
SizedBox(width: 170, child: cards[i]),
|
SizedBox(width: 170, child: cards[i]),
|
||||||
],
|
],
|
||||||
]),
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -457,22 +531,30 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 原型 .sec-head:h2 17/700 + 副标 fs-xs muted,下距 12。
|
/// 原型 .sec-head:h2 17/700 + 副标 fs-xs muted,下距 12。
|
||||||
Widget _secHead(dynamic t, String title, String sub) => Padding(
|
/// [trailing] 靠右附加控件(原型 .sec-head .sum-search,如汇总卡的往来单位搜索)。
|
||||||
|
Widget _secHead(dynamic t, String title, String sub, {Widget? trailing}) =>
|
||||||
|
Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 12),
|
padding: const EdgeInsets.only(bottom: 12),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
Text(title,
|
Text(
|
||||||
|
title,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: AppDims.fsH2,
|
fontSize: AppDims.fsH2,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
color: t.heading)),
|
color: t.heading,
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 1),
|
padding: const EdgeInsets.only(bottom: 1),
|
||||||
child: Text(sub,
|
child: Text(
|
||||||
style: TextStyle(fontSize: AppDims.fsXs, color: t.muted)),
|
sub,
|
||||||
|
style: TextStyle(fontSize: AppDims.fsXs, color: t.muted),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
if (trailing != null) ...[const Spacer(), trailing],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -492,12 +574,19 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
|
|||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
return trend.when(
|
return trend.when(
|
||||||
loading: () => placeholder(const SizedBox(
|
loading: () => placeholder(
|
||||||
|
const SizedBox(
|
||||||
width: 20,
|
width: 20,
|
||||||
height: 20,
|
height: 20,
|
||||||
child: CircularProgressIndicator(strokeWidth: 2))),
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
error: (e, _) => placeholder(Text('趋势加载失败',
|
),
|
||||||
style: TextStyle(color: t.muted, fontSize: AppDims.fsSm))),
|
),
|
||||||
|
error: (e, _) => placeholder(
|
||||||
|
Text(
|
||||||
|
'趋势加载失败',
|
||||||
|
style: TextStyle(color: t.muted, fontSize: AppDims.fsSm),
|
||||||
|
),
|
||||||
|
),
|
||||||
data: (points) => DsBarChart(
|
data: (points) => DsBarChart(
|
||||||
groups: [
|
groups: [
|
||||||
for (final p in points)
|
for (final p in points)
|
||||||
@@ -522,35 +611,49 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
|
|||||||
return async.when(
|
return async.when(
|
||||||
loading: () => const Padding(
|
loading: () => const Padding(
|
||||||
padding: EdgeInsets.all(24),
|
padding: EdgeInsets.all(24),
|
||||||
child: Center(child: CircularProgressIndicator())),
|
child: Center(child: CircularProgressIndicator()),
|
||||||
error: (e, _) => Row(children: [
|
),
|
||||||
Text('汇总加载失败',
|
error: (e, _) => Row(
|
||||||
style: TextStyle(color: t.muted, fontSize: AppDims.fsSm)),
|
children: [
|
||||||
|
Text(
|
||||||
|
'汇总加载失败',
|
||||||
|
style: TextStyle(color: t.muted, fontSize: AppDims.fsSm),
|
||||||
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
DsButton('重试',
|
DsButton(
|
||||||
|
'重试',
|
||||||
small: true,
|
small: true,
|
||||||
onPressed: () => ref.invalidate(financePartnerRowsProvider)),
|
onPressed: () => ref.invalidate(financePartnerRowsProvider),
|
||||||
]),
|
),
|
||||||
data: (rows) => DsTable(
|
],
|
||||||
|
),
|
||||||
|
data: (allRows) {
|
||||||
|
final rows = _filterSummaryRows(allRows);
|
||||||
|
return DsTable(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
emptyText: '暂无未结清的应收 / 应付',
|
emptyText: _summaryKw.trim().isEmpty ? '暂无未结清的应收 / 应付' : '没有匹配的往来单位',
|
||||||
mobileCards: rows
|
mobileCards: rows
|
||||||
.map((r) => MobileListCard(
|
.map(
|
||||||
|
(r) => MobileListCard(
|
||||||
title: Text(r.name),
|
title: Text(r.name),
|
||||||
trailing: Text(signedYuan(r.net),
|
trailing: Text(
|
||||||
|
signedYuan(r.net),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
fontFamily: AppFonts.mono,
|
fontFamily: AppFonts.mono,
|
||||||
fontFamilyFallback: AppFonts.monoFallback,
|
fontFamilyFallback: AppFonts.monoFallback,
|
||||||
color: r.net > 0
|
color: r.net > 0
|
||||||
? t.success
|
? t.success
|
||||||
: (r.net < 0 ? t.danger : t.text))),
|
: (r.net < 0 ? t.danger : t.text),
|
||||||
|
),
|
||||||
|
),
|
||||||
fields: [
|
fields: [
|
||||||
MobileCardField('应收', r.recv > 0 ? _yuan(r.recv) : '—'),
|
MobileCardField('应收', r.recv > 0 ? _yuan(r.recv) : '—'),
|
||||||
MobileCardField('应付', r.pay > 0 ? _yuan(r.pay) : '—'),
|
MobileCardField('应付', r.pay > 0 ? _yuan(r.pay) : '—'),
|
||||||
],
|
],
|
||||||
onTap: () => showFinancePartnerDrawer(context, row: r),
|
onTap: () => showFinancePartnerDrawer(context, row: r),
|
||||||
))
|
),
|
||||||
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
columns: const [
|
columns: const [
|
||||||
DsColumn('name', '往来单位'),
|
DsColumn('name', '往来单位'),
|
||||||
@@ -563,37 +666,54 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
|
|||||||
return DsRow(
|
return DsRow(
|
||||||
onTap: () => showFinancePartnerDrawer(context, row: r),
|
onTap: () => showFinancePartnerDrawer(context, row: r),
|
||||||
cells: [
|
cells: [
|
||||||
Text(r.name,
|
Text(
|
||||||
style:
|
r.name,
|
||||||
TextStyle(fontWeight: FontWeight.w600, color: t.heading)),
|
style: TextStyle(
|
||||||
Text(r.recv > 0 ? _yuan(r.recv) : '—',
|
fontWeight: FontWeight.w600,
|
||||||
|
color: t.heading,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
r.recv > 0 ? _yuan(r.recv) : '—',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontFamily: AppFonts.mono,
|
fontFamily: AppFonts.mono,
|
||||||
fontFamilyFallback: AppFonts.monoFallback)),
|
fontFamilyFallback: AppFonts.monoFallback,
|
||||||
Text(r.pay > 0 ? _yuan(r.pay) : '—',
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
r.pay > 0 ? _yuan(r.pay) : '—',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontFamily: AppFonts.mono,
|
fontFamily: AppFonts.mono,
|
||||||
fontFamilyFallback: AppFonts.monoFallback)),
|
fontFamilyFallback: AppFonts.monoFallback,
|
||||||
Text(signedYuan(r.net),
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
signedYuan(r.net),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontFamily: AppFonts.mono,
|
fontFamily: AppFonts.mono,
|
||||||
fontFamilyFallback: AppFonts.monoFallback,
|
fontFamilyFallback: AppFonts.monoFallback,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: r.net > 0
|
color: r.net > 0
|
||||||
? t.success
|
? t.success
|
||||||
: (r.net < 0 ? t.danger : t.text))),
|
: (r.net < 0 ? t.danger : t.text),
|
||||||
|
),
|
||||||
|
),
|
||||||
InkWell(
|
InkWell(
|
||||||
onTap: () => showFinancePartnerDrawer(context, row: r),
|
onTap: () => showFinancePartnerDrawer(context, row: r),
|
||||||
child: Text('查看',
|
child: Text(
|
||||||
|
'查看',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: AppDims.fsBody,
|
fontSize: AppDims.fsBody,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: t.primary)),
|
color: t.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
),
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -605,23 +725,33 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
|
|||||||
|
|
||||||
// ── 收支流水表 ──
|
// ── 收支流水表 ──
|
||||||
Widget _flowTable(
|
Widget _flowTable(
|
||||||
dynamic t, bool mobile, List<FinanceRecord> flows, int total) {
|
dynamic t,
|
||||||
|
bool mobile,
|
||||||
|
List<FinanceRecord> flows,
|
||||||
|
int total,
|
||||||
|
) {
|
||||||
final chips = [
|
final chips = [
|
||||||
for (final c in _flowChips) ...[
|
for (final c in _flowChips) ...[
|
||||||
DsChip(
|
DsChip(
|
||||||
label: c,
|
label: c,
|
||||||
selected: _flowChip == c,
|
selected: _flowChip == c,
|
||||||
caret: false,
|
caret: false,
|
||||||
onTap: () => _setFlowChip(c)),
|
onTap: () => _setFlowChip(c),
|
||||||
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
final toolbar = mobile
|
final toolbar = mobile
|
||||||
? SingleChildScrollView(
|
? SingleChildScrollView(
|
||||||
scrollDirection: Axis.horizontal, child: Row(children: chips))
|
scrollDirection: Axis.horizontal,
|
||||||
: Row(children: [
|
child: Row(children: chips),
|
||||||
Text('类型',
|
)
|
||||||
style: TextStyle(fontSize: AppDims.fsSm, color: t.muted)),
|
: Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'类型',
|
||||||
|
style: TextStyle(fontSize: AppDims.fsSm, color: t.muted),
|
||||||
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
...chips,
|
...chips,
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
@@ -629,9 +759,13 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
|
|||||||
// stock_out_list_screen.dart 客户 ComboSearchField 写法)。仅桌面
|
// stock_out_list_screen.dart 客户 ComboSearchField 写法)。仅桌面
|
||||||
// 工具栏放置,移动端对应筛选在 _timeRow 尾部的「往来单位」chip → sheet。
|
// 工具栏放置,移动端对应筛选在 _timeRow 尾部的「往来单位」chip → sheet。
|
||||||
ComboSearchField(
|
ComboSearchField(
|
||||||
options: (ref.watch(allPartnersProvider).valueOrNull?.data ??
|
options:
|
||||||
|
(ref.watch(allPartnersProvider).valueOrNull?.data ??
|
||||||
const [])
|
const [])
|
||||||
.map((p) => OptionItem(id: p.id, name: p.name, code: p.code))
|
.map(
|
||||||
|
(p) =>
|
||||||
|
OptionItem(id: p.id, name: p.name, code: p.code),
|
||||||
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
selectedId: _partnerId,
|
selectedId: _partnerId,
|
||||||
hint: '往来单位',
|
hint: '往来单位',
|
||||||
@@ -639,13 +773,17 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
|
|||||||
onChanged: _setPartner,
|
onChanged: _setPartner,
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
Text('共 $total 条',
|
Text(
|
||||||
|
'共 $total 条',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: AppDims.fsSm,
|
fontSize: AppDims.fsSm,
|
||||||
color: t.muted,
|
color: t.muted,
|
||||||
fontFamily: AppFonts.mono,
|
fontFamily: AppFonts.mono,
|
||||||
fontFamilyFallback: AppFonts.monoFallback)),
|
fontFamilyFallback: AppFonts.monoFallback,
|
||||||
]);
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
return DsTable(
|
return DsTable(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
@@ -669,20 +807,27 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
|
|||||||
],
|
],
|
||||||
rows: flows.map((r) {
|
rows: flows.map((r) {
|
||||||
final isIn = r.type == 'receipt' || r.type == 'receivable';
|
final isIn = r.type == 'receipt' || r.type == 'receivable';
|
||||||
return DsRow(cells: [
|
return DsRow(
|
||||||
Text((r.recordDate ?? '').split('T').first,
|
cells: [
|
||||||
|
Text(
|
||||||
|
(r.recordDate ?? '').split('T').first,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontFamily: AppFonts.mono,
|
fontFamily: AppFonts.mono,
|
||||||
fontFamilyFallback: AppFonts.monoFallback)),
|
fontFamilyFallback: AppFonts.monoFallback,
|
||||||
|
),
|
||||||
|
),
|
||||||
financeTypeBadge(r),
|
financeTypeBadge(r),
|
||||||
Text((r.partnerName?.isNotEmpty == true) ? r.partnerName! : '—'),
|
Text((r.partnerName?.isNotEmpty == true) ? r.partnerName! : '—'),
|
||||||
// 原型 .qty:+/- 前缀、恒 heading 色(不上正负色)
|
// 原型 .qty:+/- 前缀、恒 heading 色(不上正负色)
|
||||||
Text('${isIn ? '+' : '-'}${_yuan(r.amount.abs())}',
|
Text(
|
||||||
|
'${isIn ? '+' : '-'}${_yuan(r.amount.abs())}',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontFamily: AppFonts.mono,
|
fontFamily: AppFonts.mono,
|
||||||
fontFamilyFallback: AppFonts.monoFallback,
|
fontFamilyFallback: AppFonts.monoFallback,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: t.heading)),
|
color: t.heading,
|
||||||
|
),
|
||||||
|
),
|
||||||
Text(
|
Text(
|
||||||
r.refType != null && r.refId != null
|
r.refType != null && r.refId != null
|
||||||
? '${r.docTitle} #${r.refId}'
|
? '${r.docTitle} #${r.refId}'
|
||||||
@@ -691,14 +836,19 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
|
|||||||
fontFamily: AppFonts.mono,
|
fontFamily: AppFonts.mono,
|
||||||
fontFamilyFallback: AppFonts.monoFallback,
|
fontFamilyFallback: AppFonts.monoFallback,
|
||||||
fontSize: AppDims.fsSm,
|
fontSize: AppDims.fsSm,
|
||||||
color: t.muted)),
|
color: t.muted,
|
||||||
Text((r.remark?.isNotEmpty == true) ? r.remark! : '—',
|
),
|
||||||
style: TextStyle(color: t.muted)),
|
),
|
||||||
|
Text(
|
||||||
|
(r.remark?.isNotEmpty == true) ? r.remark! : '—',
|
||||||
|
style: TextStyle(color: t.muted),
|
||||||
|
),
|
||||||
r.status == 'open'
|
r.status == 'open'
|
||||||
? const DsBadge('未结清', tone: DsBadgeTone.warn)
|
? const DsBadge('未结清', tone: DsBadgeTone.warn)
|
||||||
: const DsBadge('已结清', tone: DsBadgeTone.muted),
|
: const DsBadge('已结清', tone: DsBadgeTone.muted),
|
||||||
_closeAction(t, r),
|
_closeAction(t, r),
|
||||||
]);
|
],
|
||||||
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -715,11 +865,14 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
|
|||||||
return WriteGuard(
|
return WriteGuard(
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () => _confirmClose(r),
|
onTap: () => _confirmClose(r),
|
||||||
child: Text('结清',
|
child: Text(
|
||||||
|
'结清',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: AppDims.fsBody,
|
fontSize: AppDims.fsBody,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: t.primary)),
|
color: t.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -729,13 +882,17 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
|
|||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => AlertDialog(
|
builder: (ctx) => AlertDialog(
|
||||||
title: const Text('确认结清'),
|
title: const Text('确认结清'),
|
||||||
content: Text('确认将「${r.partnerName ?? ''} ${r.typeLabel} '
|
content: Text(
|
||||||
'${_yuan(r.amount.abs())}」标记为已结清?'),
|
'确认将「${r.partnerName ?? ''} ${r.typeLabel} '
|
||||||
|
'${_yuan(r.amount.abs())}」标记为已结清?',
|
||||||
|
),
|
||||||
actions: [
|
actions: [
|
||||||
DsButton('取消', onPressed: () => Navigator.of(ctx).pop(false)),
|
DsButton('取消', onPressed: () => Navigator.of(ctx).pop(false)),
|
||||||
DsButton('结清',
|
DsButton(
|
||||||
|
'结清',
|
||||||
variant: DsBtnVariant.primary,
|
variant: DsBtnVariant.primary,
|
||||||
onPressed: () => Navigator.of(ctx).pop(true)),
|
onPressed: () => Navigator.of(ctx).pop(true),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -758,7 +915,8 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
|
|||||||
final isIn = r.type == 'receipt' || r.type == 'receivable';
|
final isIn = r.type == 'receipt' || r.type == 'receivable';
|
||||||
return MobileListCard(
|
return MobileListCard(
|
||||||
title: Text(
|
title: Text(
|
||||||
(r.partnerName?.isNotEmpty == true) ? r.partnerName! : r.typeLabel),
|
(r.partnerName?.isNotEmpty == true) ? r.partnerName! : r.typeLabel,
|
||||||
|
),
|
||||||
subtitle: Text((r.recordDate ?? '').split('T').first),
|
subtitle: Text((r.recordDate ?? '').split('T').first),
|
||||||
trailing: financeTypeBadge(r),
|
trailing: financeTypeBadge(r),
|
||||||
fields: [
|
fields: [
|
||||||
@@ -766,7 +924,8 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
|
|||||||
MobileCardField('状态', r.status == 'open' ? '未结清' : '已结清'),
|
MobileCardField('状态', r.status == 'open' ? '未结清' : '已结清'),
|
||||||
if (r.remark?.isNotEmpty == true) MobileCardField('备注', r.remark),
|
if (r.remark?.isNotEmpty == true) MobileCardField('备注', r.remark),
|
||||||
],
|
],
|
||||||
actions: (r.status == 'open' &&
|
actions:
|
||||||
|
(r.status == 'open' &&
|
||||||
(r.type == 'receivable' || r.type == 'payable'))
|
(r.type == 'receivable' || r.type == 'payable'))
|
||||||
? [
|
? [
|
||||||
WriteGuard(
|
WriteGuard(
|
||||||
@@ -799,7 +958,8 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
|
|||||||
Widget _mKpis(dynamic t) {
|
Widget _mKpis(dynamic t) {
|
||||||
final outSum = ref.watch(stockOutSummaryProvider).valueOrNull;
|
final outSum = ref.watch(stockOutSummaryProvider).valueOrNull;
|
||||||
final inSum = ref.watch(stockInSummaryProvider).valueOrNull;
|
final inSum = ref.watch(stockInSummaryProvider).valueOrNull;
|
||||||
final rows = ref.watch(financePartnerRowsProvider).valueOrNull ??
|
final rows =
|
||||||
|
ref.watch(financePartnerRowsProvider).valueOrNull ??
|
||||||
const <PartnerFinanceRow>[];
|
const <PartnerFinanceRow>[];
|
||||||
var recv = 0.0, pay = 0.0, openCount = 0;
|
var recv = 0.0, pay = 0.0, openCount = 0;
|
||||||
for (final r in rows) {
|
for (final r in rows) {
|
||||||
@@ -813,7 +973,8 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
|
|||||||
final (buyDelta, buyTone) = inSum != null
|
final (buyDelta, buyTone) = inSum != null
|
||||||
? _momDelta(inSum.monthAmount, inSum.lastMonthAmount)
|
? _momDelta(inSum.monthAmount, inSum.lastMonthAmount)
|
||||||
: ('较上月 —', DsKpiDelta.neutral);
|
: ('较上月 —', DsKpiDelta.neutral);
|
||||||
return MKpiGrid(items: [
|
return MKpiGrid(
|
||||||
|
items: [
|
||||||
MKpiItem(
|
MKpiItem(
|
||||||
label: '本月收入',
|
label: '本月收入',
|
||||||
value: outSum != null ? _yuanWan(outSum.monthAmount) : '—',
|
value: outSum != null ? _yuanWan(outSum.monthAmount) : '—',
|
||||||
@@ -839,25 +1000,32 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
|
|||||||
delta: '按到期及时结清',
|
delta: '按到期及时结清',
|
||||||
deltaTone: MKpiDeltaTone.normal,
|
deltaTone: MKpiDeltaTone.normal,
|
||||||
),
|
),
|
||||||
]);
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 三分段列表体:m-section 计数标题 + 卡片流(应收/应付=往来汇总,流水=收支记录)。
|
/// 三分段列表体:m-section 计数标题 + 卡片流(应收/应付=往来汇总,流水=收支记录)。
|
||||||
List<Widget> _mSegBody(dynamic t, List<FinanceRecord> flows) {
|
List<Widget> _mSegBody(dynamic t, List<FinanceRecord> flows) {
|
||||||
Widget section(String title, int count) => Padding(
|
Widget section(String title, int count) => Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(2, 16, 2, 8),
|
padding: const EdgeInsets.fromLTRB(2, 16, 2, 8),
|
||||||
child: Text('$title · 共 $count',
|
child: Text(
|
||||||
|
'$title · 共 $count',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: AppDims.fsSm,
|
fontSize: AppDims.fsSm,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
letterSpacing: .4,
|
letterSpacing: .4,
|
||||||
color: t.muted)),
|
color: t.muted,
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
Widget empty() => Padding(
|
Widget empty([String msg = '暂无数据']) => Padding(
|
||||||
padding: const EdgeInsets.all(24),
|
padding: const EdgeInsets.all(24),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text('暂无数据',
|
child: Text(
|
||||||
style: TextStyle(color: t.muted, fontSize: AppDims.fsSm))),
|
msg,
|
||||||
|
style: TextStyle(color: t.muted, fontSize: AppDims.fsSm),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
List<Widget> stacked(List<Widget> cards) => [
|
List<Widget> stacked(List<Widget> cards) => [
|
||||||
for (var i = 0; i < cards.length; i++) ...[
|
for (var i = 0; i < cards.length; i++) ...[
|
||||||
@@ -880,26 +1048,31 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
|
|||||||
loading: () => [
|
loading: () => [
|
||||||
const Padding(
|
const Padding(
|
||||||
padding: EdgeInsets.all(24),
|
padding: EdgeInsets.all(24),
|
||||||
child: Center(child: CircularProgressIndicator())),
|
child: Center(child: CircularProgressIndicator()),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
error: (e, _) => [
|
error: (e, _) => [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(24),
|
padding: const EdgeInsets.all(24),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text('汇总加载失败',
|
child: Text(
|
||||||
style: TextStyle(color: t.muted, fontSize: AppDims.fsSm))),
|
'汇总加载失败',
|
||||||
|
style: TextStyle(color: t.muted, fontSize: AppDims.fsSm),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
data: (rows) {
|
data: (rows) {
|
||||||
final ar = _mSeg == 0;
|
final ar = _mSeg == 0;
|
||||||
final list = rows.where((r) => ar ? r.recv > 0 : r.pay > 0).toList();
|
final list = _filterSummaryRows(
|
||||||
|
rows.where((r) => ar ? r.recv > 0 : r.pay > 0).toList(),
|
||||||
|
);
|
||||||
return [
|
return [
|
||||||
section(ar ? '应收账款' : '应付账款', list.length),
|
section(ar ? '应收账款' : '应付账款', list.length),
|
||||||
if (list.isEmpty)
|
if (list.isEmpty)
|
||||||
empty()
|
empty(_summaryKw.trim().isEmpty ? '暂无数据' : '没有匹配的往来单位')
|
||||||
else
|
else
|
||||||
...stacked(
|
...stacked([for (final r in list) _mPartnerCard(t, r, ar: ar)]),
|
||||||
[for (final r in list) _mPartnerCard(t, r, ar: ar)]),
|
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -914,16 +1087,18 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
|
|||||||
trailing: Column(
|
trailing: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
DsBadge('未结清',
|
DsBadge('未结清', tone: DsBadgeTone.warn, icon: statusIcon('未结清')),
|
||||||
tone: DsBadgeTone.warn, icon: statusIcon('未结清')),
|
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Text(_yuan(ar ? r.recv : r.pay),
|
Text(
|
||||||
|
_yuan(ar ? r.recv : r.pay),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontFamily: AppFonts.mono,
|
fontFamily: AppFonts.mono,
|
||||||
fontFamilyFallback: AppFonts.monoFallback,
|
fontFamilyFallback: AppFonts.monoFallback,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: t.heading)),
|
color: t.heading,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -934,27 +1109,33 @@ class _FinanceScreenState extends ConsumerState<FinanceScreen> {
|
|||||||
final isIn = r.type == 'receipt' || r.type == 'receivable';
|
final isIn = r.type == 'receipt' || r.type == 'receivable';
|
||||||
return MobileListCard(
|
return MobileListCard(
|
||||||
title: Text(
|
title: Text(
|
||||||
(r.partnerName?.isNotEmpty == true) ? r.partnerName! : r.typeLabel),
|
(r.partnerName?.isNotEmpty == true) ? r.partnerName! : r.typeLabel,
|
||||||
|
),
|
||||||
subtitle: Text((r.recordDate ?? '').split('T').first),
|
subtitle: Text((r.recordDate ?? '').split('T').first),
|
||||||
trailing: Column(
|
trailing: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
DsBadge(r.typeLabel,
|
DsBadge(
|
||||||
|
r.typeLabel,
|
||||||
tone: switch (r.type) {
|
tone: switch (r.type) {
|
||||||
'receipt' => DsBadgeTone.ok,
|
'receipt' => DsBadgeTone.ok,
|
||||||
'payment' => DsBadgeTone.danger,
|
'payment' => DsBadgeTone.danger,
|
||||||
'receivable' => DsBadgeTone.warn,
|
'receivable' => DsBadgeTone.warn,
|
||||||
_ => DsBadgeTone.info,
|
_ => DsBadgeTone.info,
|
||||||
},
|
},
|
||||||
icon: statusIcon(r.typeLabel)),
|
icon: statusIcon(r.typeLabel),
|
||||||
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Text('${isIn ? '+' : '-'}${_yuan(r.amount.abs())}',
|
Text(
|
||||||
|
'${isIn ? '+' : '-'}${_yuan(r.amount.abs())}',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontFamily: AppFonts.mono,
|
fontFamily: AppFonts.mono,
|
||||||
fontFamilyFallback: AppFonts.monoFallback,
|
fontFamilyFallback: AppFonts.monoFallback,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: isIn ? t.success : t.danger)),
|
color: isIn ? t.success : t.danger,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -988,29 +1169,32 @@ class _PartnerSheetBodyState extends State<_PartnerSheetBody> {
|
|||||||
required String name,
|
required String name,
|
||||||
required bool selected,
|
required bool selected,
|
||||||
required VoidCallback onTap,
|
required VoidCallback onTap,
|
||||||
}) =>
|
}) => InkWell(
|
||||||
InkWell(
|
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 13, horizontal: 4),
|
padding: const EdgeInsets.symmetric(vertical: 13, horizontal: 4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border(bottom: BorderSide(color: t.borderSubtle)),
|
border: Border(bottom: BorderSide(color: t.borderSubtle)),
|
||||||
),
|
),
|
||||||
child: Row(children: [
|
child: Row(
|
||||||
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(name,
|
child: Text(
|
||||||
|
name,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: AppDims.fsBody,
|
fontSize: AppDims.fsBody,
|
||||||
fontWeight:
|
fontWeight: selected ? FontWeight.w600 : FontWeight.w400,
|
||||||
selected ? FontWeight.w600 : FontWeight.w400,
|
color: selected ? t.primary : t.text,
|
||||||
color: selected ? t.primary : t.text)),
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (selected) ...[
|
if (selected) ...[
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Icon(LucideIcons.check, size: 18, color: t.primary),
|
Icon(LucideIcons.check, size: 18, color: t.primary),
|
||||||
],
|
],
|
||||||
]),
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1023,7 +1207,8 @@ class _PartnerSheetBodyState extends State<_PartnerSheetBody> {
|
|||||||
color: t.bg,
|
color: t.bg,
|
||||||
borderRadius: BorderRadius.circular(AppDims.rMd),
|
borderRadius: BorderRadius.circular(AppDims.rMd),
|
||||||
),
|
),
|
||||||
child: Row(children: [
|
child: Row(
|
||||||
|
children: [
|
||||||
Icon(LucideIcons.search, size: 15, color: t.faint),
|
Icon(LucideIcons.search, size: 15, color: t.faint),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -1034,13 +1219,16 @@ class _PartnerSheetBodyState extends State<_PartnerSheetBody> {
|
|||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
hintText: '搜索往来单位…',
|
hintText: '搜索往来单位…',
|
||||||
hintStyle:
|
hintStyle: TextStyle(
|
||||||
TextStyle(fontSize: AppDims.fsBody, color: t.faint),
|
fontSize: AppDims.fsBody,
|
||||||
|
color: t.faint,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onChanged: (v) => setState(() => _kw = v),
|
onChanged: (v) => setState(() => _kw = v),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
]),
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
optRow(
|
optRow(
|
||||||
@@ -1051,8 +1239,10 @@ class _PartnerSheetBodyState extends State<_PartnerSheetBody> {
|
|||||||
if (hits.isEmpty)
|
if (hits.isEmpty)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
child: Text('无匹配结果',
|
child: Text(
|
||||||
style: TextStyle(color: t.muted, fontSize: AppDims.fsSm)),
|
'无匹配结果',
|
||||||
|
style: TextStyle(color: t.muted, fontSize: AppDims.fsSm),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
for (final o in hits)
|
for (final o in hits)
|
||||||
|
|||||||
@@ -0,0 +1,194 @@
|
|||||||
|
// test/finance_summary_search_test.dart — 财务「应收/应付 汇总」往来单位搜索回归。
|
||||||
|
//
|
||||||
|
// financePartnerRowsProvider(GET /finance/summary)不分页、一次性全量加载,
|
||||||
|
// 搜索口径=显示口径:仅需验证本地 contains 过滤在已加载行上生效——桌面
|
||||||
|
// _summaryTable 工具头 DsSearchBox / 移动 应收·应付 分段头 MSearchRow 均覆盖:
|
||||||
|
// 1. 输入关键字 → 汇总行按往来单位名过滤,未命中不显示;
|
||||||
|
// 2. 关键字不匹配任何行 → 展示「没有匹配的往来单位」空态;
|
||||||
|
// 3. 清空关键字 → 恢复全量展示。
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
import 'package:jiu_client/core/api/api_client.dart';
|
||||||
|
import 'package:jiu_client/core/auth/auth_state.dart';
|
||||||
|
import 'package:jiu_client/core/models/page_result.dart';
|
||||||
|
import 'package:jiu_client/core/theme/themes.dart';
|
||||||
|
import 'package:jiu_client/models/finance.dart';
|
||||||
|
import 'package:jiu_client/models/partner.dart';
|
||||||
|
import 'package:jiu_client/models/stock_summary.dart';
|
||||||
|
import 'package:jiu_client/providers/finance_provider.dart';
|
||||||
|
import 'package:jiu_client/providers/partner_provider.dart';
|
||||||
|
import 'package:jiu_client/providers/stock_in_provider.dart';
|
||||||
|
import 'package:jiu_client/providers/stock_out_provider.dart';
|
||||||
|
import 'package:jiu_client/repositories/finance_repository.dart';
|
||||||
|
import 'package:jiu_client/screens/finance/finance_screen.dart';
|
||||||
|
import 'package:jiu_client/widgets/ds/ds_atoms.dart' show DsSearchBox;
|
||||||
|
import 'package:jiu_client/widgets/ds/m_search_row.dart' show MSearchRow;
|
||||||
|
|
||||||
|
/// 不打真实网络:仅用于满足 FinanceRepository 构造签名,方法全部被覆写。
|
||||||
|
class _DummyApiClient extends ApiClient {
|
||||||
|
_DummyApiClient() : super(token: 'test-token');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 固定 3 条往来单位汇总(2 应收 + 1 应付),流水列表恒空(本测试不关心流水)。
|
||||||
|
class _FakeFinanceRepository extends FinanceRepository {
|
||||||
|
_FakeFinanceRepository() : super(_DummyApiClient());
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<PageResult<FinanceRecord>> listRecords({
|
||||||
|
String? type,
|
||||||
|
String? month,
|
||||||
|
String? startDate,
|
||||||
|
String? endDate,
|
||||||
|
int? partnerId,
|
||||||
|
int page = 1,
|
||||||
|
int pageSize = 50,
|
||||||
|
}) async =>
|
||||||
|
const PageResult(data: [], total: 0, page: 1, pageSize: 50);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<PartnerFinanceSummary>> partnerSummary() async => const [
|
||||||
|
PartnerFinanceSummary(
|
||||||
|
partnerId: 1,
|
||||||
|
partnerName: '华东酒类批发',
|
||||||
|
type: 'receivable',
|
||||||
|
recordCount: 2,
|
||||||
|
totalAmount: 128400),
|
||||||
|
PartnerFinanceSummary(
|
||||||
|
partnerId: 2,
|
||||||
|
partnerName: '金樽商贸',
|
||||||
|
type: 'receivable',
|
||||||
|
recordCount: 1,
|
||||||
|
totalAmount: 86200),
|
||||||
|
PartnerFinanceSummary(
|
||||||
|
partnerId: 3,
|
||||||
|
partnerName: '茅台华南经销',
|
||||||
|
type: 'payable',
|
||||||
|
recordCount: 1,
|
||||||
|
totalAmount: 142800),
|
||||||
|
];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<TrendPoint>> trend({int months = 6}) async => const [];
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FakeAllPartners extends PartnerListNotifier {
|
||||||
|
@override
|
||||||
|
Future<PageResult<Partner>> build() async => const PageResult(
|
||||||
|
data: [
|
||||||
|
Partner(id: 1, name: '华东酒类批发', type: 'customer'),
|
||||||
|
Partner(id: 2, name: '金樽商贸', type: 'customer'),
|
||||||
|
Partner(id: 3, name: '茅台华南经销', type: 'supplier'),
|
||||||
|
],
|
||||||
|
total: 3,
|
||||||
|
page: 1,
|
||||||
|
pageSize: 1000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Override> _overrides() => [
|
||||||
|
financeRepositoryProvider.overrideWith((ref) => _FakeFinanceRepository()),
|
||||||
|
allPartnersProvider.overrideWith(() => _FakeAllPartners()),
|
||||||
|
stockOutSummaryProvider.overrideWith(
|
||||||
|
(ref) async => const StockSummary(monthAmount: 0, lastMonthAmount: 0)),
|
||||||
|
stockInSummaryProvider.overrideWith(
|
||||||
|
(ref) async => const StockSummary(monthAmount: 0, lastMonthAmount: 0)),
|
||||||
|
isReadonlyProvider.overrideWithValue(false),
|
||||||
|
];
|
||||||
|
|
||||||
|
Future<void> _pumpFinance(
|
||||||
|
WidgetTester tester, {
|
||||||
|
required Size size,
|
||||||
|
}) async {
|
||||||
|
tester.view.physicalSize = size;
|
||||||
|
tester.view.devicePixelRatio = 1.0;
|
||||||
|
addTearDown(tester.view.resetPhysicalSize);
|
||||||
|
addTearDown(tester.view.resetDevicePixelRatio);
|
||||||
|
|
||||||
|
await tester.pumpWidget(ProviderScope(
|
||||||
|
overrides: _overrides(),
|
||||||
|
child: MaterialApp(
|
||||||
|
debugShowCheckedModeBanner: false,
|
||||||
|
theme: buildTheme('a'),
|
||||||
|
home: const Scaffold(body: FinanceScreen()),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
SharedPreferences.setMockInitialValues({});
|
||||||
|
|
||||||
|
group('桌面汇总卡往来单位搜索', () {
|
||||||
|
testWidgets('输入关键字过滤汇总行,无匹配报空态,清空恢复全量', (tester) async {
|
||||||
|
await _pumpFinance(tester, size: const Size(1280, 900));
|
||||||
|
|
||||||
|
// 初始:3 条汇总行全部展示。
|
||||||
|
expect(find.text('华东酒类批发'), findsOneWidget);
|
||||||
|
expect(find.text('金樽商贸'), findsOneWidget);
|
||||||
|
expect(find.text('茅台华南经销'), findsOneWidget);
|
||||||
|
|
||||||
|
final searchField = find.descendant(
|
||||||
|
of: find.byType(DsSearchBox),
|
||||||
|
matching: find.byType(TextField),
|
||||||
|
);
|
||||||
|
expect(searchField, findsOneWidget);
|
||||||
|
|
||||||
|
// 输入「金樽」:只剩「金樽商贸」。
|
||||||
|
await tester.enterText(searchField, '金樽');
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('金樽商贸'), findsOneWidget);
|
||||||
|
expect(find.text('华东酒类批发'), findsNothing);
|
||||||
|
expect(find.text('茅台华南经销'), findsNothing);
|
||||||
|
|
||||||
|
// 输入不存在的关键字:空态提示。
|
||||||
|
await tester.enterText(searchField, '不存在的单位名');
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('没有匹配的往来单位'), findsOneWidget);
|
||||||
|
expect(find.text('金樽商贸'), findsNothing);
|
||||||
|
|
||||||
|
// 清空:恢复全量 3 行。
|
||||||
|
await tester.enterText(searchField, '');
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('华东酒类批发'), findsOneWidget);
|
||||||
|
expect(find.text('金樽商贸'), findsOneWidget);
|
||||||
|
expect(find.text('茅台华南经销'), findsOneWidget);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('移动应收/应付分段往来单位搜索', () {
|
||||||
|
testWidgets('应收分段输入关键字过滤卡片,清空恢复全量', (tester) async {
|
||||||
|
await _pumpFinance(tester, size: const Size(390, 1600));
|
||||||
|
|
||||||
|
// 默认「应收」分段:仅展示 recv>0 的两条(华东/金樽),不含应付的茅台。
|
||||||
|
expect(find.text('华东酒类批发'), findsOneWidget);
|
||||||
|
expect(find.text('金樽商贸'), findsOneWidget);
|
||||||
|
expect(find.text('茅台华南经销'), findsNothing);
|
||||||
|
|
||||||
|
final searchField = find.descendant(
|
||||||
|
of: find.byType(MSearchRow),
|
||||||
|
matching: find.byType(TextField),
|
||||||
|
);
|
||||||
|
expect(searchField, findsOneWidget);
|
||||||
|
|
||||||
|
await tester.enterText(searchField, '金樽');
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('金樽商贸'), findsOneWidget);
|
||||||
|
expect(find.text('华东酒类批发'), findsNothing);
|
||||||
|
|
||||||
|
// 无匹配 → 分段空态。
|
||||||
|
await tester.enterText(searchField, '不存在的单位名');
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('没有匹配的往来单位'), findsOneWidget);
|
||||||
|
|
||||||
|
// 清空恢复。
|
||||||
|
await tester.enterText(searchField, '');
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('华东酒类批发'), findsOneWidget);
|
||||||
|
expect(find.text('金樽商贸'), findsOneWidget);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 280 KiB After Width: | Height: | Size: 285 KiB |
|
Before Width: | Height: | Size: 277 KiB After Width: | Height: | Size: 282 KiB |
|
Before Width: | Height: | Size: 281 KiB After Width: | Height: | Size: 286 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 108 KiB |
@@ -105,7 +105,7 @@ AppTokens 目前**仅颜色**。原型还驱动:
|
|||||||
| 基础数据(移动) | `m-products.html` | 同上窄屏(卡片流) | 同步 | ✅ golden 自比 | ✅ golden 自比 | ✅ golden 自比 |
|
| 基础数据(移动) | `m-products.html` | 同上窄屏(卡片流) | 同步 | ✅ golden 自比 | ✅ golden 自比 | ✅ golden 自比 |
|
||||||
| 入库列表(桌/移) | `stock-in-list.html` | `stock_in_list_screen.dart` | 同步 | ✅ 副标 + 共享 StatusPill(草稿/待审/已审/已拒);**2026-07-14 六项优化**:详情抽屉明细增加生产日期+批次号两列(抽屉加宽 660)、状态筛选改多选 chip 且默认勾选「草稿+待审核」、新建单默认填充默认仓库(原型 stock-in-list.html/stock-in.js 同步);fidelity a 3.323% / b 2.319% / c 3.633%(均 ≤ 阈值 6%),移动 golden 自比全绿 | ✅ golden | ✅ golden |
|
| 入库列表(桌/移) | `stock-in-list.html` | `stock_in_list_screen.dart` | 同步 | ✅ 副标 + 共享 StatusPill(草稿/待审/已审/已拒);**2026-07-14 六项优化**:详情抽屉明细增加生产日期+批次号两列(抽屉加宽 660)、状态筛选改多选 chip 且默认勾选「草稿+待审核」、新建单默认填充默认仓库(原型 stock-in-list.html/stock-in.js 同步);fidelity a 3.323% / b 2.319% / c 3.633%(均 ≤ 阈值 6%),移动 golden 自比全绿 | ✅ golden | ✅ golden |
|
||||||
| 出库列表(桌/移) | `stock-out-list.html` | `stock_out_list_screen.dart` | 同步 | ✅ 副标 + 共享 StatusPill;**2026-07-14 六项优化**:详情抽屉明细同增生产日期+批次号两列(抽屉 660)、状态筛选改多选 chip 且默认「草稿+待审核」、新建单默认填充默认仓库(原型 stock-out-list.html 同步);fidelity a 3.228% / b 2.217% / c 3.538%(均 ≤ 阈值 6%),移动 golden 自比全绿 | ✅ golden | ✅ golden |
|
| 出库列表(桌/移) | `stock-out-list.html` | `stock_out_list_screen.dart` | 同步 | ✅ 副标 + 共享 StatusPill;**2026-07-14 六项优化**:详情抽屉明细同增生产日期+批次号两列(抽屉 660)、状态筛选改多选 chip 且默认「草稿+待审核」、新建单默认填充默认仓库(原型 stock-out-list.html 同步);fidelity a 3.228% / b 2.217% / c 3.538%(均 ≤ 阈值 6%),移动 golden 自比全绿 | ✅ golden | ✅ golden |
|
||||||
| 财务(桌/移) | `finance.html` | `finance_screen.dart` | 同步 | ✅ **Phase2 重建**:时间范围 chips(真实过滤)+KPI 4 卡(stock summary 环比)+收支趋势柱状图(DsBarChart, /finance/trend)+应收应付汇总(/finance/summary)+流水表+往来抽屉+登记收支(补往来单位);**2026-07-14 六项优化**:流水表筛选新增「往来单位」下拉、时间范围统一改用通用自定义时间组件(原型 finance.html/m-finance.html 同步);fidelity a 4.036% / b 3.131% / c 4.340%(均 ≤ 阈值 6%),移动 golden 自比全绿 | ✅ fidelity | ✅ fidelity |
|
| 财务(桌/移) | `finance.html` | `finance_screen.dart` | 同步 | ✅ **Phase2 重建**:时间范围 chips(真实过滤)+KPI 4 卡(stock summary 环比)+收支趋势柱状图(DsBarChart, /finance/trend)+应收应付汇总(/finance/summary)+流水表+往来抽屉+登记收支(补往来单位);**2026-07-14 六项优化**:流水表筛选新增「往来单位」下拉、时间范围统一改用通用自定义时间组件(原型 finance.html/m-finance.html 同步);fidelity a 4.036% / b 3.131% / c 4.340%(均 ≤ 阈值 6%),移动 golden 自比全绿;**2026-07-14 追加**:应收/应付汇总卡头部新增「往来单位」搜索——本地即时过滤(`/finance/summary` 全量非分页,搜索口径=显示口径),桌面 `DsSearchBox`(`_secHead` trailing)/移动 应收·应付 分段头 `MSearchRow`(流水分段沿用既有 chip→sheet 单选,不重复),原型 finance.html `.sum-search` / m-finance.html 应收应付分段 `.m-search` 同步,finance 相关 6 个整屏 golden + m_finance 3 个 golden 已按新布局 `--update-goldens` | ✅ fidelity | ✅ fidelity |
|
||||||
| 设备管理(桌/移) | `devices.html` | `device_management_screen.dart` | 同步 | ✅ **Phase2 重建**:会话表(DsTable)+外设卡网格(custom_fields.peripherals 本地存档)+打印模板;fidelity 2.4–3.0%≤8% | ✅ fidelity | ✅ fidelity |
|
| 设备管理(桌/移) | `devices.html` | `device_management_screen.dart` | 同步 | ✅ **Phase2 重建**:会话表(DsTable)+外设卡网格(custom_fields.peripherals 本地存档)+打印模板;fidelity 2.4–3.0%≤8% | ✅ fidelity | ✅ fidelity |
|
||||||
| 系统设置(桌/移) | `settings.html` | `settings_screen.dart` | 同步 | ✅ **Phase2 重建**;假「系统参数」已删;fidelity 1.7–2.3%≤8%;**2026-07-10 授权管理面板迁出**(提升为独立一级屏 `license.html`),subnav 剩 门店信息/用户管理/偏好——**真实端已同步完成**(`settings_screen.dart` 子导航同步摘除授权管理项,golden 已按新态重生成,见授权管理两行) | ✅ fidelity | ✅ fidelity |
|
| 系统设置(桌/移) | `settings.html` | `settings_screen.dart` | 同步 | ✅ **Phase2 重建**;假「系统参数」已删;fidelity 1.7–2.3%≤8%;**2026-07-10 授权管理面板迁出**(提升为独立一级屏 `license.html`),subnav 剩 门店信息/用户管理/偏好——**真实端已同步完成**(`settings_screen.dart` 子导航同步摘除授权管理项,golden 已按新态重生成,见授权管理两行) | ✅ fidelity | ✅ fidelity |
|
||||||
| 用户管理(桌/移) | `users.html` | `users_screen.dart`(`/settings/users`) | 同步 | ✅ **Phase2 新独立页**:KPI 4 卡+搜索/角色筛选+rcard 弹窗,角色四级拉平;fidelity 1.3–2.1%≤8% | ✅ fidelity | ✅ fidelity |
|
| 用户管理(桌/移) | `users.html` | `users_screen.dart`(`/settings/users`) | 同步 | ✅ **Phase2 新独立页**:KPI 4 卡+搜索/角色筛选+rcard 弹窗,角色四级拉平;fidelity 1.3–2.1%≤8% | ✅ fidelity | ✅ fidelity |
|
||||||
|
|||||||
@@ -28,6 +28,8 @@
|
|||||||
.sec-head{display:flex; align-items:baseline; gap:10px; margin:0 0 12px;}
|
.sec-head{display:flex; align-items:baseline; gap:10px; margin:0 0 12px;}
|
||||||
.sec-head h2{margin:0; font-size:var(--fs-h2); color:var(--heading); font-weight:700;}
|
.sec-head h2{margin:0; font-size:var(--fs-h2); color:var(--heading); font-weight:700;}
|
||||||
.sec-head .sh-sub{font-size:var(--fs-xs); color:var(--muted);}
|
.sec-head .sh-sub{font-size:var(--fs-xs); color:var(--muted);}
|
||||||
|
/* 汇总卡头部往来单位搜索(即时本地过滤,非 combo 单选) */
|
||||||
|
.sec-head .sum-search{margin-left:auto; width:220px; align-self:center;}
|
||||||
.block{margin-bottom:22px;}
|
.block{margin-bottom:22px;}
|
||||||
|
|
||||||
/* ====== 新增原子:条形图 .barchart / .bar ====== */
|
/* ====== 新增原子:条形图 .barchart / .bar ====== */
|
||||||
@@ -108,7 +110,9 @@
|
|||||||
|
|
||||||
<!-- 4. 应收/应付 汇总表 -->
|
<!-- 4. 应收/应付 汇总表 -->
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<div class="sec-head"><h2>应收 / 应付 汇总</h2><span class="sh-sub">按往来单位 · 点击行查看流水</span></div>
|
<div class="sec-head"><h2>应收 / 应付 汇总</h2><span class="sh-sub">按往来单位 · 点击行查看流水</span>
|
||||||
|
<div class="searchbox sum-search"><svg viewBox="0 0 24 24"><use href="#i-search"/></svg><input id="sumSearch" placeholder="搜索往来单位…" oninput="sumKw=this.value; buildSummary();"></div>
|
||||||
|
</div>
|
||||||
<div class="table"><table>
|
<div class="table"><table>
|
||||||
<thead><tr><th>往来单位</th><th class="num">应收</th><th class="num">应付</th><th class="num">净额</th><th class="act">操作</th></tr></thead>
|
<thead><tr><th>往来单位</th><th class="num">应收</th><th class="num">应付</th><th class="num">净额</th><th class="act">操作</th></tr></thead>
|
||||||
<tbody id="sumBody"></tbody>
|
<tbody id="sumBody"></tbody>
|
||||||
@@ -181,19 +185,23 @@ const SUMMARY=[
|
|||||||
{name:'川渝糖酒集团',recv:0,pay:60400}
|
{name:'川渝糖酒集团',recv:0,pay:60400}
|
||||||
];
|
];
|
||||||
function yuan(n){ return '¥'+n.toLocaleString(); }
|
function yuan(n){ return '¥'+n.toLocaleString(); }
|
||||||
|
// 汇总卡「往来单位」搜索:本地即时过滤(数据已全量加载,非服务端分页,搜索口径=显示口径)
|
||||||
|
let sumKw='';
|
||||||
function buildSummary(){
|
function buildSummary(){
|
||||||
document.getElementById('sumBody').innerHTML=SUMMARY.map((s,i)=>{
|
const q=sumKw.trim().toLowerCase();
|
||||||
|
const rows=SUMMARY.map((s,i)=>({...s,i})).filter(s=>!q||s.name.toLowerCase().includes(q));
|
||||||
|
document.getElementById('sumBody').innerHTML=rows.length?rows.map(s=>{
|
||||||
const net=s.recv-s.pay;
|
const net=s.recv-s.pay;
|
||||||
const netCls=net>0?'net-pos':(net<0?'net-neg':'');
|
const netCls=net>0?'net-pos':(net<0?'net-neg':'');
|
||||||
const netTxt=(net>0?'+':'')+yuan(net);
|
const netTxt=(net>0?'+':'')+yuan(net);
|
||||||
return `<tr class="click" onclick="openPartner(${i})">
|
return `<tr class="click" onclick="openPartner(${s.i})">
|
||||||
<td class="pname">${s.name}</td>
|
<td class="pname">${s.name}</td>
|
||||||
<td class="num">${s.recv?yuan(s.recv):'—'}</td>
|
<td class="num">${s.recv?yuan(s.recv):'—'}</td>
|
||||||
<td class="num">${s.pay?yuan(s.pay):'—'}</td>
|
<td class="num">${s.pay?yuan(s.pay):'—'}</td>
|
||||||
<td class="num ${netCls}">${netTxt}</td>
|
<td class="num ${netCls}">${netTxt}</td>
|
||||||
<td class="act" onclick="event.stopPropagation()"><span class="link" onclick="openPartner(${i})">查看</span></td>
|
<td class="act" onclick="event.stopPropagation()"><span class="link" onclick="openPartner(${s.i})">查看</span></td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
}).join('');
|
}).join(''):`<tr><td colspan="5"><div class="empty">没有匹配的往来单位</div></td></tr>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- 5. 收支流水 ---- */
|
/* ---- 5. 收支流水 ---- */
|
||||||
|
|||||||
@@ -38,6 +38,9 @@
|
|||||||
<div class="k"><div class="kl">应付合计</div><div class="kv">¥9.6万</div><div class="kd warn">5 笔未结</div></div>
|
<div class="k"><div class="kl">应付合计</div><div class="kv">¥9.6万</div><div class="kd warn">5 笔未结</div></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="seg" style="width:100%; margin-top:14px"><button class="on" style="flex:1" onclick="setTab('ar')">应收</button><button style="flex:1" onclick="setTab('ap')">应付</button><button style="flex:1" onclick="setTab('flow')">流水</button></div>
|
<div class="seg" style="width:100%; margin-top:14px"><button class="on" style="flex:1" onclick="setTab('ar')">应收</button><button style="flex:1" onclick="setTab('ap')">应付</button><button style="flex:1" onclick="setTab('flow')">流水</button></div>
|
||||||
|
<!-- 应收/应付分段头部往来单位搜索(同口径本地过滤,仅 ar/ap 显示;流水分段
|
||||||
|
沿用上方时间行「往来单位」chip → sheet 单选,不重复) -->
|
||||||
|
<div class="m-search" id="sumSearchRow" style="margin-top:12px"><svg viewBox="0 0 24 24"><use href="#i-search"/></svg><input id="sumKwInput" placeholder="搜索往来单位…" oninput="sumKw=this.value; render();"></div>
|
||||||
<div class="m-section" id="cnt">应收账款</div>
|
<div class="m-section" id="cnt">应收账款</div>
|
||||||
<div id="list"></div>
|
<div id="list"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -100,15 +103,22 @@ function drawPartnerOpts(kw){
|
|||||||
}
|
}
|
||||||
function setFlowParty(v){ flowParty=v; closeSheet(); render(); }
|
function setFlowParty(v){ flowParty=v; closeSheet(); render(); }
|
||||||
|
|
||||||
|
// 应收/应付分段「往来单位」搜索:本地即时过滤(数据已全量加载,搜索口径=显示口径)
|
||||||
|
let sumKw='';
|
||||||
function render(){
|
function render(){
|
||||||
const cfg={ar:['应收账款',AR],ap:['应付账款',AP],flow:['资金流水',FLOW]}[tab];
|
const cfg={ar:['应收账款',AR],ap:['应付账款',AP],flow:['资金流水',FLOW]}[tab];
|
||||||
// 往来单位筛选仅作用于「流水」分段(对应真实端 financeListProvider.setPartner 只过滤流水表)
|
// 往来单位筛选仅作用于「流水」分段(对应真实端 financeListProvider.setPartner 只过滤流水表)
|
||||||
const rows=tab==='flow'?cfg[1].filter(r=>flowParty==='全部'||r.party.split(' · ')[0]===flowParty):cfg[1];
|
let rows=tab==='flow'?cfg[1].filter(r=>flowParty==='全部'||r.party.split(' · ')[0]===flowParty):cfg[1];
|
||||||
|
document.getElementById('sumSearchRow').style.display=tab==='flow'?'none':'flex';
|
||||||
|
if(tab!=='flow'){
|
||||||
|
const q=sumKw.trim().toLowerCase();
|
||||||
|
if(q) rows=rows.filter(r=>r.party.toLowerCase().includes(q));
|
||||||
|
}
|
||||||
document.getElementById('cnt').textContent=`${cfg[0]} · 共 ${rows.length}`;
|
document.getElementById('cnt').textContent=`${cfg[0]} · 共 ${rows.length}`;
|
||||||
document.getElementById('list').innerHTML=rows.map(r=>`<div class="m-card"><div class="m-row">
|
document.getElementById('list').innerHTML=rows.length?rows.map(r=>`<div class="m-card"><div class="m-row">
|
||||||
<div class="mc-main"><div class="mc-nm">${r.party}</div><div class="mc-sub">${r.date}</div></div>
|
<div class="mc-main"><div class="mc-nm">${r.party}</div><div class="mc-sub">${r.date}</div></div>
|
||||||
<div class="mc-right"><span class="badge b-${r.status}">${r.status}</span><span class="mc-amt">${r.amt}</span></div>
|
<div class="mc-right"><span class="badge b-${r.status}">${r.status}</span><span class="mc-amt">${r.amt}</span></div>
|
||||||
</div></div>`).join('');
|
</div></div>`).join(''):`<div class="m-empty"><svg viewBox="0 0 24 24"><use href="#i-box"/></svg>${tab==='flow'?'暂无数据':'没有匹配的往来单位'}</div>`;
|
||||||
document.getElementById('partyVal').textContent=flowParty==='全部'?'':flowParty;
|
document.getElementById('partyVal').textContent=flowParty==='全部'?'':flowParty;
|
||||||
document.getElementById('chipParty').classList.toggle('on',flowParty!=='全部');
|
document.getElementById('chipParty').classList.toggle('on',flowParty!=='全部');
|
||||||
}
|
}
|
||||||
|
|||||||