76ab19ea47
- 蓝色顶栏(#1565C0)+ 深蓝侧边栏(#0D47A1),含折叠/展开 - 底部状态栏:门店编号、登录用户、登录时间、实时时钟、版本号 - 登录页:居中卡片,支持密码显示/隐藏 - 入库单列表:筛选/搜索/日期选择/分页,三个 Tab(入库单/查询/审核) - 新建入库单:基本信息 + 商品明细动态增删,实时计算合计金额 - 出库单列表:同入库单风格 - 库存查询:4 张统计卡片 + 颜色高亮缺货/预警行,库存预警 Tab - 往来单位、财务、商品、系统设置(用户/仓库/编号规则/系统参数) - 使用 Riverpod 状态管理 + go_router 路由 + Dio HTTP Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
481 lines
19 KiB
Dart
481 lines
19 KiB
Dart
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))),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|