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:
@@ -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;
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
@@ -0,0 +1,96 @@
|
||||
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/models/page_result.dart';
|
||||
import 'package:jiu_client/core/auth/auth_state.dart';
|
||||
import 'package:jiu_client/models/partner.dart';
|
||||
import 'package:jiu_client/providers/partner_provider.dart';
|
||||
import 'package:jiu_client/screens/partners/partners_screen.dart';
|
||||
|
||||
import '../support/golden_harness.dart';
|
||||
|
||||
/// design-distill 阶段4:往来单位整屏 golden × 三主题(桌面 + 移动,回归闸)。
|
||||
/// 保真目检:shoot partners.html + montage 与 goldens/partners_*.png 并排。
|
||||
/// 更新基准:flutter test --update-goldens test/golden/partners_golden_test.dart
|
||||
|
||||
const _suppliers = [
|
||||
Partner(
|
||||
id: 1, code: 'GYS-001', name: '茅台华东总代', type: 'supplier',
|
||||
contact: '张伟', phone: '138-0011-2233', address: '上海市黄浦区中山东路 8 号',
|
||||
status: 'enabled'),
|
||||
Partner(
|
||||
id: 2, code: 'GYS-002', name: '老窖酒水批发', type: 'supplier',
|
||||
contact: '李建国', phone: '139-2200-7788', address: '江苏省南京市建邺区江东中路',
|
||||
status: 'enabled'),
|
||||
Partner(
|
||||
id: 3, code: 'GYS-003', name: '五粮液川南运营中心', type: 'supplier',
|
||||
contact: '刘海涛', phone: '135-8899-0011', address: '四川省宜宾市翠屏区岷江路',
|
||||
status: 'disabled'),
|
||||
];
|
||||
|
||||
const _customers = [
|
||||
Partner(
|
||||
id: 11, code: 'KH-001', name: '鼎丰超市', type: 'customer',
|
||||
contact: '王芳', phone: '137-6655-4321', address: '浙江省杭州市西湖区文三路',
|
||||
status: 'enabled'),
|
||||
Partner(
|
||||
id: 12, code: 'KH-002', name: '金樽酒楼', type: 'customer',
|
||||
contact: '陈静', phone: '136-2211-9900', address: '江苏省苏州市姑苏区干将路',
|
||||
status: 'enabled'),
|
||||
];
|
||||
|
||||
class _FakeSupplierNotifier extends PartnerListNotifier {
|
||||
@override
|
||||
Future<PageResult<Partner>> build() async => const PageResult(
|
||||
data: _suppliers, total: 3, page: 1, pageSize: 20);
|
||||
@override
|
||||
void setPage(int page) {}
|
||||
@override
|
||||
void setPageSize(int pageSize) {}
|
||||
@override
|
||||
void setKeyword(String keyword) {}
|
||||
@override
|
||||
void reload() {}
|
||||
}
|
||||
|
||||
class _FakeCustomerNotifier extends PartnerListNotifier {
|
||||
@override
|
||||
Future<PageResult<Partner>> build() async => const PageResult(
|
||||
data: _customers, total: 2, page: 1, pageSize: 20);
|
||||
@override
|
||||
void setPage(int page) {}
|
||||
@override
|
||||
void setPageSize(int pageSize) {}
|
||||
@override
|
||||
void setKeyword(String keyword) {}
|
||||
@override
|
||||
void reload() {}
|
||||
}
|
||||
|
||||
List<Override> _overrides() => [
|
||||
supplierListProvider.overrideWith(() => _FakeSupplierNotifier()),
|
||||
customerListProvider.overrideWith(() => _FakeCustomerNotifier()),
|
||||
isReadonlyProvider.overrideWithValue(false),
|
||||
];
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
|
||||
goldenAcrossThemes(
|
||||
'partners 整屏(桌面)',
|
||||
goldenPrefix: 'partners',
|
||||
child: () => const Scaffold(body: PartnersScreen()),
|
||||
overrides: _overrides,
|
||||
logical: const Size(1280, 900),
|
||||
);
|
||||
|
||||
goldenAcrossThemes(
|
||||
'partners 整屏(移动)',
|
||||
goldenPrefix: 'partners_mobile',
|
||||
child: () => const Scaffold(body: PartnersScreen()),
|
||||
overrides: _overrides,
|
||||
logical: const Size(390, 1200),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user