feat(client): 往来单位屏设计语言重建(副标/状态徽章/两行名称/响应式 toolbar)

- 副标「共 N 个供应商/客户」(还原 .head .sub)
- 状态 → StatusPill(启用绿/停用灰,新 _PartnerStatusPill),桌面+移动一致
- 编码+名称合并两行(name 加粗 + code 等宽 muted)
- toolbar 响应式(窄屏搜索独占一行 + 操作行)+ token 间距,修预存窄屏溢出
- 整屏 golden ×三主题(桌面+移动)入回归闸
暂缓(数据缺口/有意偏差):应收/应付列(Partner 模型无该字段)、
类型 chips(现保留 供应商/客户 两 Tab)。

analyze 0 error · flutter test +181(往来桌面/移动各 3 golden)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSKEiHsvauyxYUW2itzUXX
This commit is contained in:
wangjia
2026-06-25 13:05:12 +08:00
parent a661afe0d0
commit 07d3559d0a
8 changed files with 190 additions and 57 deletions
@@ -6,9 +6,11 @@ import '../../core/models/page_result.dart';
import '../../core/responsive/responsive.dart';
import '../../core/config/app_constants.dart';
import '../../core/theme/context_tokens.dart';
import '../../core/theme/app_dims.g.dart';
import '../../models/partner.dart';
import '../../providers/partner_provider.dart';
import '../../widgets/data_table_card.dart';
import '../../widgets/kpi_card.dart';
import '../../widgets/mobile_list_card.dart';
import '../../widgets/page_scaffold.dart';
import '../../core/utils/export_util.dart';
@@ -154,15 +156,7 @@ class _PartnersScreenState extends ConsumerState<PartnersScreen> {
return MobileListCard(
title: Text(p.name),
subtitle: p.code != null ? Text(p.code!) : null,
trailing: Text(
p.status == 'enabled' ? '正常' : '禁用',
style: TextStyle(
fontSize: 12,
color: p.status == 'enabled'
? context.tokens.success
: context.tokens.muted,
),
),
trailing: _PartnerStatusPill(p.status),
fields: [
if (p.contact?.isNotEmpty == true) MobileCardField('联系人', p.contact),
if (p.phone?.isNotEmpty == true) MobileCardField('电话', p.phone),
@@ -190,24 +184,48 @@ class _PartnersScreenState extends ConsumerState<PartnersScreen> {
);
}
return DataTableCard(
return Column(
children: [
// 副标(还原原型 .head .sub
Container(
width: double.infinity,
color: context.tokens.bg,
padding: const EdgeInsets.fromLTRB(
AppDims.sp3, AppDims.sp3, AppDims.sp3, 0),
child: Text(
'${result.total}${isSupplier ? '供应商' : '客户'}',
style: TextStyle(
fontSize: AppDims.fsSm, color: context.tokens.muted),
),
),
Expanded(
child: DataTableCard(
totalCount: result.total,
page: result.page,
pageSize: result.pageSize,
onPageChanged: onPageChanged,
onPageSizeChanged: onPageSizeChanged,
mobileCards: partners.map(partnerCard).toList(),
toolbar: Row(
children: [
toolbar: Builder(builder: (ctx) {
final searchField = TextField(
controller: searchCtrl,
decoration: InputDecoration(
hintText: isSupplier ? '搜索供应商名称/编码' : '搜索客户名称/编码',
prefixIcon: const Icon(Icons.search, size: 16),
hintStyle: const TextStyle(fontSize: 13),
),
onChanged: onSearchChanged,
);
final buttons = <Widget>[
if (!WriteGuard.isReadonly(ref)) ...[
WriteGuard(
child: ElevatedButton.icon(
onPressed: onAdd,
icon: const Icon(Icons.add, size: 16),
label: Text(isSupplier ? '新建' : '新建'),
label: const Text('新建'),
),
),
const SizedBox(width: 8),
const SizedBox(width: AppDims.sp2),
],
OutlinedButton.icon(
onPressed: () => exportExcel(
@@ -221,34 +239,37 @@ class _PartnersScreenState extends ConsumerState<PartnersScreen> {
icon: const Icon(Icons.download, size: 16),
label: const Text('导出'),
),
const Spacer(),
SizedBox(
width: 200,
child: TextField(
controller: searchCtrl,
decoration: InputDecoration(
hintText: isSupplier ? '搜索供应商名称/编码' : '搜索客户名称/编码',
prefixIcon: const Icon(Icons.search, size: 16),
hintStyle: const TextStyle(fontSize: 13),
),
onChanged: onSearchChanged,
),
),
],
),
columns: [
const DataColumn(label: Text('编码')),
const DataColumn(label: Text('名称')),
const DataColumn(label: Text('联系人')),
const DataColumn(label: Text('联系电话')),
const DataColumn(label: Text('地址')),
const DataColumn(label: Text('状态')),
const DataColumn(label: Text('操作')),
];
if (ctx.isMobile) {
// 窄屏:搜索独占一行 + 操作按钮一行
return Column(
mainAxisSize: MainAxisSize.min,
children: [
searchField,
const SizedBox(height: AppDims.sp2),
Row(children: [...buttons, const Spacer()]),
],
);
}
return Row(
children: [
...buttons,
const Spacer(),
SizedBox(width: 240, child: searchField),
],
);
}),
columns: const [
DataColumn(label: Text('名称')),
DataColumn(label: Text('联系人')),
DataColumn(label: Text('联系电话')),
DataColumn(label: Text('地址')),
DataColumn(label: Text('状态')),
DataColumn(label: Text('操作')),
],
rows: partners.isEmpty
? [
DataRow(cells: [
const DataCell(SizedBox()),
DataCell(Text(isSupplier ? '暂无供应商' : '暂无客户',
style:
TextStyle(color: context.tokens.muted))),
@@ -262,17 +283,24 @@ class _PartnersScreenState extends ConsumerState<PartnersScreen> {
: partners
.map((p) => DataRow(
cells: [
DataCell(Text(p.code ?? '-',
style: TextStyle(
fontFamily: 'monospace',
fontSize: 12,
color: context.tokens.muted))),
DataCell(SizedBox(
width: 180,
child: Text(p.name,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontWeight: FontWeight.w500)),
width: 200,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(p.name,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontWeight: FontWeight.w600)),
if ((p.code ?? '').isNotEmpty)
Text(p.code!,
style: TextStyle(
fontFamily: 'monospace',
fontSize: AppDims.fsXs,
color: context.tokens.muted)),
],
),
)),
DataCell(Text(p.contact ?? '-')),
DataCell(Text(p.phone ?? '-')),
@@ -281,15 +309,7 @@ class _PartnersScreenState extends ConsumerState<PartnersScreen> {
child: Text(p.address ?? '-',
overflow: TextOverflow.ellipsis),
)),
DataCell(Text(
p.status == 'enabled' ? '正常' : '禁用',
style: TextStyle(
fontSize: 13,
color: p.status == 'enabled'
? context.tokens.success
: context.tokens.muted,
),
)),
DataCell(_PartnerStatusPill(p.status)),
DataCell(Row(
mainAxisSize: MainAxisSize.min,
children: WriteGuard.isReadonly(ref)
@@ -318,6 +338,9 @@ class _PartnersScreenState extends ConsumerState<PartnersScreen> {
],
))
.toList(),
),
),
],
);
}
@@ -388,6 +411,20 @@ class _PartnersScreenState extends ConsumerState<PartnersScreen> {
}
}
// 往来单位状态徽章(启用/停用),还原原型 .badge 圆点软底。
class _PartnerStatusPill extends StatelessWidget {
final String status;
const _PartnerStatusPill(this.status);
@override
Widget build(BuildContext context) {
final t = context.tokens;
return status == 'enabled'
? StatusPill(label: '启用', color: t.success, background: t.okSoft)
: StatusPill(label: '停用', color: t.muted, background: t.borderSubtle);
}
}
class _PartnerFormDialog extends ConsumerStatefulWidget {
final bool isSupplier;
final Partner? partner;