import 'package:flutter/material.dart'; import '../api/oa_client.dart'; import '../nav/open_module.dart'; import '../pages/approvals_page.dart'; import '../pages/attendance_page.dart'; import '../pages/calendar_page.dart'; import '../pages/flow_page.dart'; import '../pages/hr_apply_page.dart'; import '../pages/module_list_page.dart'; import '../pages/todos_page.dart'; import '../pages/work_assign_page.dart'; import '../session/session.dart'; import '../theme.dart'; import '../widgets/desktop_ui.dart'; import '../widgets/wecom.dart'; typedef DesktopOpenTab = void Function(String key, String title, IconData icon, Color color, Widget page); class DesktopWorkbenchPage extends StatefulWidget { const DesktopWorkbenchPage({ super.key, required this.session, required this.api, required this.onOpenTab, }); final SessionStore session; final OaClient api; final DesktopOpenTab onOpenTab; @override State createState() => _DesktopWorkbenchPageState(); } class _DesktopWorkbenchPageState 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<_DeskApp> _personalApps() { return [ if (_match('待办')) _DeskApp('todos', '待办', '处理待办事项与提醒', Icons.task_alt, const Color(0xFFFA9D3B), TodosPage(api: widget.api)), if (_match('审批')) _DeskApp('approvals', '待审批', '审批各类办公申请', Icons.fact_check, kBind, ApprovalsPage(api: widget.api)), if (_match('申请')) _DeskApp('flow', '我的申请', '查看我发起的申请', Icons.assignment_outlined, const Color(0xFF6267F2), FlowPage(api: widget.api)), if (_match('日程')) _DeskApp('calendar', '日程', '会议与日程安排', Icons.calendar_month, const Color(0xFF10AEFF), CalendarPage(api: widget.api)), if (_match('安排')) _DeskApp('work', '工作安排', '任务分配与跟进', Icons.event_note, const Color(0xFF00B578), WorkAssignPage(session: widget.session, api: widget.api)), if (_match('打卡') || _match('考勤')) _DeskApp('attendance', '考勤打卡', '上下班打卡记录', Icons.access_time_filled, const Color(0xFF267EF0), AttendancePage(api: widget.api)), if (_match('人事')) _DeskApp('hr', '人事申请', '请假、加班、外出等', Icons.beach_access, const Color(0xFF8B5CF6), HrApplyPage(api: widget.api)), ]; } List<_DeskApp> _menuApps() { final out = <_DeskApp>[]; for (final m in widget.session.menus) { if (m.name == '工作台' || m.name == '个人办公' || m.code == 'office:overview') 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 (_skipChild(c)) continue; if (!(_q.isEmpty || c.name.contains(_q) || m.name.contains(_q))) continue; out.add(_DeskApp( c.code, c.name, m.name, iconFor(c.code, c.name), colorFor(c.code, c.name, i), _pageFor(c), )); } } else if (!_skipChild(m) && _match(m.name)) { out.add(_DeskApp(m.code, m.name, '业务应用', iconFor(m.code, m.name), colorFor(m.code, m.name), _pageFor(m))); } } return out; } bool _skipChild(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); } Widget _pageFor(MenuNode n) { final shell = shellFor(n.code, n.path); if (shell != null) { return ModuleListPage(api: widget.api, title: n.name, shell: shell); } return Center(child: Text('${n.name} 暂未配置桌面入口', style: const TextStyle(color: kMute))); } List<_DeskApp> get _shownApps { 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 >= 1200 ? 3 : 2; return ColoredBox( color: kDeskBg, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ DesktopPaneHeader( title: '工作台', actions: [ SizedBox(width: 220, child: DesktopSearchBar(hint: '搜索应用', onChanged: (v) => setState(() => _q = v.trim()))), const SizedBox(width: 8), OutlinedButton(onPressed: _load, child: const Text('刷新')), ], bottom: Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 10), child: Row( children: [ _catChip('all', '全部应用'), const SizedBox(width: 8), _catChip('personal', '个人办公'), const SizedBox(width: 8), _catChip('biz', '业务模块'), ], ), ), ), if (_loading) const LinearProgressIndicator(minHeight: 2, color: kBind), Expanded( child: RefreshIndicator( onRefresh: _load, child: ListView( padding: const EdgeInsets.fromLTRB(20, 16, 20, 24), children: [ Container( padding: const EdgeInsets.fromLTRB(20, 18, 20, 18), decoration: BoxDecoration( gradient: const LinearGradient(colors: [Color(0xFFEEF4FF), Color(0xFFF8FBFF)]), borderRadius: BorderRadius.circular(12), border: Border.all(color: const Color(0xFFDCE8FF)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( _greet.isEmpty ? '你好,${widget.session.displayName}' : _greet, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w700, color: kInk), ), const SizedBox(height: 6), const Text('工作台 · 个人办公与业务入口', style: TextStyle(color: kMute, fontSize: 13)), ], ), ), const SizedBox(height: 16), Row( children: [ _statCard('待办', '${_ov['pendingTodos'] ?? 0}', () => _open('todos', '待办', Icons.task_alt, const Color(0xFFFA9D3B), TodosPage(api: widget.api))), const SizedBox(width: 12), _statCard('待审批', '${_ov['pendingApprovals'] ?? 0}', () => _open('approvals', '待审批', Icons.fact_check, kBind, ApprovalsPage(api: widget.api))), const SizedBox(width: 12), _statCard('我的申请', '${_ov['myPending'] ?? 0}', () => _open('flow', '我的申请', Icons.assignment_outlined, const Color(0xFF6267F2), FlowPage(api: widget.api))), const SizedBox(width: 12), _statCard('逾期', '${_ov['overdueTodos'] ?? 0}', () => _open('todos', '待办', Icons.task_alt, const Color(0xFFFA9D3B), TodosPage(api: widget.api))), ], ), const SizedBox(height: 20), if (_shownApps.isEmpty) const Padding( padding: EdgeInsets.only(top: 48), child: Center(child: Text('没有匹配的应用', style: TextStyle(color: kMute))), ) else GridView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: cols, mainAxisSpacing: 12, crossAxisSpacing: 12, childAspectRatio: 2.8, ), itemCount: _shownApps.length, itemBuilder: (_, i) { final a = _shownApps[i]; return DesktopAppCard( label: a.title, desc: a.desc, icon: a.icon, color: a.color, onTap: () => _open(a.key, a.title, a.icon, a.color, a.page), ); }, ), ], ), ), ), ], ), ); } Widget _catChip(String id, String label) { final on = _cat == id; return GestureDetector( onTap: () => setState(() => _cat = id), child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: on ? kDeskActive : Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all(color: on ? const Color(0xFFBFD7FF) : kLine), ), child: Text(label, style: TextStyle(fontSize: 13, color: on ? kBind : kMute, fontWeight: on ? FontWeight.w600 : FontWeight.w400)), ), ); } Widget _statCard(String label, String value, VoidCallback onTap) { return Expanded( child: Material( color: Colors.white, borderRadius: BorderRadius.circular(10), child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(10), child: Padding( padding: const EdgeInsets.symmetric(vertical: 16), child: Column( children: [ Text(value, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.w700, color: kBind)), const SizedBox(height: 4), Text(label, style: const TextStyle(fontSize: 12, color: kMute)), ], ), ), ), ), ); } } class _DeskApp { _DeskApp(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; }