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
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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user