chore: release client-v1.0.71
Deploy Client / build-client-web (push) Successful in 47s
Deploy Client / build-macos (push) Successful in 2m11s
Deploy Client / build-android (push) Successful in 1m11s
Deploy Client / build-ios (push) Successful in 2m29s
Deploy Client / build-windows (push) Successful in 2m27s
Deploy Client / release-deploy-client (push) Successful in 1m27s
Deploy Client / build-client-web (push) Successful in 47s
Deploy Client / build-macos (push) Successful in 2m11s
Deploy Client / build-android (push) Successful in 1m11s
Deploy Client / build-ios (push) Successful in 2m29s
Deploy Client / build-windows (push) Successful in 2m27s
Deploy Client / release-deploy-client (push) Successful in 1m27s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YZ4DskSRKsSiheQonFtQvx
This commit is contained in:
@@ -0,0 +1,420 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../core/responsive/responsive.dart';
|
||||
import '../core/theme/app_theme.dart';
|
||||
import '../core/utils/dialog_util.dart';
|
||||
|
||||
/// 退单状态小徽章(none 返回 null 不显示)。partial=部分退单(amber),full=已退单(红)。
|
||||
Widget? returnStateBadge(String state) {
|
||||
Color? c;
|
||||
String? t;
|
||||
if (state == 'partial') {
|
||||
c = AppTheme.warning;
|
||||
t = '部分退单';
|
||||
} else if (state == 'full') {
|
||||
c = AppTheme.danger;
|
||||
t = '已退单';
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: c.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(8)),
|
||||
child: Text(t,
|
||||
style: TextStyle(fontSize: 10, color: c, fontWeight: FontWeight.w600)),
|
||||
);
|
||||
}
|
||||
|
||||
/// 退单弹窗的一条明细。
|
||||
class ReturnLine {
|
||||
final int itemId;
|
||||
final String code;
|
||||
final String name;
|
||||
final String series;
|
||||
final String spec;
|
||||
final double quantity;
|
||||
final double unitPrice;
|
||||
final double totalPrice;
|
||||
final bool alreadyReturned; // 之前已退过(不可再选)
|
||||
|
||||
const ReturnLine({
|
||||
required this.itemId,
|
||||
required this.code,
|
||||
required this.name,
|
||||
required this.series,
|
||||
required this.spec,
|
||||
required this.quantity,
|
||||
required this.unitPrice,
|
||||
required this.totalPrice,
|
||||
this.alreadyReturned = false,
|
||||
});
|
||||
}
|
||||
|
||||
/// 打开退单弹窗。提交成功返回 true(调用方据此刷新列表),取消返回 null。
|
||||
/// - [isOut] true=出库(退回库存)/ false=入库(从库存删除),影响文案。
|
||||
/// - [onSubmit] 收到选中的 itemId 列表,向后端提交;抛异常表示失败。
|
||||
Future<bool?> showOrderReturnDialog({
|
||||
required BuildContext context,
|
||||
required String title,
|
||||
required List<(String, String)> meta,
|
||||
required bool isOut,
|
||||
required List<ReturnLine> lines,
|
||||
required Future<void> Function(List<int> itemIds) onSubmit,
|
||||
}) {
|
||||
return showAppDialog<bool>(
|
||||
context: context,
|
||||
builder: (_) => _OrderReturnDialog(
|
||||
title: title,
|
||||
meta: meta,
|
||||
isOut: isOut,
|
||||
lines: lines,
|
||||
onSubmit: onSubmit,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _OrderReturnDialog extends StatefulWidget {
|
||||
final String title;
|
||||
final List<(String, String)> meta;
|
||||
final bool isOut;
|
||||
final List<ReturnLine> lines;
|
||||
final Future<void> Function(List<int> itemIds) onSubmit;
|
||||
|
||||
const _OrderReturnDialog({
|
||||
required this.title,
|
||||
required this.meta,
|
||||
required this.isOut,
|
||||
required this.lines,
|
||||
required this.onSubmit,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_OrderReturnDialog> createState() => _OrderReturnDialogState();
|
||||
}
|
||||
|
||||
class _OrderReturnDialogState extends State<_OrderReturnDialog> {
|
||||
final Set<int> _staged = {}; // 本次暂存要退的 itemId
|
||||
bool _submitting = false;
|
||||
|
||||
bool _isReturned(ReturnLine l) => l.alreadyReturned || _staged.contains(l.itemId);
|
||||
|
||||
List<ReturnLine> get _selectable =>
|
||||
widget.lines.where((l) => !l.alreadyReturned).toList();
|
||||
|
||||
Future<void> _confirmLine(ReturnLine l) async {
|
||||
final ok = await showAppDialog<bool>(
|
||||
context: context,
|
||||
builder: (_) => AlertDialog(
|
||||
title: const Text('退单确认', style: TextStyle(fontSize: 16)),
|
||||
content: RichText(
|
||||
text: TextSpan(
|
||||
style: const TextStyle(fontSize: 13, color: AppTheme.textPrimary, height: 1.6),
|
||||
children: [
|
||||
const TextSpan(text: '确认退掉以下明细?\n'),
|
||||
TextSpan(
|
||||
text: l.name,
|
||||
style: const TextStyle(fontWeight: FontWeight.w700)),
|
||||
TextSpan(text: ' · ${l.series} · ${l.spec}\n'),
|
||||
TextSpan(
|
||||
text: '编码 ${l.code} · 数量 ${_q(l.quantity)}',
|
||||
style: const TextStyle(color: AppTheme.textSecondary, fontSize: 12)),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text('取消')),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.danger, foregroundColor: Colors.white),
|
||||
child: const Text('退单'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (ok == true) setState(() => _staged.add(l.itemId));
|
||||
}
|
||||
|
||||
Future<void> _confirmAll() async {
|
||||
final pending = _selectable.where((l) => !_staged.contains(l.itemId)).toList();
|
||||
if (pending.isEmpty) return;
|
||||
final ok = await showAppDialog<bool>(
|
||||
context: context,
|
||||
builder: (_) => AlertDialog(
|
||||
title: const Text('全部退单确认', style: TextStyle(fontSize: 16)),
|
||||
content: Text('确认退掉本单全部 ${pending.length} 条未退明细?',
|
||||
style: const TextStyle(fontSize: 13)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text('取消')),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.danger, foregroundColor: Colors.white),
|
||||
child: const Text('全部退单'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (ok == true) {
|
||||
setState(() => _staged.addAll(pending.map((e) => e.itemId)));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _submit() async {
|
||||
if (_staged.isEmpty) return;
|
||||
final ok = await showAppDialog<bool>(
|
||||
context: context,
|
||||
builder: (_) => AlertDialog(
|
||||
title: const Text('提交退单', style: TextStyle(fontSize: 16)),
|
||||
content: Text(
|
||||
'确认提交退单?${widget.isOut ? '退回库存' : '从库存删除'} 将立即生效,且不可撤销。',
|
||||
style: const TextStyle(fontSize: 13)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text('取消')),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.danger, foregroundColor: Colors.white),
|
||||
child: const Text('提交退单'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (ok != true) return;
|
||||
setState(() => _submitting = true);
|
||||
try {
|
||||
await widget.onSubmit(_staged.toList());
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
||||
content: Text('退单成功'), backgroundColor: AppTheme.success));
|
||||
Navigator.pop(context, true);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() => _submitting = false);
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text('退单失败:$e'), backgroundColor: AppTheme.danger));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final stagedQty = widget.lines
|
||||
.where((l) => _staged.contains(l.itemId))
|
||||
.fold<double>(0, (s, l) => s + l.quantity);
|
||||
|
||||
return Dialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
child: SizedBox(
|
||||
width: context.dialogWidth(820),
|
||||
height: 560,
|
||||
child: Column(
|
||||
children: [
|
||||
// 头部
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 12, 10),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(widget.title,
|
||||
style: const TextStyle(
|
||||
fontSize: 15, fontWeight: FontWeight.w600),
|
||||
overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
OutlinedButton(
|
||||
onPressed: _submitting ? null : _confirmAll,
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppTheme.danger,
|
||||
side: const BorderSide(color: AppTheme.danger),
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
child: const Text('全部退单', style: TextStyle(fontSize: 12)),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, size: 20),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
// 单据信息
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Wrap(
|
||||
spacing: 20,
|
||||
runSpacing: 4,
|
||||
children: [
|
||||
for (final (k, v) in widget.meta)
|
||||
Text.rich(TextSpan(
|
||||
style: const TextStyle(fontSize: 12, color: AppTheme.textSecondary),
|
||||
children: [
|
||||
TextSpan(text: '$k:'),
|
||||
TextSpan(text: v, style: const TextStyle(color: AppTheme.textPrimary)),
|
||||
],
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 行为提示
|
||||
Container(
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.fromLTRB(16, 2, 16, 8),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF0F6FF),
|
||||
border: const Border(left: BorderSide(color: AppTheme.primary, width: 3)),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
widget.isOut
|
||||
? '提交后,已退单明细的数量将「加回库存」。退单的明细不会删除,以红色「已退单」标注。'
|
||||
: '提交后,已退单明细对应的库存将「从库存删除」。退单的明细不会删除,以红色「已退单」标注。',
|
||||
style: const TextStyle(fontSize: 12, color: AppTheme.primaryDark),
|
||||
),
|
||||
),
|
||||
// 明细列表
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
itemCount: widget.lines.length,
|
||||
separatorBuilder: (_, __) => const Divider(height: 1),
|
||||
itemBuilder: (_, i) => _lineTile(widget.lines[i], i),
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
// 底部
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 10, 16, 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
_staged.isEmpty
|
||||
? '未选择退单明细'
|
||||
: '已退单 ${_staged.length} 项 · ${widget.isOut ? '退回库存 +' : '从库存移除 −'}${_q(stagedQty)} 件',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: _staged.isEmpty
|
||||
? AppTheme.textSecondary
|
||||
: AppTheme.danger,
|
||||
fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
OutlinedButton(
|
||||
onPressed: _submitting ? null : () => Navigator.pop(context),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: (_staged.isEmpty || _submitting) ? null : _submit,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.danger,
|
||||
foregroundColor: Colors.white),
|
||||
child: _submitting
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2, color: Colors.white))
|
||||
: const Text('提交退单'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _lineTile(ReturnLine l, int idx) {
|
||||
final returned = _isReturned(l);
|
||||
final nameStyle = TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: returned ? AppTheme.danger : AppTheme.textPrimary,
|
||||
decoration: returned ? TextDecoration.lineThrough : null,
|
||||
);
|
||||
final subStyle = TextStyle(
|
||||
fontSize: 11,
|
||||
color: returned ? AppTheme.danger : AppTheme.textSecondary,
|
||||
decoration: returned ? TextDecoration.lineThrough : null,
|
||||
);
|
||||
return Container(
|
||||
color: returned ? const Color(0xFFFFF4F4) : null,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 9),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 24,
|
||||
child: Text('${idx + 1}',
|
||||
style: const TextStyle(
|
||||
fontSize: 12, color: AppTheme.textSecondary))),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(children: [
|
||||
Flexible(child: Text(l.name, style: nameStyle, overflow: TextOverflow.ellipsis)),
|
||||
if (returned) ...[
|
||||
const SizedBox(width: 6),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.danger,
|
||||
borderRadius: BorderRadius.circular(8)),
|
||||
child: const Text('已退单',
|
||||
style: TextStyle(fontSize: 10, color: Colors.white)),
|
||||
),
|
||||
],
|
||||
]),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'${l.code} · ${l.series} · ${l.spec} · ${_q(l.quantity)}×¥${l.unitPrice.toStringAsFixed(2)} = ¥${l.totalPrice.toStringAsFixed(2)}',
|
||||
style: subStyle,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
SizedBox(
|
||||
width: 64,
|
||||
child: l.alreadyReturned
|
||||
? const SizedBox.shrink()
|
||||
: (_staged.contains(l.itemId)
|
||||
? TextButton(
|
||||
onPressed: _submitting
|
||||
? null
|
||||
: () => setState(() => _staged.remove(l.itemId)),
|
||||
child: const Text('撤销',
|
||||
style: TextStyle(fontSize: 12, color: AppTheme.primary)),
|
||||
)
|
||||
: OutlinedButton(
|
||||
onPressed: _submitting ? null : () => _confirmLine(l),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppTheme.danger,
|
||||
side: const BorderSide(color: AppTheme.danger),
|
||||
visualDensity: VisualDensity.compact,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
),
|
||||
child: const Text('退单', style: TextStyle(fontSize: 12)),
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static String _q(double v) =>
|
||||
v == v.roundToDouble() ? v.toInt().toString() : v.toString();
|
||||
}
|
||||
@@ -24,7 +24,9 @@ List<Widget> buildOrderRowActions({
|
||||
required VoidCallback onApprove,
|
||||
required VoidCallback onReject,
|
||||
required VoidCallback onWithdraw,
|
||||
required VoidCallback onReturn,
|
||||
bool canWithdraw = false,
|
||||
bool canReturn = false,
|
||||
List<Widget> afterPrint = const [],
|
||||
}) {
|
||||
TextButton btn(String text, Color color, VoidCallback onPressed, {Key? key}) =>
|
||||
@@ -39,6 +41,11 @@ List<Widget> buildOrderRowActions({
|
||||
...afterPrint,
|
||||
if (!readonly && status == 'approved')
|
||||
WriteGuard(child: btn('结清', AppTheme.accent, onSettle)),
|
||||
// 退单(已审核单退货):error 样式,按权限显示
|
||||
if (!readonly && status == 'approved' && canReturn)
|
||||
WriteGuard(
|
||||
child: btn('退单', AppTheme.danger, onReturn,
|
||||
key: Key('btn_return_$orderId'))),
|
||||
if (!readonly && status == 'draft') ...[
|
||||
WriteGuard(child: btn('修改', AppTheme.primary, onEdit)),
|
||||
WriteGuard(child: btn('删除', AppTheme.danger, onDelete)),
|
||||
|
||||
Reference in New Issue
Block a user