admin_permissions_page.dart 32 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006
  1. import 'dart:async';
  2. import 'package:flutter/material.dart';
  3. import 'package:go_router/go_router.dart';
  4. import 'package:flutter_riverpod/flutter_riverpod.dart';
  5. import '../../core/theme/app_colors.dart';
  6. import '../../core/i18n/app_localizations.dart';
  7. import '../shell/nav_bar_config.dart';
  8. /// 权限管理 - 页面3.2 【管理员专属】
  9. class AdminPermissionsPage extends ConsumerStatefulWidget {
  10. const AdminPermissionsPage({super.key});
  11. @override
  12. ConsumerState<AdminPermissionsPage> createState() =>
  13. _AdminPermissionsPageState();
  14. }
  15. class _AdminPermissionsPageState extends ConsumerState<AdminPermissionsPage> {
  16. final _searchCtrl = TextEditingController();
  17. Timer? _debounce;
  18. String _searchQuery = '';
  19. // 模拟当前登录用户(自保护用)
  20. static const _currentUserId = '0001';
  21. final _employees = <_Employee>[
  22. _Employee(
  23. name: '张三',
  24. avatarText: '张',
  25. employeeId: '0048',
  26. department: '销售部',
  27. roles: ['普通员工', '审批人'],
  28. isActive: true,
  29. ),
  30. _Employee(
  31. name: '王经理',
  32. avatarText: '王',
  33. employeeId: '0012',
  34. department: '销售部',
  35. roles: ['审批人', '系统管理员'],
  36. isActive: true,
  37. ),
  38. _Employee(
  39. name: '李会计',
  40. avatarText: '李',
  41. employeeId: '0025',
  42. department: '财务部',
  43. roles: ['财务人员'],
  44. isActive: true,
  45. ),
  46. _Employee(
  47. name: '赵管理员',
  48. avatarText: '赵',
  49. employeeId: '0001',
  50. department: '信息技术部',
  51. roles: ['系统管理员'],
  52. isActive: true,
  53. ),
  54. _Employee(
  55. name: '钱六',
  56. avatarText: '钱',
  57. employeeId: '0052',
  58. department: '财务部',
  59. roles: ['财务人员'],
  60. isActive: false,
  61. ),
  62. _Employee(
  63. name: '孙七',
  64. avatarText: '孙',
  65. employeeId: '0078',
  66. department: '行政部',
  67. roles: ['普通员工'],
  68. isActive: true,
  69. ),
  70. _Employee(
  71. name: '周八',
  72. avatarText: '周',
  73. employeeId: '0091',
  74. department: '技术部',
  75. roles: ['普通员工'],
  76. isActive: true,
  77. ),
  78. ];
  79. List<_Employee> get _filteredEmployees {
  80. if (_searchQuery.isEmpty) return _employees;
  81. final q = _searchQuery.toLowerCase();
  82. return _employees.where((e) {
  83. return e.name.toLowerCase().contains(q) ||
  84. e.employeeId.toLowerCase().contains(q);
  85. }).toList();
  86. }
  87. @override
  88. void initState() {
  89. super.initState();
  90. }
  91. @override
  92. void dispose() {
  93. _searchCtrl.dispose();
  94. _debounce?.cancel();
  95. super.dispose();
  96. }
  97. void _onSearchChanged(String value) {
  98. _debounce?.cancel();
  99. _debounce = Timer(const Duration(milliseconds: 300), () {
  100. if (mounted) {
  101. setState(() => _searchQuery = value);
  102. }
  103. });
  104. }
  105. @override
  106. Widget build(BuildContext context) {
  107. final l10n = AppLocalizations.of(context);
  108. ref
  109. .read(navBarConfigProvider.notifier)
  110. .update(
  111. NavBarConfig(
  112. title: l10n.get('permissionManagement'),
  113. showBack: true,
  114. onBack: () => context.pop(),
  115. ),
  116. );
  117. return Column(
  118. children: [
  119. _buildSearchBar(),
  120. Expanded(
  121. child: ListView.builder(
  122. padding: const EdgeInsets.all(16),
  123. itemCount: _filteredEmployees.length,
  124. itemBuilder: (_, i) => _buildEmpCard(_filteredEmployees[i]),
  125. ),
  126. ),
  127. ],
  128. );
  129. }
  130. // ── 搜索栏(300ms 防抖) ──
  131. Widget _buildSearchBar() {
  132. return Container(
  133. width: double.infinity,
  134. padding: const EdgeInsets.fromLTRB(16, 10, 16, 10),
  135. color: AppColors.bgCard,
  136. child: Container(
  137. padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 2),
  138. decoration: BoxDecoration(
  139. color: AppColors.bgPage,
  140. borderRadius: BorderRadius.circular(18),
  141. border: Border.all(color: AppColors.border),
  142. ),
  143. child: Row(
  144. children: [
  145. const Icon(Icons.search, size: 18, color: AppColors.textPlaceholder),
  146. const SizedBox(width: 6),
  147. Expanded(
  148. child: TextField(
  149. controller: _searchCtrl,
  150. onChanged: _onSearchChanged,
  151. decoration: const InputDecoration(
  152. hintText: '输入姓名或工号进行检索...',
  153. hintStyle:
  154. TextStyle(fontSize: 14, color: AppColors.textPlaceholder),
  155. border: InputBorder.none,
  156. contentPadding: EdgeInsets.symmetric(vertical: 10),
  157. isDense: true,
  158. ),
  159. style:
  160. const TextStyle(fontSize: 14, color: AppColors.textPrimary),
  161. ),
  162. ),
  163. if (_searchCtrl.text.isNotEmpty)
  164. GestureDetector(
  165. onTap: () {
  166. _searchCtrl.clear();
  167. _onSearchChanged('');
  168. setState(() => _searchQuery = '');
  169. },
  170. child: const Icon(Icons.clear, size: 16, color: AppColors.textPlaceholder),
  171. ),
  172. ],
  173. ),
  174. ),
  175. );
  176. }
  177. // ── 员工卡片 ──
  178. Widget _buildEmpCard(_Employee emp) {
  179. return Padding(
  180. padding: const EdgeInsets.only(bottom: 12),
  181. child: GestureDetector(
  182. onTap: () => _openPermissionDrawer(emp),
  183. child: Container(
  184. padding: const EdgeInsets.all(12),
  185. decoration: BoxDecoration(
  186. color: emp.isActive ? AppColors.bgCard : const Color(0xFFF9F9F9),
  187. borderRadius: BorderRadius.circular(8),
  188. boxShadow: const [
  189. BoxShadow(
  190. color: Color(0x08000000), blurRadius: 4, offset: Offset(0, 1)),
  191. ],
  192. ),
  193. child: Row(
  194. crossAxisAlignment: CrossAxisAlignment.start,
  195. children: [
  196. // 头像
  197. Container(
  198. width: 40,
  199. height: 40,
  200. decoration: BoxDecoration(
  201. color: AppColors.primary,
  202. borderRadius: BorderRadius.circular(20),
  203. ),
  204. child: Center(
  205. child: Text(
  206. emp.avatarText,
  207. style: const TextStyle(
  208. fontSize: 16,
  209. fontWeight: FontWeight.w600,
  210. color: Colors.white,
  211. ),
  212. ),
  213. ),
  214. ),
  215. const SizedBox(width: 10),
  216. // 信息区
  217. Expanded(
  218. child: Column(
  219. crossAxisAlignment: CrossAxisAlignment.start,
  220. children: [
  221. Row(
  222. children: [
  223. Text(
  224. emp.name,
  225. style: const TextStyle(
  226. fontSize: 15,
  227. fontWeight: FontWeight.w600,
  228. color: AppColors.textPrimary,
  229. ),
  230. ),
  231. const SizedBox(width: 6),
  232. Text(
  233. '工号:${emp.employeeId}',
  234. style: const TextStyle(
  235. fontSize: 12,
  236. color: AppColors.textPlaceholder,
  237. ),
  238. ),
  239. ],
  240. ),
  241. const SizedBox(height: 2),
  242. Text(
  243. emp.department,
  244. style: const TextStyle(
  245. fontSize: 12,
  246. color: AppColors.textSecondary,
  247. ),
  248. ),
  249. ],
  250. ),
  251. ),
  252. // 角色标签区
  253. Column(
  254. crossAxisAlignment: CrossAxisAlignment.end,
  255. children: [
  256. ...emp.roles.map(
  257. (role) => Padding(
  258. padding: const EdgeInsets.only(bottom: 4),
  259. child: _buildRoleTag(role),
  260. ),
  261. ),
  262. if (!emp.isActive)
  263. Container(
  264. padding: const EdgeInsets.symmetric(
  265. horizontal: 6, vertical: 2),
  266. decoration: BoxDecoration(
  267. color: AppColors.dangerBg,
  268. borderRadius: BorderRadius.circular(3),
  269. ),
  270. child: const Text('已禁用',
  271. style:
  272. TextStyle(fontSize: 10, color: AppColors.danger)),
  273. ),
  274. ],
  275. ),
  276. ],
  277. ),
  278. ),
  279. ),
  280. );
  281. }
  282. Widget _buildRoleTag(String role) {
  283. Color bgColor;
  284. Color textColor;
  285. switch (role) {
  286. case '审批人':
  287. bgColor = AppColors.warningBg;
  288. textColor = AppColors.warning;
  289. break;
  290. case '财务人员':
  291. bgColor = AppColors.successBg;
  292. textColor = AppColors.success;
  293. break;
  294. case '系统管理员':
  295. bgColor = AppColors.dangerBg;
  296. textColor = AppColors.danger;
  297. break;
  298. default:
  299. bgColor = AppColors.primaryLight;
  300. textColor = AppColors.primary;
  301. }
  302. return Container(
  303. padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
  304. decoration: BoxDecoration(
  305. color: bgColor,
  306. borderRadius: BorderRadius.circular(3),
  307. ),
  308. child: Text(role, style: TextStyle(fontSize: 10, color: textColor)),
  309. );
  310. }
  311. // ── 权限抽屉(右侧滑出) ──
  312. void _openPermissionDrawer(_Employee emp) {
  313. // 深拷贝当前权限状态
  314. final checked = <String, bool>{};
  315. for (final perm in _allPermissions) {
  316. checked[perm.id] = _getDefaultPerms(emp.roles).contains(perm.id);
  317. }
  318. showGeneralDialog(
  319. context: context,
  320. barrierDismissible: true,
  321. barrierLabel: '',
  322. barrierColor: Colors.black45,
  323. transitionDuration: const Duration(milliseconds: 300),
  324. pageBuilder: (ctx, anim1, anim2) {
  325. return _PermissionDrawer(
  326. employee: emp,
  327. checked: checked,
  328. currentUserId: _currentUserId,
  329. onSave: () {
  330. Navigator.of(ctx).pop();
  331. ScaffoldMessenger.of(context).showSnackBar(
  332. const SnackBar(
  333. content: Text('权限已更新'),
  334. duration: Duration(seconds: 2)),
  335. );
  336. },
  337. onCancel: () => Navigator.of(ctx).pop(),
  338. );
  339. },
  340. transitionBuilder: (ctx, anim, secondaryAnim, child) {
  341. return SlideTransition(
  342. position: Tween<Offset>(
  343. begin: const Offset(1.0, 0.0),
  344. end: Offset.zero,
  345. ).animate(CurvedAnimation(parent: anim, curve: Curves.easeOutCubic)),
  346. child: child,
  347. );
  348. },
  349. );
  350. }
  351. }
  352. // ── 权限数据定义 ──
  353. class _PermModule {
  354. final String name;
  355. final List<_PermItem> items;
  356. const _PermModule({required this.name, required this.items});
  357. }
  358. class _PermItem {
  359. final String id;
  360. final String label;
  361. const _PermItem({required this.id, required this.label});
  362. }
  363. const _allPermissions = <_PermItem>[
  364. _PermItem(id: 'expense.apply', label: '发起报销'),
  365. _PermItem(id: 'expense.view_own', label: '查看本人报销'),
  366. _PermItem(id: 'expense.view_dept', label: '查看部门报销'),
  367. _PermItem(id: 'expense.view_all', label: '查看全公司报销'),
  368. _PermItem(id: 'expense.approve', label: '审批报销'),
  369. _PermItem(id: 'expense.mark_paid', label: '确认付款'),
  370. _PermItem(id: 'expense.export', label: '导出报销数据'),
  371. _PermItem(id: 'preapply.apply', label: '发起事前申请'),
  372. _PermItem(id: 'preapply.view_own', label: '查看本人申请'),
  373. _PermItem(id: 'preapply.view_dept', label: '查看部门申请'),
  374. _PermItem(id: 'preapply.approve', label: '审批事前申请'),
  375. _PermItem(id: 'overtime.apply', label: '发起加班'),
  376. _PermItem(id: 'overtime.view_own', label: '查看本人加班'),
  377. _PermItem(id: 'overtime.view_dept', label: '查看部门加班'),
  378. _PermItem(id: 'overtime.approve', label: '审批加班'),
  379. _PermItem(id: 'vehicle.apply', label: '发起用车'),
  380. _PermItem(id: 'vehicle.view_own', label: '查看本人用车'),
  381. _PermItem(id: 'vehicle.view_dept', label: '查看部门用车'),
  382. _PermItem(id: 'vehicle.approve', label: '审批用车'),
  383. _PermItem(id: 'outing.create', label: '创建外勤日志'),
  384. _PermItem(id: 'outing.view_own', label: '查看本人日志'),
  385. _PermItem(id: 'outing.view_dept', label: '查看部门日志'),
  386. _PermItem(id: 'outing.comment', label: '点评外勤日志'),
  387. _PermItem(id: 'announcement.view', label: '查看公告'),
  388. _PermItem(id: 'announcement.create', label: '发布公告'),
  389. _PermItem(id: 'report.view', label: '查看报表'),
  390. _PermItem(id: 'report.export', label: '导出报表'),
  391. _PermItem(id: 'admin.permissions', label: '管理权限'),
  392. ];
  393. // 按模块分组
  394. const _permModules = <_PermModule>[
  395. _PermModule(name: '报销管理', items: [
  396. _PermItem(id: 'expense.apply', label: '发起报销'),
  397. _PermItem(id: 'expense.view_own', label: '查看本人报销'),
  398. _PermItem(id: 'expense.view_dept', label: '查看部门报销'),
  399. _PermItem(id: 'expense.view_all', label: '查看全公司报销'),
  400. _PermItem(id: 'expense.approve', label: '审批报销'),
  401. _PermItem(id: 'expense.mark_paid', label: '确认付款'),
  402. _PermItem(id: 'expense.export', label: '导出报销数据'),
  403. ]),
  404. _PermModule(name: '事前申请', items: [
  405. _PermItem(id: 'preapply.apply', label: '发起事前申请'),
  406. _PermItem(id: 'preapply.view_own', label: '查看本人申请'),
  407. _PermItem(id: 'preapply.view_dept', label: '查看部门申请'),
  408. _PermItem(id: 'preapply.approve', label: '审批事前申请'),
  409. ]),
  410. _PermModule(name: '加班管理', items: [
  411. _PermItem(id: 'overtime.apply', label: '发起加班'),
  412. _PermItem(id: 'overtime.view_own', label: '查看本人加班'),
  413. _PermItem(id: 'overtime.view_dept', label: '查看部门加班'),
  414. _PermItem(id: 'overtime.approve', label: '审批加班'),
  415. ]),
  416. _PermModule(name: '用车管理', items: [
  417. _PermItem(id: 'vehicle.apply', label: '发起用车'),
  418. _PermItem(id: 'vehicle.view_own', label: '查看本人用车'),
  419. _PermItem(id: 'vehicle.view_dept', label: '查看部门用车'),
  420. _PermItem(id: 'vehicle.approve', label: '审批用车'),
  421. ]),
  422. _PermModule(name: '外勤管理', items: [
  423. _PermItem(id: 'outing.create', label: '创建外勤日志'),
  424. _PermItem(id: 'outing.view_own', label: '查看本人日志'),
  425. _PermItem(id: 'outing.view_dept', label: '查看部门日志'),
  426. _PermItem(id: 'outing.comment', label: '点评外勤日志'),
  427. ]),
  428. _PermModule(name: '公告管理', items: [
  429. _PermItem(id: 'announcement.view', label: '查看公告'),
  430. _PermItem(id: 'announcement.create', label: '发布公告'),
  431. ]),
  432. _PermModule(name: '报表管理', items: [
  433. _PermItem(id: 'report.view', label: '查看报表'),
  434. _PermItem(id: 'report.export', label: '导出报表'),
  435. ]),
  436. _PermModule(name: '系统管理', items: [
  437. _PermItem(id: 'admin.permissions', label: '管理权限'),
  438. ]),
  439. ];
  440. // 角色预设
  441. const _presets = <_RolePreset>[
  442. _RolePreset(name: '员工', permissions: [
  443. 'expense.apply', 'expense.view_own',
  444. 'preapply.apply', 'preapply.view_own',
  445. 'overtime.apply', 'overtime.view_own',
  446. 'vehicle.apply', 'vehicle.view_own',
  447. 'outing.create', 'outing.view_own',
  448. 'announcement.view',
  449. 'report.view',
  450. ]),
  451. _RolePreset(name: '审批人', permissions: [
  452. 'expense.apply', 'expense.view_own', 'expense.view_dept', 'expense.approve',
  453. 'preapply.apply', 'preapply.view_own', 'preapply.view_dept', 'preapply.approve',
  454. 'overtime.apply', 'overtime.view_own', 'overtime.view_dept', 'overtime.approve',
  455. 'vehicle.apply', 'vehicle.view_own', 'vehicle.view_dept', 'vehicle.approve',
  456. 'outing.create', 'outing.view_own', 'outing.view_dept', 'outing.comment',
  457. 'announcement.view',
  458. 'report.view',
  459. ]),
  460. _RolePreset(name: '财务人员', permissions: [
  461. 'expense.apply', 'expense.view_own', 'expense.view_all', 'expense.mark_paid', 'expense.export',
  462. 'preapply.apply', 'preapply.view_own',
  463. 'announcement.view',
  464. 'report.view', 'report.export',
  465. ]),
  466. _RolePreset(name: '系统管理员', permissions: [
  467. 'expense.apply', 'expense.view_own', 'expense.view_dept', 'expense.view_all',
  468. 'expense.approve', 'expense.mark_paid', 'expense.export',
  469. 'preapply.apply', 'preapply.view_own', 'preapply.view_dept', 'preapply.approve',
  470. 'overtime.apply', 'overtime.view_own', 'overtime.view_dept', 'overtime.approve',
  471. 'vehicle.apply', 'vehicle.view_own', 'vehicle.view_dept', 'vehicle.approve',
  472. 'outing.create', 'outing.view_own', 'outing.view_dept', 'outing.comment',
  473. 'announcement.view', 'announcement.create',
  474. 'report.view', 'report.export',
  475. 'admin.permissions',
  476. ]),
  477. ];
  478. Set<String> _getDefaultPerms(List<String> roles) {
  479. if (roles.contains('系统管理员')) return _presets[3].permissions.toSet();
  480. if (roles.contains('财务人员')) return _presets[2].permissions.toSet();
  481. if (roles.contains('审批人')) return _presets[1].permissions.toSet();
  482. return _presets[0].permissions.toSet();
  483. }
  484. class _RolePreset {
  485. final String name;
  486. final List<String> permissions;
  487. const _RolePreset({required this.name, required this.permissions});
  488. }
  489. // ── 员工数据模型 ──
  490. class _Employee {
  491. final String name;
  492. final String avatarText;
  493. final String employeeId;
  494. final String department;
  495. final List<String> roles;
  496. final bool isActive;
  497. const _Employee({
  498. required this.name,
  499. required this.avatarText,
  500. required this.employeeId,
  501. required this.department,
  502. required this.roles,
  503. this.isActive = true,
  504. });
  505. }
  506. // ── 权限抽屉组件 ──
  507. class _PermissionDrawer extends StatefulWidget {
  508. final _Employee employee;
  509. final Map<String, bool> checked;
  510. final String currentUserId;
  511. final VoidCallback onSave;
  512. final VoidCallback onCancel;
  513. const _PermissionDrawer({
  514. required this.employee,
  515. required this.checked,
  516. required this.currentUserId,
  517. required this.onSave,
  518. required this.onCancel,
  519. });
  520. @override
  521. State<_PermissionDrawer> createState() => _PermissionDrawerState();
  522. }
  523. class _PermissionDrawerState extends State<_PermissionDrawer> {
  524. late Map<String, bool> _checked;
  525. bool _showHistory = false;
  526. bool get _isSelfAdmin =>
  527. widget.employee.employeeId == widget.currentUserId;
  528. // 模拟变更记录
  529. final _mockHistory = [
  530. _ChangeLog(
  531. time: '2026-06-04 14:32',
  532. operator: '赵管理员',
  533. summary: '添加了财务人员角色',
  534. ),
  535. _ChangeLog(
  536. time: '2026-06-03 09:15',
  537. operator: '赵管理员',
  538. summary: '添加了审批人权限(报销审批、加班审批)',
  539. ),
  540. _ChangeLog(
  541. time: '2026-05-28 16:40',
  542. operator: '王经理',
  543. summary: '修改为普通员工权限',
  544. ),
  545. _ChangeLog(
  546. time: '2026-05-20 11:00',
  547. operator: '赵管理员',
  548. summary: '初始权限分配',
  549. ),
  550. ];
  551. @override
  552. void initState() {
  553. super.initState();
  554. _checked = Map.from(widget.checked);
  555. }
  556. @override
  557. Widget build(BuildContext context) {
  558. final width = MediaQuery.of(context).size.width * 0.82;
  559. return Material(
  560. color: Colors.transparent,
  561. child: Align(
  562. alignment: Alignment.centerRight,
  563. child: Container(
  564. width: width,
  565. height: double.infinity,
  566. decoration: const BoxDecoration(
  567. color: AppColors.bgPage,
  568. borderRadius: BorderRadius.only(
  569. topLeft: Radius.circular(12),
  570. bottomLeft: Radius.circular(12),
  571. ),
  572. ),
  573. child: Column(
  574. children: [
  575. // 标题栏
  576. _buildHeader(),
  577. // 可滚动内容
  578. Expanded(
  579. child: SingleChildScrollView(
  580. child: Column(
  581. crossAxisAlignment: CrossAxisAlignment.start,
  582. children: [
  583. _buildEmployeeInfo(),
  584. _buildQuickPresets(),
  585. _buildPermissionList(),
  586. _buildHistorySection(),
  587. const SizedBox(height: 24),
  588. ],
  589. ),
  590. ),
  591. ),
  592. // 底部保存按钮
  593. _buildSaveButton(),
  594. ],
  595. ),
  596. ),
  597. ),
  598. );
  599. }
  600. Widget _buildHeader() {
  601. final l10n = AppLocalizations.of(context);
  602. return Container(
  603. padding: const EdgeInsets.fromLTRB(16, 48, 8, 12),
  604. decoration: const BoxDecoration(
  605. color: AppColors.bgCard,
  606. border: Border(bottom: BorderSide(color: AppColors.border)),
  607. ),
  608. child: Row(
  609. children: [
  610. Text(l10n.get('permissionEdit'),
  611. style: const TextStyle(
  612. fontSize: 18,
  613. fontWeight: FontWeight.w600,
  614. color: AppColors.textPrimary)),
  615. const Spacer(),
  616. IconButton(
  617. icon: const Icon(Icons.close, size: 20, color: AppColors.textSecondary),
  618. onPressed: widget.onCancel,
  619. ),
  620. ],
  621. ),
  622. );
  623. }
  624. Widget _buildEmployeeInfo() {
  625. return Container(
  626. width: double.infinity,
  627. padding: const EdgeInsets.all(16),
  628. color: AppColors.bgCard,
  629. child: Row(
  630. children: [
  631. Container(
  632. width: 44,
  633. height: 44,
  634. decoration: BoxDecoration(
  635. color: AppColors.primary,
  636. borderRadius: BorderRadius.circular(22),
  637. ),
  638. child: Center(
  639. child: Text(
  640. widget.employee.avatarText,
  641. style: const TextStyle(
  642. fontSize: 18,
  643. fontWeight: FontWeight.w600,
  644. color: Colors.white),
  645. ),
  646. ),
  647. ),
  648. const SizedBox(width: 12),
  649. Expanded(
  650. child: Column(
  651. crossAxisAlignment: CrossAxisAlignment.start,
  652. children: [
  653. Text(widget.employee.name,
  654. style: const TextStyle(
  655. fontSize: 16,
  656. fontWeight: FontWeight.w600,
  657. color: AppColors.textPrimary)),
  658. const SizedBox(height: 2),
  659. Text('${widget.employee.department} · 工号:${widget.employee.employeeId}',
  660. style: const TextStyle(
  661. fontSize: 12, color: AppColors.textSecondary)),
  662. ],
  663. ),
  664. ),
  665. ],
  666. ),
  667. );
  668. }
  669. // ── 快捷套餐 ──
  670. Widget _buildQuickPresets() {
  671. final l10n = AppLocalizations.of(context);
  672. return Container(
  673. width: double.infinity,
  674. padding: const EdgeInsets.fromLTRB(16, 16, 16, 12),
  675. color: AppColors.bgCard,
  676. child: Column(
  677. crossAxisAlignment: CrossAxisAlignment.start,
  678. children: [
  679. Text(l10n.get('quickPresets'),
  680. style: const TextStyle(
  681. fontSize: 13,
  682. fontWeight: FontWeight.w600,
  683. color: AppColors.textSecondary)),
  684. const SizedBox(height: 10),
  685. Wrap(
  686. spacing: 8,
  687. runSpacing: 8,
  688. children: _presets.map((preset) {
  689. return GestureDetector(
  690. onTap: () => _applyPreset(preset),
  691. child: Container(
  692. padding:
  693. const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
  694. decoration: BoxDecoration(
  695. color: AppColors.primaryLight,
  696. borderRadius: BorderRadius.circular(16),
  697. border: Border.all(color: AppColors.primary.withValues(alpha: 0.3)),
  698. ),
  699. child: Text(preset.name,
  700. style: const TextStyle(
  701. fontSize: 13,
  702. color: AppColors.primary,
  703. fontWeight: FontWeight.w500)),
  704. ),
  705. );
  706. }).toList(),
  707. ),
  708. ],
  709. ),
  710. );
  711. }
  712. void _applyPreset(_RolePreset preset) {
  713. if (_isSelfAdmin && preset.name != '系统管理员') {
  714. // 自保护:自己是admin不能取消自己的admin
  715. final hadAdmin = widget.checked.keys.any(
  716. (k) => k == 'admin.permissions' && widget.checked[k] == true);
  717. if (hadAdmin && !preset.permissions.contains('admin.permissions')) {
  718. ScaffoldMessenger.of(context).showSnackBar(
  719. const SnackBar(
  720. content: Text('无法取消自己的管理员权限'),
  721. duration: Duration(seconds: 2)),
  722. );
  723. return;
  724. }
  725. }
  726. setState(() {
  727. // 先全重置
  728. for (final k in _checked.keys) {
  729. _checked[k] = false;
  730. }
  731. // 再勾选预设
  732. for (final p in preset.permissions) {
  733. if (_checked.containsKey(p)) {
  734. _checked[p] = true;
  735. }
  736. }
  737. });
  738. }
  739. // ── 权限点列表 ──
  740. Widget _buildPermissionList() {
  741. final l10n = AppLocalizations.of(context);
  742. return Container(
  743. width: double.infinity,
  744. padding: const EdgeInsets.fromLTRB(16, 8, 16, 4),
  745. color: AppColors.bgCard,
  746. child: Column(
  747. crossAxisAlignment: CrossAxisAlignment.start,
  748. children: [
  749. Text(l10n.get('permissionItems'),
  750. style: const TextStyle(
  751. fontSize: 13,
  752. fontWeight: FontWeight.w600,
  753. color: AppColors.textSecondary)),
  754. const SizedBox(height: 8),
  755. ..._permModules.map((module) => _buildModuleGroup(module)),
  756. ],
  757. ),
  758. );
  759. }
  760. Widget _buildModuleGroup(_PermModule module) {
  761. return Padding(
  762. padding: const EdgeInsets.only(bottom: 12),
  763. child: Column(
  764. crossAxisAlignment: CrossAxisAlignment.start,
  765. children: [
  766. Text(module.name,
  767. style: const TextStyle(
  768. fontSize: 13,
  769. fontWeight: FontWeight.w600,
  770. color: AppColors.textPrimary)),
  771. const SizedBox(height: 4),
  772. ...module.items.map((perm) {
  773. final val = _checked[perm.id] ?? false;
  774. final isAdminPerm = perm.id == 'admin.permissions';
  775. final canToggle = !(_isSelfAdmin && isAdminPerm);
  776. return GestureDetector(
  777. onTap: canToggle
  778. ? () => setState(() => _checked[perm.id] = !val)
  779. : null,
  780. child: Container(
  781. padding:
  782. const EdgeInsets.symmetric(vertical: 6, horizontal: 4),
  783. child: Row(
  784. children: [
  785. Icon(
  786. val ? Icons.check_box : Icons.check_box_outline_blank,
  787. size: 20,
  788. color: val
  789. ? canToggle
  790. ? AppColors.primary
  791. : AppColors.textPlaceholder
  792. : AppColors.textPlaceholder,
  793. ),
  794. const SizedBox(width: 8),
  795. Text(
  796. perm.label,
  797. style: TextStyle(
  798. fontSize: 13,
  799. color: canToggle
  800. ? AppColors.textPrimary
  801. : AppColors.textPlaceholder,
  802. ),
  803. ),
  804. if (!canToggle)
  805. const Padding(
  806. padding: EdgeInsets.only(left: 6),
  807. child: Icon(Icons.lock_outline,
  808. size: 12, color: AppColors.textPlaceholder),
  809. ),
  810. ],
  811. ),
  812. ),
  813. );
  814. }),
  815. ],
  816. ),
  817. );
  818. }
  819. // ── 变更记录 ──
  820. Widget _buildHistorySection() {
  821. final l10n = AppLocalizations.of(context);
  822. return Container(
  823. width: double.infinity,
  824. margin: const EdgeInsets.symmetric(horizontal: 16),
  825. decoration: BoxDecoration(
  826. color: AppColors.bgCard,
  827. borderRadius: BorderRadius.circular(8),
  828. ),
  829. child: Column(
  830. children: [
  831. GestureDetector(
  832. onTap: () => setState(() => _showHistory = !_showHistory),
  833. child: Padding(
  834. padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
  835. child: Row(
  836. children: [
  837. const Icon(Icons.history, size: 16, color: AppColors.textSecondary),
  838. const SizedBox(width: 6),
  839. Text(l10n.get('changeLog'),
  840. style: const TextStyle(
  841. fontSize: 13,
  842. fontWeight: FontWeight.w600,
  843. color: AppColors.textSecondary)),
  844. const Spacer(),
  845. Text(l10n.getString('recentItems', args: {'count': '${_mockHistory.length}'}),
  846. style: const TextStyle(
  847. fontSize: 11, color: AppColors.textPlaceholder)),
  848. Icon(
  849. _showHistory
  850. ? Icons.keyboard_arrow_up
  851. : Icons.keyboard_arrow_down,
  852. size: 18,
  853. color: AppColors.textPlaceholder,
  854. ),
  855. ],
  856. ),
  857. ),
  858. ),
  859. if (_showHistory)
  860. ..._mockHistory.map((log) => _buildTimelineItem(log)),
  861. ],
  862. ),
  863. );
  864. }
  865. Widget _buildTimelineItem(_ChangeLog log) {
  866. return Container(
  867. padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
  868. child: Row(
  869. crossAxisAlignment: CrossAxisAlignment.start,
  870. children: [
  871. Column(
  872. children: [
  873. Container(
  874. width: 8,
  875. height: 8,
  876. decoration: BoxDecoration(
  877. color: AppColors.primary,
  878. shape: BoxShape.circle,
  879. ),
  880. ),
  881. Container(
  882. width: 1,
  883. height: 40,
  884. color: AppColors.border,
  885. ),
  886. ],
  887. ),
  888. const SizedBox(width: 10),
  889. Expanded(
  890. child: Column(
  891. crossAxisAlignment: CrossAxisAlignment.start,
  892. children: [
  893. Text(log.summary,
  894. style: const TextStyle(
  895. fontSize: 13, color: AppColors.textPrimary)),
  896. const SizedBox(height: 2),
  897. Text('${log.operator} · ${log.time}',
  898. style: const TextStyle(
  899. fontSize: 11, color: AppColors.textPlaceholder)),
  900. ],
  901. ),
  902. ),
  903. ],
  904. ),
  905. );
  906. }
  907. // ── 保存按钮 ──
  908. Widget _buildSaveButton() {
  909. final l10n = AppLocalizations.of(context);
  910. return Container(
  911. width: double.infinity,
  912. padding: const EdgeInsets.all(16),
  913. decoration: BoxDecoration(
  914. color: AppColors.bgCard,
  915. boxShadow: const [
  916. BoxShadow(
  917. color: Color(0x15000000),
  918. blurRadius: 8,
  919. offset: Offset(0, -2)),
  920. ],
  921. ),
  922. child: GestureDetector(
  923. onTap: () {
  924. if (_isSelfAdmin && !(_checked['admin.permissions'] ?? false)) {
  925. ScaffoldMessenger.of(context).showSnackBar(
  926. const SnackBar(
  927. content: Text('无法取消自己的管理员权限'),
  928. duration: Duration(seconds: 2)),
  929. );
  930. return;
  931. }
  932. widget.onSave();
  933. },
  934. child: Container(
  935. width: double.infinity,
  936. padding: const EdgeInsets.symmetric(vertical: 14),
  937. decoration: BoxDecoration(
  938. color: AppColors.primary,
  939. borderRadius: BorderRadius.circular(22),
  940. ),
  941. child: Text(
  942. l10n.get('confirmSave'),
  943. textAlign: TextAlign.center,
  944. style: const TextStyle(
  945. fontSize: 16,
  946. fontWeight: FontWeight.w600,
  947. color: Colors.white,
  948. ),
  949. ),
  950. ),
  951. ),
  952. );
  953. }
  954. }
  955. class _ChangeLog {
  956. final String time;
  957. final String operator;
  958. final String summary;
  959. const _ChangeLog({
  960. required this.time,
  961. required this.operator,
  962. required this.summary,
  963. });
  964. }