admin_permissions_page.dart 34 KB

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