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>
This commit is contained in:
wangjia
2026-07-04 12:13:38 +08:00
parent be9afcf68d
commit 4287f5a876
20 changed files with 665 additions and 260 deletions
+115 -61
View File
@@ -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 的既有对应物 DsChipselected 态)
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-partnersbadge b-供应商/客户/两者 + BADGE_ICON)。
/// 色调与 partnerTypeBadgepartner_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-partnersmc-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),