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:
2026-09-02 10:04:03 +00:00
commit 76f266645d
507 changed files with 99891 additions and 0 deletions
+116
View File
@@ -0,0 +1,116 @@
import 'package:flutter/material.dart';
import '../api/oa_client.dart';
import '../pages/approvals_page.dart';
import '../pages/calendar_page.dart';
import '../pages/directory_page.dart';
import '../pages/flow_page.dart';
import '../pages/hr_apply_page.dart';
import '../pages/module_list_page.dart';
import '../pages/notices_page.dart';
import '../pages/todos_page.dart';
import '../pages/work_assign_page.dart';
import '../session/session.dart';
void pushPage(BuildContext context, Widget page) {
Navigator.of(context).push(MaterialPageRoute(builder: (_) => page));
}
void openMenuNode(BuildContext context, SessionStore session, OaClient api, MenuNode n) {
if (n.code.startsWith('system') || n.name == '系统设置') return;
if (n.name == '发送短信' || n.code.contains('sms')) return;
if (n.name == '消息' || n.code.contains('office:im') || n.code == 'im') return;
if (n.name == '公司公告' || n.name == '员工通讯' || n.name == '通讯录') return;
switch (n.code) {
case 'office:overview':
return;
case 'office:todos':
pushPage(context, TodosPage(api: api));
return;
case 'office:approvals':
pushPage(context, ApprovalsPage(api: api));
return;
case 'office:flow':
pushPage(context, FlowPage(api: api));
return;
case 'office:calendar':
pushPage(context, CalendarPage(api: api));
return;
case 'office:notices':
pushPage(context, NoticesPage(api: api));
return;
case 'office:assign':
pushPage(context, WorkAssignPage(session: session, api: api));
return;
case 'office:directory':
pushPage(context, DirectoryPage(session: session, api: api));
return;
case 'hr:apply':
case 'office:leave':
pushPage(context, HrApplyPage(api: api));
return;
}
final shell = shellFor(n.code, n.path);
if (shell != null) {
pushPage(context, ModuleListPage(api: api, title: n.name, shell: shell));
return;
}
if (n.children.isNotEmpty) {
showModalBottomSheet<void>(
context: context,
builder: (ctx) => SafeArea(
child: ListView(
shrinkWrap: true,
children: [
for (final c in n.children)
ListTile(
title: Text(c.name),
onTap: () {
Navigator.pop(ctx);
openMenuNode(context, session, api, c);
},
),
],
),
),
);
}
}
IconData iconFor(String code, String name) {
final k = '$code $name';
if (k.contains('todo') || name.contains('待办')) return Icons.task_alt;
if (k.contains('approv') || name.contains('审批')) return Icons.fact_check_outlined;
if (name.contains('申请') || k.contains('flow')) return Icons.assignment_outlined;
if (name.contains('日程') || k.contains('calendar')) return Icons.calendar_month_outlined;
if (name.contains('公告') || k.contains('notice')) return Icons.campaign_outlined;
if (name.contains('安排') || k.contains('assign')) return Icons.event_note_outlined;
if (name.contains('通讯') || k.contains('directory')) return Icons.contacts_outlined;
if (name.contains('人事') || name.contains('员工') || k.contains('hr') || k.contains('employee')) return Icons.badge_outlined;
if (name.contains('投标') || k.contains('bid')) return Icons.gavel_outlined;
if (name.contains('合同') || k.contains('contract')) return Icons.description_outlined;
if (name.contains('用章') || name.contains('证照') || k.contains('seal')) return Icons.verified_outlined;
if (name.contains('资产') || k.contains('asset')) return Icons.devices_other_outlined;
if (name.contains('项目') || k.contains('project')) return Icons.account_tree_outlined;
if (name.contains('财务') || name.contains('报销') || name.contains('付款') || k.contains('finance') || k.contains('expense')) {
return Icons.account_balance_wallet_outlined;
}
if (name.contains('客户') || name.contains('往来') || k.contains('party')) return Icons.handshake_outlined;
if (name.contains('报表') || k.contains('report')) return Icons.bar_chart_outlined;
if (name.contains('请假') || name.contains('加班')) return Icons.beach_access_outlined;
return Icons.apps_outlined;
}
Color colorFor(String code, String name, [int i = 0]) {
const palette = [
Color(0xFF267EF0),
Color(0xFF07C160),
Color(0xFFFA9D3B),
Color(0xFF6267F2),
Color(0xFF10AEFF),
Color(0xFFE75D5D),
Color(0xFF00B578),
Color(0xFF8B5CF6),
];
return palette[(code.hashCode.abs() + name.hashCode.abs() + i) % palette.length];
}