chore: release v1.0.27
Deploy / build-windows (push) Successful in 1m56s
Deploy / build-android (push) Has been cancelled
Deploy / build-ios (push) Has been cancelled
Deploy / release-deploy (push) Has been cancelled
Deploy / build-linux-web (push) Successful in 53s
Deploy / build-macos (push) Failing after 1m16s

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-09 00:15:46 +08:00
parent 65d6b12a21
commit daa83bd408
9 changed files with 1535 additions and 360 deletions
+2
View File
@@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart';
import 'dart:async';
import 'package:flutter/foundation.dart' show kIsWeb;
import '../../core/auth/auth_state.dart';
import '../../core/config/app_config.dart';
import '../../core/responsive/responsive.dart';
@@ -161,6 +162,7 @@ class _AppShellState extends ConsumerState<AppShell> {
child: Scaffold(
key: _scaffoldKey,
drawer: isMobile ? _buildDrawer(context, user, location) : null,
drawerEnableOpenDragGesture: !kIsWeb && isMobile,
body: Column(
children: [
// Top Bar
@@ -556,39 +556,12 @@ class _StockInFormScreenState extends ConsumerState<StockInFormScreen> {
)),
)
else
Table(
columnWidths: const {
0: FixedColumnWidth(36), // 序号
1: FlexColumnWidth(1.2), // 商品编码
2: FlexColumnWidth(2.0), // 名称
3: FlexColumnWidth(1.3), // 系列
4: FlexColumnWidth(1.3), // 规格
5: FlexColumnWidth(0.9), // 单品数量
6: FlexColumnWidth(1.0), // 数量
7: FlexColumnWidth(1.0), // 单价
8: FlexColumnWidth(1.0), // 金额
9: FlexColumnWidth(1.2), // 批次号
10: FlexColumnWidth(1.2), // 生产日期
11: FixedColumnWidth(60), // 操作
},
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TableRow(
decoration: const BoxDecoration(color: Color(0xFFF0F4FF)),
children: [
'序号', '商品编码', '名称', '系列', '规格', '单品数量', '数量', '单价', '金额', '批次号', '生产日期', '操作',
]
.map((h) => Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 10),
child: Text(h,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: AppTheme.primaryDark)),
))
.toList(),
),
...List.generate(_items.length, (i) => _buildItemRow(i)),
_buildItemTableHeader(),
const Divider(height: 1),
...List.generate(_items.length, _buildItemGroup),
],
),
const Divider(height: 1),
@@ -855,7 +828,36 @@ class _StockInFormScreenState extends ConsumerState<StockInFormScreen> {
?.code ??
'';
TableRow _buildItemRow(int index) {
// ── 桌面表格:表头行 ──────────────────────────────────────────────────────
Widget _buildItemTableHeader() {
Widget th(String s) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
child: Text(s,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: AppTheme.primaryDark)),
);
return Container(
color: const Color(0xFFF0F4FF),
child: Row(children: [
SizedBox(width: 36, child: th('序号')),
Expanded(flex: 12, child: th('商品编码')),
Expanded(flex: 20, child: th('名称')),
Expanded(flex: 13, child: th('系列')),
Expanded(flex: 13, child: th('规格')),
Expanded(flex: 9, child: th('单品数量')),
Expanded(flex: 10, child: th('数量')),
Expanded(flex: 10, child: th('单价')),
Expanded(flex: 10, child: th('金额')),
const SizedBox(width: 32),
const SizedBox(width: 44),
]),
);
}
// ── 桌面表格:每条明细(主行 + 可折叠选填区) ──────────────────────────
Widget _buildItemGroup(int index) {
final item = _items[index];
final qty = double.tryParse(item.qtyCtrl.text) ?? 0;
final price = double.tryParse(item.priceCtrl.text) ?? 0;
@@ -863,60 +865,153 @@ class _StockInFormScreenState extends ConsumerState<StockInFormScreen> {
final specQty = _specQtyOf(item);
final productCode = _productCodeOf(item);
return TableRow(
decoration: BoxDecoration(
color: index.isEven ? Colors.white : const Color(0xFFFAFAFA),
),
Widget tc(String s, {Color? color, FontWeight? weight}) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12),
child: Text(s,
style: TextStyle(
fontSize: 13,
color: color,
fontWeight: weight)),
);
final hasOptional = item.batchNoCtrl.text.isNotEmpty ||
item.productionDate != null ||
item.selectedOriginId != null ||
item.selectedShelfLifeId != null ||
item.selectedStorageId != null ||
item.selectedDescriptionDocId != null;
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12),
child: Text('${index + 1}',
style: const TextStyle(fontSize: 13, color: AppTheme.textSecondary)),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12),
child: Text(productCode,
style: const TextStyle(fontSize: 12, color: AppTheme.textSecondary)),
),
Padding(padding: const EdgeInsets.all(4), child: _nameField(item)),
Padding(padding: const EdgeInsets.all(4), child: _seriesField(item)),
Padding(padding: const EdgeInsets.all(4), child: _specField(item)),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12),
child: Text(
specQty > 0 ? '$specQty' : '-',
style: TextStyle(
fontSize: 13,
color: specQty > 0 ? Colors.black87 : AppTheme.textSecondary,
),
),
),
Padding(padding: const EdgeInsets.all(4), child: _qtyField(item)),
Padding(padding: const EdgeInsets.all(4), child: _priceField(item)),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12),
child: Text(
'¥${amount.toStringAsFixed(2)}',
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500),
),
),
Padding(padding: const EdgeInsets.all(4), child: _batchField(item)),
Padding(padding: const EdgeInsets.all(4), child: _dateField(item)),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
child: IconButton(
icon: const Icon(Icons.delete_outline, size: 18, color: AppTheme.danger),
onPressed: _items.length > 1 ? () => _removeItem(index) : null,
tooltip: '删除',
padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 28, minHeight: 28),
IntrinsicHeight(
child: Container(
color: index.isEven ? Colors.white : const Color(0xFFFAFAFA),
child: Row(crossAxisAlignment: CrossAxisAlignment.center, children: [
SizedBox(
width: 36,
child: tc('${index + 1}', color: AppTheme.textSecondary)),
Expanded(
flex: 12,
child: tc(productCode, color: AppTheme.textSecondary)),
Expanded(
flex: 20,
child: Padding(
padding: const EdgeInsets.all(4),
child: _nameField(item))),
Expanded(
flex: 13,
child: Padding(
padding: const EdgeInsets.all(4),
child: _seriesField(item))),
Expanded(
flex: 13,
child: Padding(
padding: const EdgeInsets.all(4),
child: _specField(item))),
Expanded(
flex: 9,
child: tc(specQty > 0 ? '$specQty' : '-',
color: specQty > 0
? Colors.black87
: AppTheme.textSecondary)),
Expanded(
flex: 10,
child: Padding(
padding: const EdgeInsets.all(4),
child: _qtyField(item))),
Expanded(
flex: 10,
child: Padding(
padding: const EdgeInsets.all(4),
child: _priceField(item))),
Expanded(
flex: 10,
child: tc('¥${amount.toStringAsFixed(2)}',
weight: FontWeight.w500)),
SizedBox(
width: 32,
child: Tooltip(
message: item.expanded ? '收起选填' : '展开选填(批次/产地/保质期等)',
child: IconButton(
icon: Icon(
item.expanded ? Icons.expand_less : Icons.expand_more,
size: 18,
color: hasOptional
? AppTheme.primary
: AppTheme.textSecondary,
),
onPressed: () =>
setState(() => item.expanded = !item.expanded),
padding: EdgeInsets.zero,
constraints:
const BoxConstraints(minWidth: 28, minHeight: 28),
),
),
),
SizedBox(
width: 44,
child: IconButton(
icon: const Icon(Icons.delete_outline,
size: 18, color: AppTheme.danger),
onPressed:
_items.length > 1 ? () => _removeItem(index) : null,
tooltip: '删除',
padding: EdgeInsets.zero,
constraints:
const BoxConstraints(minWidth: 28, minHeight: 28),
),
),
]),
),
),
if (item.expanded) _buildOptionalSection(item),
const Divider(height: 1),
],
);
}
/// 窄屏(手机):每个明细行渲染为一张卡片,字段竖排,避免表格横向溢出。
// ── 可折叠选填区(桌面) ──────────────────────────────────────────────────
Widget _buildOptionalSection(_ItemRow item) {
return Container(
decoration: const BoxDecoration(
color: Color(0xFFF0F6FF),
border: Border(left: BorderSide(color: AppTheme.primary, width: 3)),
),
padding: const EdgeInsets.fromLTRB(16, 10, 16, 14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('选填信息',
style: TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
fontWeight: FontWeight.w500)),
const SizedBox(height: 10),
Wrap(
spacing: 16,
runSpacing: 12,
children: [
_OptionalField(
label: '批次号', width: 180, child: _batchField(item)),
_OptionalField(
label: '生产日期', width: 160, child: _dateField(item)),
_OptionalField(
label: '产地', width: 180, child: _originField(item)),
_OptionalField(
label: '保质期', width: 180, child: _shelfLifeField(item)),
_OptionalField(
label: '储存方式', width: 180, child: _storageField(item)),
_OptionalField(
label: '介绍文档', width: 200, child: _descriptionDocField(item)),
],
),
],
),
);
}
/// 窄屏(手机):每个明细行渲染为一张卡片,必填字段竖排,选填字段默认折叠。
Widget _buildItemCard(int index) {
final item = _items[index];
final qty = double.tryParse(item.qtyCtrl.text) ?? 0;
@@ -942,12 +1037,30 @@ class _StockInFormScreenState extends ConsumerState<StockInFormScreen> {
MobileCardField('数量', null, valueWidget: _qtyField(item)),
MobileCardField('单价', null, valueWidget: _priceField(item)),
MobileCardField('金额', '¥${amount.toStringAsFixed(2)}'),
MobileCardField('批次号', null, valueWidget: _batchField(item)),
MobileCardField('生产日期', null, valueWidget: _dateField(item)),
MobileCardField('产地', null, valueWidget: _originField(item)),
MobileCardField('保质期', null, valueWidget: _shelfLifeField(item)),
MobileCardField('储存方式', null, valueWidget: _storageField(item)),
MobileCardField('介绍文档', null, valueWidget: _descriptionDocField(item)),
MobileCardField('', null,
valueWidget: TextButton.icon(
onPressed: () => setState(() => item.expanded = !item.expanded),
icon: Icon(item.expanded ? Icons.expand_less : Icons.expand_more,
size: 16),
label: Text(item.expanded ? '收起选填项' : '展开选填项(批次/产地/保质期…)'),
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
minimumSize: const Size(0, 32),
foregroundColor: AppTheme.textSecondary,
),
)),
if (item.expanded)
MobileCardField('批次号', null, valueWidget: _batchField(item)),
if (item.expanded)
MobileCardField('生产日期', null, valueWidget: _dateField(item)),
if (item.expanded)
MobileCardField('产地', null, valueWidget: _originField(item)),
if (item.expanded)
MobileCardField('保质期', null, valueWidget: _shelfLifeField(item)),
if (item.expanded)
MobileCardField('储存方式', null, valueWidget: _storageField(item)),
if (item.expanded)
MobileCardField('介绍文档', null, valueWidget: _descriptionDocField(item)),
],
);
}
@@ -1002,6 +1115,7 @@ class _ItemRow {
final TextEditingController batchNoCtrl = TextEditingController();
final TextEditingController productionDateCtrl = TextEditingController();
DateTime? productionDate;
bool expanded = false;
void dispose() {
qtyCtrl.dispose();
@@ -1011,6 +1125,34 @@ class _ItemRow {
}
}
/// 选填区内单个字段:label + 字段控件,固定宽度。
class _OptionalField extends StatelessWidget {
final String label;
final Widget child;
final double width;
const _OptionalField(
{required this.label, required this.child, this.width = 180});
@override
Widget build(BuildContext context) {
return SizedBox(
width: width,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(label,
style: const TextStyle(
fontSize: 12, color: AppTheme.textSecondary)),
const SizedBox(height: 4),
child,
],
),
);
}
}
class _FormField extends StatelessWidget {
final String label;
final Widget child;
@@ -788,7 +788,48 @@ class _StockInDetailDialogState extends ConsumerState<_StockInDetailDialog> {
if (o.items.isEmpty)
const Text('无商品明细',
style: TextStyle(color: AppTheme.textSecondary))
else if (context.isMobile)
// 窄屏:每个商品渲染为卡片,字段竖排
Column(
children: o.items.asMap().entries.map((e) {
final i = e.key;
final item = e.value;
return Padding(
padding: const EdgeInsets.only(bottom: 10),
child: MobileListCard(
title: Text('商品 ${i + 1} ${item.productName ?? ''}'),
subtitle: item.productCode != null
? Text('编码 ${item.productCode}')
: null,
fields: [
if (item.productSeries?.isNotEmpty ?? false)
MobileCardField('系列', item.productSeries!),
MobileCardField('规格', item.productSpec ?? '-'),
MobileCardField('数量', item.quantity.toStringAsFixed(3)),
MobileCardField(
'单价', '¥${item.unitPrice.toStringAsFixed(2)}'),
MobileCardField(
'金额', '¥${item.totalPrice.toStringAsFixed(2)}'),
if (item.batchNo?.isNotEmpty ?? false)
MobileCardField('批次号', item.batchNo!),
if (item.productionDate?.isNotEmpty ?? false)
MobileCardField('生产日期',
item.productionDate!.length >= 10
? item.productionDate!.substring(0, 10)
: item.productionDate!),
if (item.productOrigin?.isNotEmpty ?? false)
MobileCardField('产地', item.productOrigin!),
if (item.productShelfLife?.isNotEmpty ?? false)
MobileCardField('保质期', item.productShelfLife!),
if (item.productStorage?.isNotEmpty ?? false)
MobileCardField('储存方式', item.productStorage!),
],
),
);
}).toList(),
)
else
// 宽屏:保持原有表格展示
Table(
border: TableBorder.all(color: AppTheme.border, width: 0.5),
columnWidths: const {
@@ -806,27 +847,37 @@ class _StockInDetailDialogState extends ConsumerState<_StockInDetailDialog> {
decoration: const BoxDecoration(color: Color(0xFFF0F4FF)),
children: ['序号', '商品编码', '名称', '系列', '规格', '数量', '单价', '金额']
.map((h) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
child: Text(h, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: AppTheme.primaryDark)),
)).toList(),
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 10),
child: Text(h,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: AppTheme.primaryDark)),
))
.toList(),
),
...o.items.asMap().entries.map((e) {
final i = e.key;
final item = e.value;
// 产地/保质期/储存拼成一行辅助文本
final attrs = [
if (item.productOrigin?.isNotEmpty ?? false) item.productOrigin!,
if (item.productShelfLife?.isNotEmpty ?? false) item.productShelfLife!,
if (item.productStorage?.isNotEmpty ?? false) item.productStorage!,
if (item.productOrigin?.isNotEmpty ?? false)
item.productOrigin!,
if (item.productShelfLife?.isNotEmpty ?? false)
item.productShelfLife!,
if (item.productStorage?.isNotEmpty ?? false)
item.productStorage!,
].join(' · ');
return TableRow(
decoration: BoxDecoration(
color: i.isEven ? Colors.white : const Color(0xFFFAFAFA)),
color:
i.isEven ? Colors.white : const Color(0xFFFAFAFA)),
children: [
_TableCell('${i + 1}'),
_TableCell(item.productCode ?? '-'),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,