import 'package:flutter/material.dart'; import '../../api/oa_client.dart'; import '../../device/device_bridge.dart'; import '../../labels.dart'; import '../../nav/open_module.dart'; import '../../pages/module_list_page.dart' show RestShell, shellFor; import '../../session/session.dart'; import '../desk_theme.dart'; import '../widgets/desk_data_table.dart'; import '../widgets/desk_widgets.dart'; import 'desk_attendance_page.dart'; import 'desk_hr_apply_page.dart'; import 'desk_legal_page.dart'; import 'desk_office_list_page.dart'; import 'desk_record_detail_page.dart'; import 'desk_work_assign_page.dart'; typedef DeskOpenTab = void Function(String key, String title, IconData icon, Color color, Widget page); class DeskWorkbenchPage extends StatefulWidget { const DeskWorkbenchPage({super.key, required this.session, required this.api, required this.onOpenTab}); final SessionStore session; final OaClient api; final DeskOpenTab onOpenTab; @override State createState() => _DeskWorkbenchPageState(); } class _DeskWorkbenchPageState extends State { Map _ov = {}; String _greet = ''; bool _loading = true; String _q = ''; String _cat = 'all'; @override void initState() { super.initState(); _load(); } Future _load() async { try { final ov = await widget.api.get('/office/overview'); String greet = ''; try { final w = await widget.api.get('/office/weather'); if (w is Map) greet = '${w['greeting'] ?? w['text'] ?? ''}'; } catch (_) {} if (!mounted) return; setState(() { _ov = Map.from(ov as Map? ?? {}); _greet = greet; _loading = false; }); } catch (_) { if (mounted) setState(() => _loading = false); } } bool _match(String name) => _q.isEmpty || name.contains(_q); void _open(String key, String title, IconData icon, Color color, Widget page) { widget.onOpenTab(key, title, icon, color, page); } List<_App> _personalApps() { return [ if (_match('待办')) _App('todos', '待办', '处理待办事项与提醒', Icons.task_alt, const Color(0xFFFA9D3B), DeskOfficeListPage(api: widget.api, title: '待办', path: '/office/todos', buckets: const [('OPEN', '待办'), ('DONE', '已办'), ('', '全部')], defaultBucket: 'OPEN')), if (_match('审批')) _App('approvals', '待审批', '审批各类办公申请', Icons.fact_check, kDeskBind, DeskOfficeListPage(api: widget.api, title: '待我审批', path: '/office/approvals', buckets: const [('PENDING', '待审批'), ('APPROVED', '已通过'), ('REJECTED', '已驳回'), ('', '全部')], defaultBucket: 'PENDING')), if (_match('申请')) _App('flow', '我的申请', '查看我发起的申请', Icons.assignment_outlined, const Color(0xFF6267F2), DeskOfficeListPage(api: widget.api, title: '我的申请', path: '/office/flow', buckets: const [('', '全部'), ('pending', '待审批'), ('approved', '已通过'), ('rejected', '已驳回')], defaultBucket: '')), if (_match('日程')) _App('calendar', '日程', '会议与日程安排', Icons.calendar_month, const Color(0xFF10AEFF), DeskOfficeListPage( api: widget.api, title: '我的日程', path: '/office/calendar', columns: const ['标题', '时间', '地点'], rowBuilder: (r) => ['${r['title'] ?? ''}', fmtTime(r['startAt'] ?? r['createdAt']), '${r['location'] ?? ''}'], )), if (_match('安排')) _App('work', '工作安排', '任务分配与跟进', Icons.event_note, const Color(0xFF00B578), DeskWorkAssignPage(session: widget.session, api: widget.api)), if (_match('打卡') || _match('考勤')) _App('attendance', '考勤打卡', '上下班打卡记录', Icons.access_time_filled, const Color(0xFF267EF0), DeskAttendancePage(api: widget.api)), if (_match('人事')) _App('hr', '人事申请', '请假、加班、外出等', Icons.beach_access, const Color(0xFF8B5CF6), DeskHrApplyPage(api: widget.api)), ]; } List<_App> _menuApps() { final out = <_App>[]; for (final m in widget.session.menus) { if (m.name == '工作台' || m.name == '个人办公') continue; if (m.name == '系统设置' || m.code.startsWith('system')) continue; if (m.children.isNotEmpty) { for (var i = 0; i < m.children.length; i++) { final c = m.children[i]; if (_skip(c)) continue; if (!(_q.isEmpty || c.name.contains(_q) || m.name.contains(_q))) continue; out.add(_App(c.code, c.name, m.name, iconFor(c.code, c.name), colorFor(c.code, c.name, i), _pageFor(c))); } } else if (!_skip(m) && _match(m.name)) { out.add(_App(m.code, m.name, '业务应用', iconFor(m.code, m.name), colorFor(m.code, m.name), _pageFor(m))); } } return out; } bool _skip(MenuNode n) { const names = {'待办', '待审批', '我的申请', '日程', '公告', '公司公告', '工作安排', '人事申请', '工作台', '个人办公', '消息', '通讯录', '员工通讯', '发送短信', '系统设置'}; if (n.code.startsWith('office:') || n.code.startsWith('system')) return true; if (n.code.contains('sms') || n.name.contains('短信')) return true; if (n.name.contains('公告') || n.name == '员工通讯') return true; return names.contains(n.name); } String _legalKind(MenuNode n) { final p = '${n.path ?? ''} ${n.code}'.toLowerCase(); if (p.contains('privacy') || p.contains('隐私')) return 'privacy'; return 'user-agreement'; } Widget _pageFor(MenuNode n) { if (n.code.contains('legal') || (n.path ?? '').startsWith('/legal')) { return DeskLegalPage(api: widget.api, kind: _legalKind(n)); } if (n.code == 'office:assign' || n.name.contains('工作安排')) { return DeskWorkAssignPage(session: widget.session, api: widget.api); } final shell = shellFor(n.code, n.path) ?? _shellFromPath(n.path); if (shell != null) { return DeskModuleListPage(api: widget.api, title: n.name, shell: shell); } return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text('${n.name} 暂未配置桌面入口', style: const TextStyle(color: kDeskMute)), if ((n.path ?? '').startsWith('http')) TextButton( onPressed: () => DeviceBridge.openUrl(n.path!), child: const Text('在浏览器打开'), ), ], ), ); } RestShell? _shellFromPath(String? path) { final p = (path ?? '').trim(); if (p.isEmpty || !p.startsWith('/') || p.contains(':')) return null; const blocked = [ '/office/overview', '/office/todos', '/office/approvals', '/office/flow', '/office/calendar', '/office/weather', '/office/geo', '/auth', '/im', '/system', '/push', '/files', '/health', ]; if (blocked.any((b) => p.startsWith(b))) return null; return RestShell(p); } List<_App> get _shown { final personal = _personalApps(); final menu = _menuApps(); if (_cat == 'personal') return personal; if (_cat == 'biz') return menu; return [...personal, ...menu]; } @override Widget build(BuildContext context) { final width = MediaQuery.sizeOf(context).width; final cols = width >= 1100 ? 3 : 2; return ColoredBox( color: kDeskBg, child: Column( children: [ DeskPaneHeader( title: '工作台', actions: [ SizedBox(width: 200, child: DeskSearchField(hint: '搜索应用', onChanged: (v) => setState(() => _q = v.trim()))), const SizedBox(width: 8), TextButton(onPressed: _load, child: const Text('刷新')), ], bottom: Padding( padding: const EdgeInsets.fromLTRB(14, 0, 14, 10), child: DeskFilterChips( value: _cat, onChanged: (v) => setState(() => _cat = v), items: const [('all', '全部应用'), ('personal', '个人办公'), ('biz', '业务模块')], ), ), ), if (_loading) const LinearProgressIndicator(minHeight: 2, color: kDeskBind), Expanded( child: ListView( padding: const EdgeInsets.fromLTRB(16, 14, 16, 24), children: [ Container( padding: const EdgeInsets.fromLTRB(18, 16, 18, 16), decoration: BoxDecoration( gradient: const LinearGradient(colors: [Color(0xFFEEF4FF), Color(0xFFF8FBFF)]), borderRadius: BorderRadius.circular(8), border: Border.all(color: const Color(0xFFDCE8FF)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( _greet.isEmpty ? '你好,${widget.session.displayName}' : _greet, style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w700), ), const SizedBox(height: 4), const Text('个人办公与业务入口', style: TextStyle(color: kDeskMute, fontSize: 13)), ], ), ), const SizedBox(height: 14), Row( children: [ _stat('待办', '${_ov['pendingTodos'] ?? 0}', () => _open('todos', '待办', Icons.task_alt, const Color(0xFFFA9D3B), DeskOfficeListPage(api: widget.api, title: '待办', path: '/office/todos', buckets: const [('OPEN', '待办'), ('DONE', '已办'), ('', '全部')], defaultBucket: 'OPEN'))), const SizedBox(width: 10), _stat('待审批', '${_ov['pendingApprovals'] ?? 0}', () => _open('approvals', '待审批', Icons.fact_check, kDeskBind, DeskOfficeListPage(api: widget.api, title: '待我审批', path: '/office/approvals', buckets: const [('PENDING', '待审批'), ('', '全部')], defaultBucket: 'PENDING'))), const SizedBox(width: 10), _stat('我的申请', '${_ov['myPending'] ?? 0}', () => _open('flow', '我的申请', Icons.assignment_outlined, const Color(0xFF6267F2), DeskOfficeListPage(api: widget.api, title: '我的申请', path: '/office/flow'))), const SizedBox(width: 10), _stat('逾期', '${_ov['overdueTodos'] ?? 0}', () => _open('todos', '待办', Icons.task_alt, const Color(0xFFFA9D3B), DeskOfficeListPage(api: widget.api, title: '待办', path: '/office/todos'))), ], ), const SizedBox(height: 18), if (_shown.isEmpty) const Padding(padding: EdgeInsets.only(top: 40), child: Center(child: Text('没有匹配的应用', style: TextStyle(color: kDeskMute)))) else GridView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: cols, mainAxisSpacing: 10, crossAxisSpacing: 10, childAspectRatio: 2.9, ), itemCount: _shown.length, itemBuilder: (_, i) { final a = _shown[i]; return DeskAppCard( title: a.title, desc: a.desc, icon: a.icon, color: a.color, onTap: () => _open(a.key, a.title, a.icon, a.color, a.page), ); }, ), ], ), ), ], ), ); } Widget _stat(String label, String value, VoidCallback onTap) { return Expanded( child: Material( color: Colors.white, borderRadius: BorderRadius.circular(8), child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(8), child: Padding( padding: const EdgeInsets.symmetric(vertical: 14), child: Column( children: [ Text(value, style: const TextStyle(fontSize: 22, fontWeight: FontWeight.w700, color: kDeskBind)), const SizedBox(height: 2), Text(label, style: const TextStyle(fontSize: 12, color: kDeskMute)), ], ), ), ), ), ); } } class _App { _App(this.key, this.title, this.desc, this.icon, this.color, this.page); final String key; final String title; final String desc; final IconData icon; final Color color; final Widget page; } /// 桌面端业务模块列表(表格),不沿用移动端 ModuleListPage。 class DeskModuleListPage extends StatefulWidget { const DeskModuleListPage({super.key, required this.api, required this.title, required this.shell}); final OaClient api; final String title; final RestShell shell; @override State createState() => _DeskModuleListPageState(); } class _DeskModuleListPageState extends State { List> _items = []; bool _loading = true; String _q = ''; @override void initState() { super.initState(); _load(); } Future _load() async { setState(() => _loading = true); try { final data = await widget.api.get(widget.shell.path, query: {'page': '1', 'pageSize': '100', ...?widget.shell.query}); if (!mounted) return; setState(() { _items = asMaps(data); _loading = false; }); } catch (_) { if (mounted) setState(() => _loading = false); } } List> get _shown { if (_q.isEmpty) return _items; return _items.where((e) => pickTitle(e).contains(_q) || pickSub(e).contains(_q)).toList(); } @override Widget build(BuildContext context) { final shown = _shown; return DeskListScaffold( title: widget.title, loading: _loading, actions: [IconButton(onPressed: _load, icon: const Icon(Icons.refresh, size: 20))], filters: DeskSearchField(hint: '搜索', onChanged: (v) => setState(() => _q = v.trim())), body: DeskDataTable( columns: const ['名称', '摘要', '状态'], rows: [ for (final r in shown) [pickTitle(r), zh(pickSub(r)), '${r['status'] ?? ''}'], ], emptyHint: '暂无${widget.title}', onRowTap: (i) { final r = shown[i]; final seed = Map.from(r); if ('${r['id']}'.length >= 8) seed['_fetch'] = '${widget.shell.path}/${r['id']}'; deskOpenRecord(context, widget.api, seed, title: pickTitle(r)); }, ), ); } } String pickTitle(Map r) { for (final k in ['title', 'name', 'code', 'no']) { final v = r[k]; if (v != null && '$v'.isNotEmpty) return '$v'; } return '记录'; } String pickSub(Map r) { for (final k in ['summary', 'remark', 'description', 'partyName', 'projectName']) { final v = r[k]; if (v != null && '$v'.isNotEmpty) return '$v'; } return ''; }