Files
jiu/client/lib/screens/settings/settings_screen.dart
T
wangjia 76ab19ea47 feat(client): 初版 Flutter UI 框架,仿照参考截图实现
- 蓝色顶栏(#1565C0)+ 深蓝侧边栏(#0D47A1),含折叠/展开
- 底部状态栏:门店编号、登录用户、登录时间、实时时钟、版本号
- 登录页:居中卡片,支持密码显示/隐藏
- 入库单列表:筛选/搜索/日期选择/分页,三个 Tab(入库单/查询/审核)
- 新建入库单:基本信息 + 商品明细动态增删,实时计算合计金额
- 出库单列表:同入库单风格
- 库存查询:4 张统计卡片 + 颜色高亮缺货/预警行,库存预警 Tab
- 往来单位、财务、商品、系统设置(用户/仓库/编号规则/系统参数)
- 使用 Riverpod 状态管理 + go_router 路由 + Dio HTTP

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 12:51:41 +08:00

481 lines
19 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/theme/app_theme.dart';
class SettingsScreen extends ConsumerStatefulWidget {
const SettingsScreen({super.key});
@override
ConsumerState<SettingsScreen> createState() => _SettingsScreenState();
}
class _SettingsScreenState extends ConsumerState<SettingsScreen> {
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 4,
child: Column(
children: [
Container(
color: AppTheme.surface,
child: const TabBar(
isScrollable: true,
labelColor: AppTheme.primary,
unselectedLabelColor: AppTheme.textSecondary,
indicatorColor: AppTheme.primary,
indicatorWeight: 2,
labelStyle:
TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
tabs: [
Tab(text: '用户管理'),
Tab(text: '仓库管理'),
Tab(text: '编号规则'),
Tab(text: '系统参数'),
],
),
),
const Divider(height: 1),
Expanded(
child: TabBarView(
children: [
_buildUsersTab(),
_buildWarehousesTab(),
_buildNumberRulesTab(),
_buildSystemParamsTab(),
],
),
),
],
),
);
}
Widget _buildUsersTab() {
final users = [
{'name': '张三', 'username': 'zhangsan', 'role': '管理员', 'status': true, 'lastLogin': '2026-04-04 09:15'},
{'name': '李四', 'username': 'lisi', 'role': '操作员', 'status': true, 'lastLogin': '2026-04-04 08:30'},
{'name': '王五', 'username': 'wangwu', 'role': '操作员', 'status': true, 'lastLogin': '2026-04-03 17:45'},
{'name': '赵六', 'username': 'zhaoliu', 'role': '只读', 'status': false, 'lastLogin': '2026-03-28 10:20'},
];
return Column(
children: [
Container(
height: 52,
color: AppTheme.surface,
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
children: [
ElevatedButton.icon(
onPressed: () => _showAddUserDialog(context),
icon: const Icon(Icons.person_add, size: 16),
label: const Text('新增用户'),
),
],
),
),
const Divider(height: 1),
Expanded(
child: SingleChildScrollView(
child: DataTable(
headingRowColor: WidgetStateProperty.all(const Color(0xFFF0F4FF)),
columns: const [
DataColumn(label: Text('姓名')),
DataColumn(label: Text('用户名')),
DataColumn(label: Text('角色')),
DataColumn(label: Text('状态')),
DataColumn(label: Text('最后登录')),
DataColumn(label: Text('操作')),
],
rows: users
.map((u) => DataRow(cells: [
DataCell(Row(
mainAxisSize: MainAxisSize.min,
children: [
CircleAvatar(
radius: 14,
backgroundColor: AppTheme.primary.withOpacity(0.15),
child: Text(
(u['name'] as String).substring(0, 1),
style: const TextStyle(
fontSize: 12,
color: AppTheme.primary,
fontWeight: FontWeight.w600),
),
),
const SizedBox(width: 8),
Text(u['name'] as String),
],
)),
DataCell(Text(u['username'] as String,
style: const TextStyle(
fontFamily: 'monospace', fontSize: 12))),
DataCell(_RoleBadge(u['role'] as String)),
DataCell(Switch(
value: u['status'] as bool,
onChanged: (_) {},
materialTapTargetSize:
MaterialTapTargetSize.shrinkWrap,
)),
DataCell(Text(u['lastLogin'] as String,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary))),
DataCell(Row(
mainAxisSize: MainAxisSize.min,
children: [
TextButton(
onPressed: () {},
child: const Text('编辑',
style: TextStyle(fontSize: 12))),
TextButton(
onPressed: () {},
child: const Text('重置密码',
style: TextStyle(fontSize: 12))),
],
)),
]))
.toList(),
),
),
),
],
);
}
Widget _buildWarehousesTab() {
final warehouses = [
{'code': 'WH001', 'name': '主仓库', 'location': '一楼东侧', 'manager': '张三', 'capacity': 1000, 'used': 680, 'status': true},
{'code': 'WH002', 'name': '副仓库', 'location': '二楼西侧', 'manager': '李四', 'capacity': 500, 'used': 210, 'status': true},
{'code': 'WH003', 'name': '保税仓库', 'location': '地下一层', 'manager': '王五', 'capacity': 300, 'used': 0, 'status': false},
];
return Column(
children: [
Container(
height: 52,
color: AppTheme.surface,
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
children: [
ElevatedButton.icon(
onPressed: () {},
icon: const Icon(Icons.add, size: 16),
label: const Text('新增仓库'),
),
],
),
),
const Divider(height: 1),
Expanded(
child: SingleChildScrollView(
child: DataTable(
headingRowColor: WidgetStateProperty.all(const Color(0xFFF0F4FF)),
columns: const [
DataColumn(label: Text('仓库编号')),
DataColumn(label: Text('仓库名称')),
DataColumn(label: Text('位置')),
DataColumn(label: Text('负责人')),
DataColumn(label: Text('使用情况')),
DataColumn(label: Text('状态')),
DataColumn(label: Text('操作')),
],
rows: warehouses
.map((w) => DataRow(cells: [
DataCell(Text(w['code'] as String,
style: const TextStyle(
fontFamily: 'monospace', fontSize: 12))),
DataCell(Text(w['name'] as String,
style: const TextStyle(fontWeight: FontWeight.w500))),
DataCell(Text(w['location'] as String)),
DataCell(Text(w['manager'] as String)),
DataCell(SizedBox(
width: 140,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${w['used']}/${w['capacity']}',
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary),
),
const SizedBox(height: 2),
LinearProgressIndicator(
value: (w['capacity'] as int) > 0
? (w['used'] as int) / (w['capacity'] as int)
: 0,
backgroundColor: AppTheme.border,
valueColor: AlwaysStoppedAnimation<Color>(
(w['used'] as int) / (w['capacity'] as int) > 0.8
? AppTheme.danger
: AppTheme.primary,
),
),
],
),
)),
DataCell(Switch(
value: w['status'] as bool,
onChanged: (_) {},
materialTapTargetSize:
MaterialTapTargetSize.shrinkWrap,
)),
DataCell(Row(
mainAxisSize: MainAxisSize.min,
children: [
TextButton(
onPressed: () {},
child: const Text('编辑',
style: TextStyle(fontSize: 12))),
],
)),
]))
.toList(),
),
),
),
],
);
}
Widget _buildNumberRulesTab() {
final rules = [
{'type': '入库单', 'prefix': 'RK', 'format': 'RK{年}{月}{日}{序号4}', 'example': 'RK20260404001', 'currentNo': 4},
{'type': '出库单', 'prefix': 'CK', 'format': 'CK{年}{月}{日}{序号4}', 'example': 'CK20260404001', 'currentNo': 2},
{'type': '盘点单', 'prefix': 'PD', 'format': 'PD{年}{月}{日}{序号4}', 'example': 'PD20260404001', 'currentNo': 1},
{'type': '商品编码', 'prefix': 'SP', 'format': 'SP{序号3}', 'example': 'SP001', 'currentNo': 12},
];
return SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('编号规则配置',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
const SizedBox(height: 4),
const Text('配置各类单据的自动编号规则,修改后对新建单据生效',
style: TextStyle(fontSize: 13, color: AppTheme.textSecondary)),
const SizedBox(height: 16),
Card(
child: DataTable(
headingRowColor:
WidgetStateProperty.all(const Color(0xFFF0F4FF)),
columns: const [
DataColumn(label: Text('单据类型')),
DataColumn(label: Text('前缀')),
DataColumn(label: Text('格式')),
DataColumn(label: Text('示例')),
DataColumn(label: Text('当前序号'), numeric: true),
DataColumn(label: Text('操作')),
],
rows: rules
.map((r) => DataRow(cells: [
DataCell(Text(r['type'] as String,
style: const TextStyle(fontWeight: FontWeight.w500))),
DataCell(Container(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: AppTheme.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(3),
),
child: Text(r['prefix'] as String,
style: const TextStyle(
color: AppTheme.primary,
fontFamily: 'monospace',
fontSize: 13,
fontWeight: FontWeight.w600)),
)),
DataCell(Text(r['format'] as String,
style: const TextStyle(
fontSize: 12, color: AppTheme.textSecondary))),
DataCell(Text(r['example'] as String,
style: const TextStyle(
fontFamily: 'monospace', fontSize: 12))),
DataCell(Text('${r['currentNo']}')),
DataCell(TextButton(
onPressed: () {},
child: const Text('编辑', style: TextStyle(fontSize: 12)))),
]))
.toList(),
),
),
],
),
);
}
Widget _buildSystemParamsTab() {
return SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('系统参数',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
const SizedBox(height: 16),
Card(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('基本设置',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: AppTheme.primaryDark)),
const Divider(height: 24),
_ParamRow(label: '系统名称', value: '酒库管理系统'),
_ParamRow(label: '货币单位', value: '人民币(CNY'),
_ParamRow(label: '日期格式', value: 'YYYY-MM-DD'),
_ParamRow(label: '时区', value: 'Asia/Shanghai (UTC+8)'),
const SizedBox(height: 16),
const Text('审核设置',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: AppTheme.primaryDark)),
const Divider(height: 24),
_ParamRow(label: '入库单需要审核', value: '', isSwitch: true),
_ParamRow(label: '出库单需要审核', value: '', isSwitch: true),
_ParamRow(label: '允许超量出库', value: '', isSwitch: false),
const SizedBox(height: 16),
const Text('库存预警设置',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: AppTheme.primaryDark)),
const Divider(height: 24),
_ParamRow(label: '启用库存预警', value: '', isSwitch: true),
_ParamRow(label: '预警提醒方式', value: '系统内通知'),
],
),
),
),
const SizedBox(height: 12),
Row(
children: [
ElevatedButton(
onPressed: () {},
child: const Text('保存设置'),
),
const SizedBox(width: 8),
OutlinedButton(
onPressed: () {},
child: const Text('重置默认'),
),
],
),
],
),
);
}
void _showAddUserDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('新增用户'),
content: SizedBox(
width: 400,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const TextField(
decoration: InputDecoration(labelText: '姓名', hintText: '请输入姓名'),
),
const SizedBox(height: 12),
const TextField(
decoration: InputDecoration(labelText: '用户名', hintText: '请输入登录用户名'),
),
const SizedBox(height: 12),
const TextField(
obscureText: true,
decoration: InputDecoration(labelText: '初始密码', hintText: '请设置初始密码'),
),
const SizedBox(height: 12),
DropdownButtonFormField<String>(
value: '操作员',
items: ['管理员', '操作员', '只读']
.map((r) => DropdownMenuItem(value: r, child: Text(r)))
.toList(),
onChanged: (_) {},
decoration: const InputDecoration(labelText: '角色'),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
ElevatedButton(
onPressed: () => Navigator.pop(context),
child: const Text('确定'),
),
],
),
);
}
}
class _RoleBadge extends StatelessWidget {
final String role;
const _RoleBadge(this.role);
@override
Widget build(BuildContext context) {
final Color bg;
final Color fg;
switch (role) {
case '管理员':
bg = const Color(0xFFE3F2FD);
fg = AppTheme.primary;
break;
case '操作员':
bg = const Color(0xFFE8F5E9);
fg = AppTheme.success;
break;
default:
bg = const Color(0xFFF5F5F5);
fg = AppTheme.textSecondary;
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(color: bg, borderRadius: BorderRadius.circular(3)),
child: Text(role, style: TextStyle(color: fg, fontSize: 12, fontWeight: FontWeight.w500)),
);
}
}
class _ParamRow extends StatelessWidget {
final String label;
final String value;
final bool? isSwitch;
const _ParamRow({required this.label, required this.value, this.isSwitch});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
SizedBox(
width: 180,
child: Text(label,
style: const TextStyle(fontSize: 14, color: AppTheme.textSecondary)),
),
if (isSwitch != null)
Switch(
value: isSwitch!,
onChanged: (_) {},
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
)
else
Text(value,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
const Spacer(),
TextButton(onPressed: () {}, child: const Text('修改', style: TextStyle(fontSize: 12))),
],
),
);
}
}