feat(client): 往来/基础数据/商品详情窄屏对齐移动原型(卡片流/图标徽章/sheet 表单)
- 往来单位:窄屏隐藏页内大标题(标题在壳顶栏);MSearchRow 搜索行; 类型 pills 全部/供应商/客户/两者(两者=客户端过滤,服务端 FIND_IN_SET 无独立值); 卡片流类型+状态徽章换图标变体、mc-sub mono 编码 - 基础数据:窄屏隐藏大标题;各 tab 搜索行换 MSearchRow(清空 × 并入,重置钮撤); 档案/字典卡片副行 mono 编码;系列/规格/仓库 新增编辑窄屏走 showMSheet 底部 sheet 轻表单 - 商品详情:窄屏 padding 收紧 sp4;信息卡 Wrap→单列;图片缩略图行窄屏横滚防溢出; 二维码弹窗宽度走 context.dialogWidth - golden:新增 m_partners / m_products / m_products_series(390×844 @2x 三主题); 重生成既有 partners_mobile / products_archive_mobile 基准(桌面 golden 零漂移) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@@ -18,6 +18,8 @@ import '../../models/partner.dart';
|
||||
import '../../providers/partner_provider.dart';
|
||||
import '../../widgets/ds/ds_atoms.dart';
|
||||
import '../../widgets/ds/ds_table.dart';
|
||||
import '../../widgets/ds/m_search_row.dart';
|
||||
import '../../widgets/ds/status_icon_map.dart';
|
||||
import '../../widgets/mobile_list_card.dart';
|
||||
import '../../widgets/partner_detail_drawer.dart';
|
||||
import '../../widgets/write_guard.dart';
|
||||
@@ -38,6 +40,9 @@ class _PartnersScreenState extends ConsumerState<PartnersScreen> {
|
||||
String _typeChip = '全部';
|
||||
|
||||
static const _typeChips = ['全部', '供应商', '客户'];
|
||||
// 窄屏对齐 m-partners 原型 F=['全部','供应商','客户','两者'];
|
||||
// 「两者」后端无独立 type 值(FIND_IN_SET 语义),列表本就一页取全 → 客户端过滤。
|
||||
static const _typeChipsMobile = ['全部', '供应商', '客户', '两者'];
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
@@ -89,41 +94,42 @@ class _PartnersScreenState extends ConsumerState<PartnersScreen> {
|
||||
),
|
||||
),
|
||||
data: (result) {
|
||||
final partners = result.data;
|
||||
final mobile = context.isMobile;
|
||||
// 原型 .main{padding:22px 26px};窄屏保持紧凑
|
||||
// 「两者」为窄屏专属 chip:服务端 FIND_IN_SET 无独立值,客户端过滤
|
||||
final partners = _typeChip == '两者'
|
||||
? result.data.where((p) => p.typeLabel == '两者').toList()
|
||||
: result.data;
|
||||
// 原型 .main{padding:22px 26px};窄屏对齐 m-partners .m-scroll{padding:14px}
|
||||
return Container(
|
||||
color: t.bg,
|
||||
padding: mobile
|
||||
? EdgeInsets.zero
|
||||
? const EdgeInsets.fromLTRB(AppDims.sp3, AppDims.sp3, AppDims.sp3, 0)
|
||||
: const EdgeInsets.fromLTRB(26, 22, 26, 22),
|
||||
child: Column(
|
||||
children: [
|
||||
// ── 头部(原型 .head{margin-bottom:18px})──
|
||||
Container(
|
||||
width: double.infinity,
|
||||
color: t.bg,
|
||||
padding: mobile
|
||||
? const EdgeInsets.fromLTRB(
|
||||
AppDims.sp4, AppDims.sp4, AppDims.sp4, AppDims.sp2)
|
||||
: const EdgeInsets.only(bottom: 18),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text('往来单位',
|
||||
style: TextStyle(
|
||||
fontSize: AppDims.fsH1,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: t.heading)),
|
||||
const SizedBox(width: AppDims.sp3),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 2),
|
||||
child: Text('供应商与客户档案 · 共 ${result.total} 个',
|
||||
// ── 头部(原型 .head{margin-bottom:18px});窄屏隐藏——
|
||||
// 标题由壳顶栏 .m-top data-title 提供(对齐 m-partners)──
|
||||
if (!mobile)
|
||||
Container(
|
||||
width: double.infinity,
|
||||
color: t.bg,
|
||||
padding: const EdgeInsets.only(bottom: 18),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text('往来单位',
|
||||
style: TextStyle(
|
||||
fontSize: AppDims.fsSm, color: t.muted)),
|
||||
),
|
||||
const Spacer(),
|
||||
if (!mobile) ...[
|
||||
fontSize: AppDims.fsH1,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: t.heading)),
|
||||
const SizedBox(width: AppDims.sp3),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 2),
|
||||
child: Text('供应商与客户档案 · 共 ${result.total} 个',
|
||||
style: TextStyle(
|
||||
fontSize: AppDims.fsSm, color: t.muted)),
|
||||
),
|
||||
const Spacer(),
|
||||
DsButton('刷新',
|
||||
icon: LucideIcons.refreshCw, onPressed: _reload),
|
||||
const SizedBox(width: 10),
|
||||
@@ -134,9 +140,8 @@ class _PartnersScreenState extends ConsumerState<PartnersScreen> {
|
||||
onPressed: () => _openForm()),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: DsTable(
|
||||
pagerInfoText: '共 ${result.total} 个往来单位',
|
||||
@@ -167,18 +172,61 @@ class _PartnersScreenState extends ConsumerState<PartnersScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
// ── toolbar(原型:searchbox 260 + chips + sp + 重置)──
|
||||
// ── toolbar(原型:searchbox 260 + chips + sp + 重置;
|
||||
// 窄屏对齐 m-partners:.m-search 搜索行 + .m-pill 类型横排)──
|
||||
Widget _toolbar(bool mobile, List<Partner> partners) {
|
||||
void onSearch(String v) {
|
||||
_debounce?.cancel();
|
||||
_debounce = Timer(AppConstants.searchDebounce, () {
|
||||
ref.read(partnersScreenProvider.notifier).setKeyword(v.trim());
|
||||
});
|
||||
}
|
||||
|
||||
if (mobile) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
MSearchRow(
|
||||
controller: _searchCtrl,
|
||||
hint: '名称 / 拼音',
|
||||
onChanged: onSearch,
|
||||
),
|
||||
const SizedBox(height: AppDims.sp2),
|
||||
Row(children: [
|
||||
// 类型 pills(原型 m-pill 的既有对应物 DsChip,selected 态)
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(children: [
|
||||
for (final c in _typeChipsMobile) ...[
|
||||
DsChip(
|
||||
label: c,
|
||||
selected: _typeChip == c,
|
||||
caret: false,
|
||||
onTap: () => _setType(c)),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
]),
|
||||
),
|
||||
),
|
||||
WriteGuard(
|
||||
child: DsButton('新增',
|
||||
small: true,
|
||||
icon: LucideIcons.plus,
|
||||
variant: DsBtnVariant.primary,
|
||||
onPressed: () => _openForm()),
|
||||
),
|
||||
]),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
final search = DsSearchBox(
|
||||
controller: _searchCtrl,
|
||||
hint: '名称 / 拼音',
|
||||
width: mobile ? null : 260,
|
||||
onChanged: (v) {
|
||||
_debounce?.cancel();
|
||||
_debounce = Timer(AppConstants.searchDebounce, () {
|
||||
ref.read(partnersScreenProvider.notifier).setKeyword(v.trim());
|
||||
});
|
||||
},
|
||||
width: 260,
|
||||
onChanged: onSearch,
|
||||
);
|
||||
final chips = [
|
||||
for (final c in _typeChips) ...[
|
||||
@@ -209,27 +257,6 @@ class _PartnersScreenState extends ConsumerState<PartnersScreen> {
|
||||
.toList(),
|
||||
);
|
||||
});
|
||||
if (mobile) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
search,
|
||||
const SizedBox(height: AppDims.sp2),
|
||||
Row(children: [
|
||||
...chips,
|
||||
const Spacer(),
|
||||
WriteGuard(
|
||||
child: DsButton('新增',
|
||||
small: true,
|
||||
icon: LucideIcons.plus,
|
||||
variant: DsBtnVariant.primary,
|
||||
onPressed: () => _openForm()),
|
||||
),
|
||||
]),
|
||||
],
|
||||
);
|
||||
}
|
||||
return Row(children: [
|
||||
search,
|
||||
const SizedBox(width: 10),
|
||||
@@ -312,13 +339,40 @@ class _PartnersScreenState extends ConsumerState<PartnersScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
// ── 窄屏卡片(m-partners:右侧类型徽章 + 应收/应付)──
|
||||
/// 类型徽章图标变体(原型 m-partners:badge b-供应商/客户/两者 + BADGE_ICON)。
|
||||
/// 色调与 partnerTypeBadge(partner_detail_drawer)一致,仅多前导图标。
|
||||
DsBadge _typeBadgeIcon(Partner p) => DsBadge(p.typeLabel,
|
||||
tone: switch (p.typeLabel) {
|
||||
'两者' => DsBadgeTone.warn,
|
||||
'客户' => DsBadgeTone.accent,
|
||||
_ => DsBadgeTone.info,
|
||||
},
|
||||
icon: statusIcon(p.typeLabel));
|
||||
|
||||
/// 状态徽章图标变体(原型 badge b-启用/b-停用)。
|
||||
DsBadge _statusBadgeIcon(Partner p) {
|
||||
final label = p.status == 'disabled' ? '停用' : '启用';
|
||||
return DsBadge(label,
|
||||
tone: label == '启用' ? DsBadgeTone.ok : DsBadgeTone.muted,
|
||||
icon: statusIcon(label));
|
||||
}
|
||||
|
||||
// ── 窄屏卡片(m-partners:mc-sub mono 编码 + 右侧类型/状态图标徽章)──
|
||||
Widget _card(Partner p, Map<int, ({double recv, double pay})> bal) {
|
||||
final b = bal[p.id];
|
||||
return MobileListCard(
|
||||
title: Text(p.name),
|
||||
subtitle: (p.code?.isNotEmpty == true) ? Text(p.code!) : null,
|
||||
trailing: partnerTypeBadge(p),
|
||||
subtitle: (p.code?.isNotEmpty == true)
|
||||
? Text(p.code!,
|
||||
style: const TextStyle(
|
||||
fontFamily: AppFonts.mono,
|
||||
fontFamilyFallback: AppFonts.monoFallback))
|
||||
: null,
|
||||
trailing: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
_typeBadgeIcon(p),
|
||||
const SizedBox(width: 6),
|
||||
_statusBadgeIcon(p),
|
||||
]),
|
||||
fields: [
|
||||
if (p.contact?.isNotEmpty == true) MobileCardField('联系人', p.contact),
|
||||
if (p.phone?.isNotEmpty == true) MobileCardField('电话', p.phone),
|
||||
|
||||
@@ -291,7 +291,8 @@ class _ProductDetailScreenState extends ConsumerState<ProductDetailScreen> {
|
||||
}
|
||||
final p = _product!;
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
// 桌面 24;窄屏收紧到 sp4,避免内容区过窄
|
||||
padding: EdgeInsets.all(context.isMobile ? AppDims.sp4 : 24.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -579,29 +580,31 @@ class _ProductDetailScreenState extends ConsumerState<ProductDetailScreen> {
|
||||
fontWeight: FontWeight.w600,
|
||||
color: context.tokens.muted)),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
height: 120,
|
||||
child: Row(
|
||||
children: [
|
||||
...p.images.asMap().entries.map((e) => _ImageThumbnail(
|
||||
url: AppConfig.baseUrl + e.value.url,
|
||||
onDelete: () => _deleteImage(e.value),
|
||||
onView: () => showFullscreenImages(
|
||||
context,
|
||||
p.images
|
||||
.map((img) => AppConfig.baseUrl + img.url)
|
||||
.toList(),
|
||||
initialIndex: e.key,
|
||||
),
|
||||
)),
|
||||
if (p.images.length < 5)
|
||||
WriteGuard(
|
||||
child: _UploadButton(
|
||||
uploading: _uploading, onTap: _pickAndUpload),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Builder(builder: (context) {
|
||||
final children = <Widget>[
|
||||
...p.images.asMap().entries.map((e) => _ImageThumbnail(
|
||||
url: AppConfig.baseUrl + e.value.url,
|
||||
onDelete: () => _deleteImage(e.value),
|
||||
onView: () => showFullscreenImages(
|
||||
context,
|
||||
p.images.map((img) => AppConfig.baseUrl + img.url).toList(),
|
||||
initialIndex: e.key,
|
||||
),
|
||||
)),
|
||||
if (p.images.length < 5)
|
||||
WriteGuard(
|
||||
child:
|
||||
_UploadButton(uploading: _uploading, onTap: _pickAndUpload),
|
||||
),
|
||||
];
|
||||
return SizedBox(
|
||||
height: 120,
|
||||
// 窄屏:缩略图横向滚动(多图时 Row 会溢出 390 视口)
|
||||
child: context.isMobile
|
||||
? ListView(scrollDirection: Axis.horizontal, children: children)
|
||||
: Row(children: children),
|
||||
);
|
||||
}),
|
||||
if (p.images.isEmpty && !_uploading)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
@@ -630,6 +633,34 @@ class _ProductDetailScreenState extends ConsumerState<ProductDetailScreen> {
|
||||
),
|
||||
);
|
||||
|
||||
// (行组件, 桌面 Wrap 内的固定宽);窄屏转单列——行占满整宽,不再 200px 截断
|
||||
final entries = <(Widget, double)>[
|
||||
(row('编号', p.code.isEmpty ? '-' : p.code), 200),
|
||||
(row('系列', p.series?.isEmpty ?? true ? '-' : p.series!), 200),
|
||||
(row('规格', p.spec?.isEmpty ?? true ? '-' : p.spec!), 200),
|
||||
(row('品牌', p.brand?.isEmpty ?? true ? '-' : p.brand!), 200),
|
||||
(row('单位', p.unit.isEmpty ? '-' : p.unit), 200),
|
||||
if (p.categoryName?.isNotEmpty ?? false) (row('分类', p.categoryName!), 200),
|
||||
(
|
||||
row(
|
||||
'进价',
|
||||
p.purchasePrice != null && p.purchasePrice! > 0
|
||||
? '¥${p.purchasePrice!.toStringAsFixed(2)}'
|
||||
: '-'),
|
||||
200
|
||||
),
|
||||
if (p.minStock != null && p.minStock! > 0)
|
||||
(row('库存预警', '${p.minStock}'), 200),
|
||||
if (p.originName?.isNotEmpty ?? false) (row('产地', p.originName!), 200),
|
||||
if (p.shelfLifeName?.isNotEmpty ?? false)
|
||||
(row('保质期', p.shelfLifeName!), 200),
|
||||
if (p.storageName?.isNotEmpty ?? false)
|
||||
(row('储存方式', p.storageName!), 200),
|
||||
if (p.barcode?.isNotEmpty ?? false) (row('条码', p.barcode!), 200),
|
||||
if (p.remark?.isNotEmpty ?? false) (row('备注', p.remark!), 400),
|
||||
..._buildCustomFieldRows(p, row),
|
||||
];
|
||||
|
||||
return Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
@@ -638,58 +669,29 @@ class _ProductDetailScreenState extends ConsumerState<ProductDetailScreen> {
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Wrap(
|
||||
spacing: 32,
|
||||
runSpacing: 0,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 200, child: row('编号', p.code.isEmpty ? '-' : p.code)),
|
||||
SizedBox(
|
||||
width: 200,
|
||||
child: row('系列', p.series?.isEmpty ?? true ? '-' : p.series!)),
|
||||
SizedBox(
|
||||
width: 200,
|
||||
child: row('规格', p.spec?.isEmpty ?? true ? '-' : p.spec!)),
|
||||
SizedBox(
|
||||
width: 200,
|
||||
child: row('品牌', p.brand?.isEmpty ?? true ? '-' : p.brand!)),
|
||||
SizedBox(
|
||||
width: 200, child: row('单位', p.unit.isEmpty ? '-' : p.unit)),
|
||||
if (p.categoryName?.isNotEmpty ?? false)
|
||||
SizedBox(width: 200, child: row('分类', p.categoryName!)),
|
||||
SizedBox(
|
||||
width: 200,
|
||||
child: row(
|
||||
'进价',
|
||||
p.purchasePrice != null && p.purchasePrice! > 0
|
||||
? '¥${p.purchasePrice!.toStringAsFixed(2)}'
|
||||
: '-')),
|
||||
if (p.minStock != null && p.minStock! > 0)
|
||||
SizedBox(width: 200, child: row('库存预警', '${p.minStock}')),
|
||||
if (p.originName?.isNotEmpty ?? false)
|
||||
SizedBox(width: 200, child: row('产地', p.originName!)),
|
||||
if (p.shelfLifeName?.isNotEmpty ?? false)
|
||||
SizedBox(width: 200, child: row('保质期', p.shelfLifeName!)),
|
||||
if (p.storageName?.isNotEmpty ?? false)
|
||||
SizedBox(width: 200, child: row('储存方式', p.storageName!)),
|
||||
if (p.barcode?.isNotEmpty ?? false)
|
||||
SizedBox(width: 200, child: row('条码', p.barcode!)),
|
||||
if (p.remark?.isNotEmpty ?? false)
|
||||
SizedBox(width: 400, child: row('备注', p.remark!)),
|
||||
...?_buildCustomFieldRows(p, row),
|
||||
],
|
||||
),
|
||||
child: context.isMobile
|
||||
? Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [for (final e in entries) e.$1],
|
||||
)
|
||||
: Wrap(
|
||||
spacing: 32,
|
||||
runSpacing: 0,
|
||||
children: [
|
||||
for (final e in entries) SizedBox(width: e.$2, child: e.$1),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget>? _buildCustomFieldRows(
|
||||
List<(Widget, double)> _buildCustomFieldRows(
|
||||
Product p, Widget Function(String, String) row) {
|
||||
final fields = p.customFields;
|
||||
if (fields == null || fields.isEmpty) return null;
|
||||
if (fields == null || fields.isEmpty) return const [];
|
||||
return fields.entries
|
||||
.where((e) => e.value != null && e.value.toString().isNotEmpty)
|
||||
.map((e) => SizedBox(width: 200, child: row(e.key, e.value.toString())))
|
||||
.map<(Widget, double)>((e) => (row(e.key, e.value.toString()), 200))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@@ -999,7 +1001,7 @@ class _QRCodeDialogState extends ConsumerState<_QRCodeDialog> {
|
||||
return AlertDialog(
|
||||
title: const Text('商品二维码'),
|
||||
content: SizedBox(
|
||||
width: 220,
|
||||
width: context.dialogWidth(220),
|
||||
height: 220,
|
||||
child: FutureBuilder<Uint8List>(
|
||||
future: _future,
|
||||
|
||||
@@ -24,6 +24,8 @@ import '../../providers/shop_provider.dart';
|
||||
import '../../providers/warehouse_provider.dart';
|
||||
import '../../widgets/ds/ds_atoms.dart';
|
||||
import '../../widgets/ds/ds_table.dart';
|
||||
import '../../widgets/ds/m_search_row.dart';
|
||||
import '../../widgets/ds/m_sheet.dart';
|
||||
import '../../widgets/mobile_list_card.dart';
|
||||
import '../../widgets/product_editor_drawer.dart';
|
||||
import '../../widgets/write_guard.dart';
|
||||
@@ -117,37 +119,37 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
|
||||
final t = context.tokens;
|
||||
final mobile = context.isMobile;
|
||||
final addAction = _addAction;
|
||||
// 原型 .main{padding:22px 26px};窄屏保持紧凑
|
||||
// 原型 .main{padding:22px 26px};窄屏对齐 m-products .m-scroll{padding:14px}
|
||||
return Container(
|
||||
color: t.bg,
|
||||
padding:
|
||||
mobile ? EdgeInsets.zero : const EdgeInsets.fromLTRB(26, 22, 26, 22),
|
||||
padding: mobile
|
||||
? const EdgeInsets.fromLTRB(AppDims.sp3, AppDims.sp3, AppDims.sp3, 0)
|
||||
: const EdgeInsets.fromLTRB(26, 22, 26, 22),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// ── 头部(原型 .head{margin-bottom:18px})──
|
||||
Container(
|
||||
color: t.bg,
|
||||
padding: mobile
|
||||
? const EdgeInsets.fromLTRB(
|
||||
AppDims.sp4, AppDims.sp4, AppDims.sp4, AppDims.sp2)
|
||||
: const EdgeInsets.only(bottom: 18),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text('基础数据',
|
||||
style: TextStyle(
|
||||
fontSize: AppDims.fsH1,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: t.heading)),
|
||||
const SizedBox(width: AppDims.sp3),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 2),
|
||||
child: Text(_subText(),
|
||||
style: TextStyle(fontSize: AppDims.fsSm, color: t.muted)),
|
||||
),
|
||||
const Spacer(),
|
||||
if (!mobile) ...[
|
||||
// ── 头部(原型 .head{margin-bottom:18px});窄屏隐藏——
|
||||
// 标题由壳顶栏 .m-top data-title 提供(对齐 m-products)──
|
||||
if (!mobile)
|
||||
Container(
|
||||
color: t.bg,
|
||||
padding: const EdgeInsets.only(bottom: 18),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text('基础数据',
|
||||
style: TextStyle(
|
||||
fontSize: AppDims.fsH1,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: t.heading)),
|
||||
const SizedBox(width: AppDims.sp3),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 2),
|
||||
child: Text(_subText(),
|
||||
style:
|
||||
TextStyle(fontSize: AppDims.fsSm, color: t.muted)),
|
||||
),
|
||||
const Spacer(),
|
||||
DsButton('刷新',
|
||||
icon: LucideIcons.refreshCw, onPressed: _reload),
|
||||
if (addAction != null) ...[
|
||||
@@ -160,13 +162,12 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
|
||||
),
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// ── .seg(原型 margin-bottom:14px)──
|
||||
// ── .seg(原型 margin-bottom:14px;窄屏横向可滚)──
|
||||
Padding(
|
||||
padding: mobile
|
||||
? const EdgeInsets.fromLTRB(AppDims.sp4, 0, AppDims.sp4, 12)
|
||||
? const EdgeInsets.only(bottom: AppDims.sp3)
|
||||
: const EdgeInsets.only(bottom: 14),
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
@@ -245,39 +246,38 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
|
||||
bool mobileAdd = false,
|
||||
}) {
|
||||
final mobile = context.isMobile;
|
||||
if (mobile) {
|
||||
// 窄屏对齐 m-products:.m-search 搜索行(自带清空 ×,重置并入)+ 新增
|
||||
final addAction = _addAction;
|
||||
return Row(children: [
|
||||
Expanded(
|
||||
child: MSearchRow(
|
||||
controller: ctrl,
|
||||
hint: hint,
|
||||
onChanged: (_) => onChanged(),
|
||||
),
|
||||
),
|
||||
if (mobileAdd && addAction != null) ...[
|
||||
const SizedBox(width: AppDims.sp2),
|
||||
WriteGuard(
|
||||
child: DsButton(_addLabel,
|
||||
icon: LucideIcons.plus,
|
||||
variant: DsBtnVariant.primary,
|
||||
onPressed: addAction),
|
||||
),
|
||||
],
|
||||
]);
|
||||
}
|
||||
final search = DsSearchBox(
|
||||
controller: ctrl,
|
||||
hint: hint,
|
||||
width: mobile ? null : 260,
|
||||
width: 260,
|
||||
onChanged: (_) => onChanged(),
|
||||
);
|
||||
final reset = DsButton('重置', small: true, onPressed: () {
|
||||
ctrl.clear();
|
||||
onChanged();
|
||||
});
|
||||
if (mobile) {
|
||||
final addAction = _addAction;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
search,
|
||||
const SizedBox(height: AppDims.sp2),
|
||||
Row(children: [
|
||||
reset,
|
||||
const Spacer(),
|
||||
if (mobileAdd && addAction != null)
|
||||
WriteGuard(
|
||||
child: DsButton(_addLabel,
|
||||
small: true,
|
||||
icon: LucideIcons.plus,
|
||||
variant: DsBtnVariant.primary,
|
||||
onPressed: addAction),
|
||||
),
|
||||
]),
|
||||
],
|
||||
);
|
||||
}
|
||||
return Row(children: [
|
||||
search,
|
||||
const Spacer(),
|
||||
@@ -375,7 +375,13 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
|
||||
onTap: () => showProductEditorDrawer(context,
|
||||
productId: p.id, cost: p.purchasePrice),
|
||||
title: Text(p.name),
|
||||
subtitle: p.code.isEmpty ? null : Text(p.code),
|
||||
// 原型 m-products mc-sub:mono 编码
|
||||
subtitle: p.code.isEmpty
|
||||
? null
|
||||
: Text(p.code,
|
||||
style: const TextStyle(
|
||||
fontFamily: AppFonts.mono,
|
||||
fontFamilyFallback: AppFonts.monoFallback)),
|
||||
fields: [
|
||||
if ((p.series ?? '').isNotEmpty)
|
||||
MobileCardField('系列', p.series),
|
||||
@@ -692,7 +698,13 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
|
||||
mobileCards: filtered
|
||||
.map((r) => MobileListCard(
|
||||
title: Text(r.name),
|
||||
subtitle: (r.code ?? '').isEmpty ? null : Text(r.code!),
|
||||
// 原型 m-products mc-sub:mono 编码
|
||||
subtitle: (r.code ?? '').isEmpty
|
||||
? null
|
||||
: Text(r.code!,
|
||||
style: const TextStyle(
|
||||
fontFamily: AppFonts.mono,
|
||||
fontFamilyFallback: AppFonts.monoFallback)),
|
||||
trailing: r.isDefault
|
||||
? const DsBadge('默认', tone: DsBadgeTone.info)
|
||||
: null,
|
||||
@@ -898,6 +910,91 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
|
||||
TextEditingController(text: initial?['remark'] as String? ?? '');
|
||||
final formKey = GlobalKey<FormState>();
|
||||
|
||||
List<Widget> fields() => [
|
||||
DsField('名称',
|
||||
required: true,
|
||||
input: TextFormField(
|
||||
controller: nameCtrl,
|
||||
decoration: const InputDecoration(hintText: '名称'),
|
||||
validator: (v) =>
|
||||
(v == null || v.trim().isEmpty) ? '请输入名称' : null,
|
||||
)),
|
||||
if (hasQuantity) ...[
|
||||
const SizedBox(height: 12),
|
||||
DsField('装箱数量',
|
||||
input: TextFormField(
|
||||
controller: quantityCtrl,
|
||||
decoration: const InputDecoration(hintText: '如 6'),
|
||||
keyboardType: TextInputType.number,
|
||||
)),
|
||||
],
|
||||
const SizedBox(height: 12),
|
||||
DsField('编号(可选)',
|
||||
input: TextFormField(
|
||||
controller: codeCtrl,
|
||||
decoration: const InputDecoration(hintText: '编号'),
|
||||
)),
|
||||
const SizedBox(height: 12),
|
||||
DsField('说明(选填)',
|
||||
input: TextFormField(
|
||||
controller: remarkCtrl,
|
||||
decoration:
|
||||
const InputDecoration(hintText: '备注说明,如适用品牌 / 用途'),
|
||||
maxLines: 2,
|
||||
)),
|
||||
];
|
||||
|
||||
Future<void> submit(BuildContext ctx) async {
|
||||
if (!formKey.currentState!.validate()) return;
|
||||
final data = <String, dynamic>{
|
||||
'name': nameCtrl.text.trim(),
|
||||
if (codeCtrl.text.isNotEmpty) 'code': codeCtrl.text.trim(),
|
||||
if (hasQuantity && quantityCtrl.text.isNotEmpty)
|
||||
'quantity': int.tryParse(quantityCtrl.text) ?? 0,
|
||||
if (remarkCtrl.text.isNotEmpty) 'remark': remarkCtrl.text.trim(),
|
||||
};
|
||||
Navigator.of(ctx).pop();
|
||||
try {
|
||||
await onSave(data);
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
showDsToast(context, '保存失败:$e', bg: context.tokens.danger);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 窄屏:底部 sheet 轻表单(原型 m-products openDictEdit:m-form + 按钮行)
|
||||
if (context.isMobile) {
|
||||
await showMSheet<void>(
|
||||
context,
|
||||
title: title,
|
||||
builder: (sctx) => Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
...fields(),
|
||||
const SizedBox(height: AppDims.sp4),
|
||||
Row(children: [
|
||||
Expanded(
|
||||
child: DsButton('取消',
|
||||
onPressed: () => Navigator.of(sctx).pop()),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: DsButton('保存',
|
||||
variant: DsBtnVariant.primary,
|
||||
onPressed: () => submit(sctx)),
|
||||
),
|
||||
]),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await showAppDialog(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
@@ -908,62 +1005,14 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
|
||||
key: formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
DsField('名称',
|
||||
required: true,
|
||||
input: TextFormField(
|
||||
controller: nameCtrl,
|
||||
decoration: const InputDecoration(hintText: '名称'),
|
||||
validator: (v) =>
|
||||
(v == null || v.trim().isEmpty) ? '请输入名称' : null,
|
||||
)),
|
||||
if (hasQuantity) ...[
|
||||
const SizedBox(height: 12),
|
||||
DsField('装箱数量',
|
||||
input: TextFormField(
|
||||
controller: quantityCtrl,
|
||||
decoration: const InputDecoration(hintText: '如 6'),
|
||||
keyboardType: TextInputType.number,
|
||||
)),
|
||||
],
|
||||
const SizedBox(height: 12),
|
||||
DsField('编号(可选)',
|
||||
input: TextFormField(
|
||||
controller: codeCtrl,
|
||||
decoration: const InputDecoration(hintText: '编号'),
|
||||
)),
|
||||
const SizedBox(height: 12),
|
||||
DsField('说明(选填)',
|
||||
input: TextFormField(
|
||||
controller: remarkCtrl,
|
||||
decoration:
|
||||
const InputDecoration(hintText: '备注说明,如适用品牌 / 用途'),
|
||||
maxLines: 2,
|
||||
)),
|
||||
],
|
||||
children: fields(),
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
DsButton('取消', onPressed: () => Navigator.of(ctx).pop()),
|
||||
DsButton('保存', variant: DsBtnVariant.primary, onPressed: () async {
|
||||
if (!formKey.currentState!.validate()) return;
|
||||
final data = <String, dynamic>{
|
||||
'name': nameCtrl.text.trim(),
|
||||
if (codeCtrl.text.isNotEmpty) 'code': codeCtrl.text.trim(),
|
||||
if (hasQuantity && quantityCtrl.text.isNotEmpty)
|
||||
'quantity': int.tryParse(quantityCtrl.text) ?? 0,
|
||||
if (remarkCtrl.text.isNotEmpty) 'remark': remarkCtrl.text.trim(),
|
||||
};
|
||||
Navigator.of(ctx).pop();
|
||||
try {
|
||||
await onSave(data);
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
showDsToast(context, '保存失败:$e', bg: context.tokens.danger);
|
||||
}
|
||||
}
|
||||
}),
|
||||
DsButton('保存',
|
||||
variant: DsBtnVariant.primary, onPressed: () => submit(ctx)),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -1048,6 +1097,19 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
|
||||
}
|
||||
|
||||
void _showWarehouseDialog({Warehouse? warehouse}) {
|
||||
// 窄屏:底部 sheet 轻表单(与系列/规格字典同形态)
|
||||
if (context.isMobile) {
|
||||
showMSheet<void>(
|
||||
context,
|
||||
title: warehouse != null ? '编辑仓库' : '新增仓库',
|
||||
builder: (_) => _WarehouseFormDialog(
|
||||
warehouse: warehouse,
|
||||
sheet: true,
|
||||
onSaved: () => ref.read(warehouseListProvider.notifier).reload(),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
showAppDialog(
|
||||
context: context,
|
||||
builder: (ctx) => _WarehouseFormDialog(
|
||||
@@ -1084,7 +1146,11 @@ class _WarehouseFormDialog extends ConsumerStatefulWidget {
|
||||
final Warehouse? warehouse;
|
||||
final VoidCallback onSaved;
|
||||
|
||||
const _WarehouseFormDialog({this.warehouse, required this.onSaved});
|
||||
/// true → 作为移动底部 sheet 的内容体渲染(无 AlertDialog 壳,按钮行内置)。
|
||||
final bool sheet;
|
||||
|
||||
const _WarehouseFormDialog(
|
||||
{this.warehouse, required this.onSaved, this.sheet = false});
|
||||
|
||||
@override
|
||||
ConsumerState<_WarehouseFormDialog> createState() =>
|
||||
@@ -1145,8 +1211,57 @@ class _WarehouseFormDialogState extends ConsumerState<_WarehouseFormDialog> {
|
||||
}
|
||||
}
|
||||
|
||||
List<Widget> _fields() => [
|
||||
DsField('仓库名称',
|
||||
required: true,
|
||||
input: TextFormField(
|
||||
controller: _nameCtrl,
|
||||
decoration: const InputDecoration(hintText: '如:主仓'),
|
||||
validator: (v) => (v == null || v.isEmpty) ? '不能为空' : null,
|
||||
)),
|
||||
const SizedBox(height: 12),
|
||||
DsField('位置',
|
||||
input: TextFormField(
|
||||
controller: _locationCtrl,
|
||||
decoration: const InputDecoration(hintText: '如:一层西侧'),
|
||||
)),
|
||||
const SizedBox(height: 12),
|
||||
CheckboxListTile(
|
||||
title: const Text('设为默认仓库'),
|
||||
value: _isDefault,
|
||||
onChanged: (v) => setState(() => _isDefault = v ?? false),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 窄屏 sheet 内容体(原型 m-form + 底部按钮行)
|
||||
if (widget.sheet) {
|
||||
return Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
..._fields(),
|
||||
const SizedBox(height: AppDims.sp4),
|
||||
Row(children: [
|
||||
Expanded(
|
||||
child:
|
||||
DsButton('取消', onPressed: () => Navigator.of(context).pop()),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: DsButton(_saving ? '保存中…' : '保存',
|
||||
variant: DsBtnVariant.primary,
|
||||
onPressed: _saving ? null : _save),
|
||||
),
|
||||
]),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
return AlertDialog(
|
||||
title: Text(widget.warehouse != null ? '编辑仓库' : '新增仓库'),
|
||||
content: SizedBox(
|
||||
@@ -1155,28 +1270,7 @@ class _WarehouseFormDialogState extends ConsumerState<_WarehouseFormDialog> {
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
DsField('仓库名称',
|
||||
required: true,
|
||||
input: TextFormField(
|
||||
controller: _nameCtrl,
|
||||
decoration: const InputDecoration(hintText: '如:主仓'),
|
||||
validator: (v) => (v == null || v.isEmpty) ? '不能为空' : null,
|
||||
)),
|
||||
const SizedBox(height: 12),
|
||||
DsField('位置',
|
||||
input: TextFormField(
|
||||
controller: _locationCtrl,
|
||||
decoration: const InputDecoration(hintText: '如:一层西侧'),
|
||||
)),
|
||||
const SizedBox(height: 12),
|
||||
CheckboxListTile(
|
||||
title: const Text('设为默认仓库'),
|
||||
value: _isDefault,
|
||||
onChanged: (v) => setState(() => _isDefault = v ?? false),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
],
|
||||
children: _fields(),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
|
After Width: | Height: | Size: 119 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 171 KiB |
|
Before Width: | Height: | Size: 161 KiB After Width: | Height: | Size: 173 KiB |
|
Before Width: | Height: | Size: 163 KiB After Width: | Height: | Size: 174 KiB |
|
Before Width: | Height: | Size: 145 KiB After Width: | Height: | Size: 154 KiB |
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 147 KiB After Width: | Height: | Size: 156 KiB |
@@ -0,0 +1,109 @@
|
||||
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';
|
||||
|
||||
/// 往来单位窄屏 golden × 三主题(390×844 @2x,对齐移动原型 m-partners.html):
|
||||
/// 页内大标题隐藏(标题在壳顶栏)+ MSearchRow 搜索行 + 类型 pills
|
||||
/// (全部/供应商/客户/两者)+ 卡片流(类型/状态图标徽章、mono 编码副行)。
|
||||
/// 更新基准:flutter test --update-goldens test/golden/partners_mobile_golden_test.dart
|
||||
|
||||
// 行数据对齐 m-partners.html 的 PARTNERS(供应商/客户/两者、启用/停用齐备)。
|
||||
const _partners = [
|
||||
Partner(
|
||||
id: 1,
|
||||
code: 'GYS001',
|
||||
name: '鼎晟供应链',
|
||||
type: 'supplier',
|
||||
contact: '张伟',
|
||||
phone: '138-0011-2233'),
|
||||
Partner(
|
||||
id: 2,
|
||||
code: 'KH002',
|
||||
name: '城东烟酒行',
|
||||
type: 'customer',
|
||||
contact: '李建国',
|
||||
phone: '139-2200-7788'),
|
||||
Partner(
|
||||
id: 3,
|
||||
code: 'GYS003',
|
||||
name: '贵州茅台经销',
|
||||
type: 'supplier',
|
||||
contact: '王芳',
|
||||
phone: '137-6655-4321'),
|
||||
Partner(
|
||||
id: 4,
|
||||
code: 'KH004',
|
||||
name: '金樽会所',
|
||||
type: 'customer',
|
||||
contact: '陈静',
|
||||
phone: '136-2211-9900'),
|
||||
Partner(
|
||||
id: 5,
|
||||
code: 'KH005',
|
||||
name: '喜宴婚庆',
|
||||
type: 'customer',
|
||||
contact: '周丽',
|
||||
phone: '139-3344-5566',
|
||||
status: 'disabled'),
|
||||
Partner(
|
||||
id: 6,
|
||||
code: 'GYS006',
|
||||
name: '川酒集团批发',
|
||||
type: 'supplier,customer',
|
||||
contact: '赵明',
|
||||
phone: '138-7766-5544'),
|
||||
];
|
||||
|
||||
// 原型 bal(应收/应付)
|
||||
const _balances = <int, ({double recv, double pay})>{
|
||||
1: (recv: 0, pay: 52800),
|
||||
2: (recv: 86200, pay: 0),
|
||||
3: (recv: 0, pay: 0),
|
||||
4: (recv: 41000, pay: 0),
|
||||
5: (recv: 0, pay: 0),
|
||||
6: (recv: 0, pay: 0),
|
||||
};
|
||||
|
||||
class _FakePartnersNotifier extends PartnerListNotifier {
|
||||
@override
|
||||
Future<PageResult<Partner>> build() async =>
|
||||
const PageResult(data: _partners, total: 6, page: 1, pageSize: 1000);
|
||||
@override
|
||||
void setPage(int page) {}
|
||||
@override
|
||||
void setPageSize(int pageSize) {}
|
||||
@override
|
||||
void setKeyword(String keyword) {}
|
||||
@override
|
||||
void setType(String? t) {}
|
||||
@override
|
||||
void reload() {}
|
||||
}
|
||||
|
||||
List<Override> _overrides() => [
|
||||
partnersScreenProvider.overrideWith(() => _FakePartnersNotifier()),
|
||||
partnerBalancesProvider.overrideWith((ref) async => _balances),
|
||||
isReadonlyProvider.overrideWithValue(false),
|
||||
];
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
|
||||
goldenAcrossThemes(
|
||||
'partners 窄屏(手机视口)',
|
||||
goldenPrefix: 'm_partners',
|
||||
child: () => const Scaffold(body: PartnersScreen()),
|
||||
overrides: _overrides,
|
||||
logical: const Size(390, 844),
|
||||
dpr: 2,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
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/product.dart';
|
||||
import 'package:jiu_client/models/product_option.dart';
|
||||
import 'package:jiu_client/models/shop.dart';
|
||||
import 'package:jiu_client/providers/product_option_provider.dart';
|
||||
import 'package:jiu_client/providers/product_provider.dart';
|
||||
import 'package:jiu_client/providers/shop_provider.dart';
|
||||
import 'package:jiu_client/screens/products/products_screen.dart';
|
||||
|
||||
import '../support/golden_harness.dart';
|
||||
|
||||
/// 基础数据窄屏 golden × 三主题(390×844 @2x,对齐移动原型 m-products.html):
|
||||
/// 页内大标题隐藏 + 分段 tab 横滚(商品档案/介绍/系列/规格/仓库)+
|
||||
/// MSearchRow 搜索行 + 卡片流(mono 编码副行)。两组:商品档案 tab / 系列字典 tab。
|
||||
/// 更新基准:flutter test --update-goldens test/golden/products_mobile_golden_test.dart
|
||||
|
||||
// 行数据照抄 m-products.html 的 PRODUCTS(前 6 行足够填满手机视口)。
|
||||
const _products = [
|
||||
Product(
|
||||
id: 1,
|
||||
code: 'MT-FT-500',
|
||||
name: '茅台 飞天',
|
||||
unit: '件',
|
||||
series: '飞天',
|
||||
spec: '500ml×6 · 53°',
|
||||
purchasePrice: 2680),
|
||||
Product(
|
||||
id: 2,
|
||||
code: 'MT-FT-100',
|
||||
name: '茅台 飞天',
|
||||
unit: '件',
|
||||
series: '飞天',
|
||||
spec: '100ml×24 · 53°',
|
||||
purchasePrice: 620),
|
||||
Product(
|
||||
id: 3,
|
||||
code: 'WLY-PW-500',
|
||||
name: '五粮液 普五',
|
||||
unit: '件',
|
||||
series: '第八代',
|
||||
spec: '500ml×6 · 52°',
|
||||
purchasePrice: 1050),
|
||||
Product(
|
||||
id: 4,
|
||||
code: 'GJ-1573-500',
|
||||
name: '国窖 1573',
|
||||
unit: '件',
|
||||
series: '经典装',
|
||||
spec: '500ml×6 · 52°',
|
||||
purchasePrice: 980),
|
||||
Product(
|
||||
id: 5,
|
||||
code: 'JNC-SJ-500',
|
||||
name: '剑南春 水晶剑',
|
||||
unit: '件',
|
||||
series: '水晶剑',
|
||||
spec: '500ml×6 · 52°',
|
||||
purchasePrice: 438),
|
||||
Product(
|
||||
id: 6,
|
||||
code: 'YH-M6-500',
|
||||
name: '洋河 梦之蓝',
|
||||
unit: '件',
|
||||
series: 'M6+',
|
||||
spec: '550ml×6 · 45°',
|
||||
purchasePrice: 620),
|
||||
];
|
||||
|
||||
// 系列字典(m-products DICTS.series)。
|
||||
const _series = [
|
||||
ProductSeriesOption(id: 1, code: 'XL001', name: '飞天', remark: '茅台主力系列'),
|
||||
ProductSeriesOption(id: 2, code: 'XL002', name: '第八代', remark: '五粮液当前主力'),
|
||||
ProductSeriesOption(id: 3, code: 'XL003', name: '经典装', remark: '国窖 1573 经典'),
|
||||
ProductSeriesOption(id: 4, code: 'XL004', name: '水晶剑', remark: '剑南春核心'),
|
||||
ProductSeriesOption(id: 5, code: 'XL005', name: 'M6+', remark: '梦之蓝高端'),
|
||||
ProductSeriesOption(id: 6, code: 'XL006', name: '青花20', remark: '汾酒青花系列'),
|
||||
];
|
||||
|
||||
const _shop = ShopInfo(
|
||||
id: 1,
|
||||
code: 'DSJH-001',
|
||||
name: '鼎晟酒行',
|
||||
address: '浙江省杭州市',
|
||||
phone: '0571-88886666',
|
||||
managerName: '王经理',
|
||||
customFields: {'default_series_id': 1});
|
||||
|
||||
class _FakeProductList extends ProductListNotifier {
|
||||
@override
|
||||
Future<PageResult<Product>> build() async =>
|
||||
const PageResult(data: _products, total: 6, page: 1, pageSize: 20);
|
||||
@override
|
||||
void setPage(int page) {}
|
||||
@override
|
||||
void setPageSize(int pageSize) {}
|
||||
@override
|
||||
void setKeyword(String keyword) {}
|
||||
@override
|
||||
void setCategoryId(int? categoryId) {}
|
||||
@override
|
||||
void reload() {}
|
||||
}
|
||||
|
||||
class _FakeSeriesList extends ProductSeriesListNotifier {
|
||||
@override
|
||||
Future<List<ProductSeriesOption>> build() async => _series;
|
||||
@override
|
||||
void reload() {}
|
||||
}
|
||||
|
||||
List<Override> _overrides() => [
|
||||
productListProvider.overrideWith(() => _FakeProductList()),
|
||||
productSeriesListProvider.overrideWith(() => _FakeSeriesList()),
|
||||
shopInfoProvider.overrideWith((ref) => _shop),
|
||||
isReadonlyProvider.overrideWithValue(false),
|
||||
];
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
|
||||
// 商品档案 tab:搜索行 + 卡片流(mono 编码副行)
|
||||
goldenAcrossThemes(
|
||||
'products 商品档案 窄屏(手机视口)',
|
||||
goldenPrefix: 'm_products',
|
||||
child: () => const Scaffold(body: ProductsScreen()),
|
||||
overrides: _overrides,
|
||||
logical: const Size(390, 844),
|
||||
dpr: 2,
|
||||
);
|
||||
|
||||
// 系列字典 tab:搜索行 + 新增按钮 + 字典卡片流(默认徽章)
|
||||
goldenAcrossThemes(
|
||||
'products 系列字典 窄屏(手机视口)',
|
||||
goldenPrefix: 'm_products_series',
|
||||
child: () => const Scaffold(body: ProductsScreen(initialTab: 2)),
|
||||
overrides: _overrides,
|
||||
logical: const Size(390, 844),
|
||||
dpr: 2,
|
||||
);
|
||||
}
|
||||