Initial commit: 风影 OA 全栈源码(API/Web/Flutter/IM)
含 Phase 1.1 IM seq 排序、断线重连、多端已读同步与微信式语音转文字 UI。 排除 node_modules、构建产物、安装包与 .env 密钥。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,295 @@
|
||||
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<DesktopWorkbenchPage> createState() => _DesktopWorkbenchPageState();
|
||||
}
|
||||
|
||||
class _DesktopWorkbenchPageState extends State<DesktopWorkbenchPage> {
|
||||
Map<String, dynamic> _ov = {};
|
||||
String _greet = '';
|
||||
bool _loading = true;
|
||||
String _q = '';
|
||||
String _cat = 'all';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_load();
|
||||
}
|
||||
|
||||
Future<void> _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<String, dynamic>.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;
|
||||
}
|
||||
Reference in New Issue
Block a user