admin_permissions_page.dart 34 KB

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