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,281 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../api/oa_client.dart';
|
||||
import '../nav/open_module.dart';
|
||||
import '../ota/updater.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/todos_page.dart';
|
||||
import '../pages/work_assign_page.dart';
|
||||
import '../session/session.dart';
|
||||
import '../theme.dart';
|
||||
import '../widgets/wecom.dart';
|
||||
|
||||
class WorkbenchPage extends StatefulWidget {
|
||||
const WorkbenchPage({super.key, required this.session, required this.api, this.embedded = true});
|
||||
final SessionStore session;
|
||||
final OaClient api;
|
||||
final bool embedded;
|
||||
|
||||
@override
|
||||
State<WorkbenchPage> createState() => _WorkbenchPageState();
|
||||
}
|
||||
|
||||
class _WorkbenchPageState extends State<WorkbenchPage> {
|
||||
Map<String, dynamic> _ov = {};
|
||||
String _greet = '';
|
||||
AppRelease? _rel;
|
||||
bool _loading = true;
|
||||
String _q = '';
|
||||
|
||||
@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 (_) {}
|
||||
AppRelease? rel;
|
||||
try {
|
||||
rel = await OtaUpdater(widget.api).fetch();
|
||||
} catch (_) {}
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_ov = Map<String, dynamic>.from(ov as Map? ?? {});
|
||||
_greet = greet;
|
||||
_rel = rel;
|
||||
_loading = false;
|
||||
});
|
||||
} catch (_) {
|
||||
if (mounted) setState(() => _loading = false);
|
||||
}
|
||||
}
|
||||
|
||||
bool _match(String name) => _q.isEmpty || name.contains(_q);
|
||||
|
||||
bool _skipGroup(MenuNode m) {
|
||||
if (m.name == '工作台' || m.name == '个人办公' || m.code == 'office:overview') return true;
|
||||
if (m.name == '系统设置' || m.code.startsWith('system')) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool _skipChild(MenuNode n) {
|
||||
const names = {
|
||||
'待办',
|
||||
'待审批',
|
||||
'我的申请',
|
||||
'日程',
|
||||
'公告',
|
||||
'公司公告',
|
||||
'工作安排',
|
||||
'人事申请',
|
||||
'工作台',
|
||||
'个人办公',
|
||||
'消息',
|
||||
'通讯录',
|
||||
'员工通讯',
|
||||
'发送短信',
|
||||
'人事设置',
|
||||
'数据字典',
|
||||
'权限设置',
|
||||
'编号规则',
|
||||
'短信服务',
|
||||
'消息通道',
|
||||
'手机端升级',
|
||||
'操作日志',
|
||||
'系统设置',
|
||||
};
|
||||
if (n.code.startsWith('office:')) return true;
|
||||
if (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);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final body = RefreshIndicator(
|
||||
onRefresh: _load,
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.only(bottom: 24),
|
||||
children: [
|
||||
if (_loading) const LinearProgressIndicator(minHeight: 2, color: kBind),
|
||||
if (_rel != null && _rel!.newer)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 12, 12, 0),
|
||||
child: Material(
|
||||
color: const Color(0xFFE8F3FF),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: InkWell(
|
||||
onTap: () => OtaUpdater(widget.api).prompt(context, _rel!, manual: true),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(14, 12, 14, 12),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.system_update_alt, color: kBind),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('发现新版本 ${_rel!.version}', style: const TextStyle(fontWeight: FontWeight.w600, color: kInk)),
|
||||
Text(_rel!.changelog.isEmpty ? '点击升级到最新手机版' : _rel!.changelog, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 12, color: kMute)),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Text('升级', style: TextStyle(color: kBind, fontWeight: FontWeight.w600)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
|
||||
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: 4),
|
||||
const Text('工作台', style: TextStyle(color: kMute, fontSize: 13)),
|
||||
],
|
||||
),
|
||||
),
|
||||
GroupCard(
|
||||
title: '待处理',
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8, 4, 8, 14),
|
||||
child: Row(
|
||||
children: [
|
||||
_stat('待办', '${_ov['pendingTodos'] ?? 0}', () => pushPage(context, TodosPage(api: widget.api))),
|
||||
_stat('待审批', '${_ov['pendingApprovals'] ?? 0}', () => pushPage(context, ApprovalsPage(api: widget.api))),
|
||||
_stat('我的申请', '${_ov['myPending'] ?? 0}', () => pushPage(context, FlowPage(api: widget.api))),
|
||||
_stat('逾期', '${_ov['overdueTodos'] ?? 0}', () => pushPage(context, TodosPage(api: widget.api))),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_match('待办') || _match('审批') || _match('申请') || _match('日程') || _match('人事'))
|
||||
GroupCard(
|
||||
title: '个人办公',
|
||||
child: AppGrid(
|
||||
items: [
|
||||
if (_match('待办'))
|
||||
AppGridItem(label: '待办', icon: Icons.task_alt, color: const Color(0xFFFA9D3B), onTap: () => pushPage(context, TodosPage(api: widget.api))),
|
||||
if (_match('审批'))
|
||||
AppGridItem(label: '待审批', icon: Icons.fact_check, color: kBind, onTap: () => pushPage(context, ApprovalsPage(api: widget.api))),
|
||||
if (_match('申请'))
|
||||
AppGridItem(label: '我的申请', icon: Icons.assignment_outlined, color: const Color(0xFF6267F2), onTap: () => pushPage(context, FlowPage(api: widget.api))),
|
||||
if (_match('日程'))
|
||||
AppGridItem(label: '日程', icon: Icons.calendar_month, color: const Color(0xFF10AEFF), onTap: () => pushPage(context, CalendarPage(api: widget.api))),
|
||||
if (_match('安排'))
|
||||
AppGridItem(label: '工作安排', icon: Icons.event_note, color: const Color(0xFF00B578), onTap: () => pushPage(context, WorkAssignPage(session: widget.session, api: widget.api))),
|
||||
if (_match('打卡') || _match('考勤'))
|
||||
AppGridItem(label: '考勤打卡', icon: Icons.access_time_filled, color: const Color(0xFF267EF0), onTap: () => pushPage(context, AttendancePage(api: widget.api))),
|
||||
if (_match('人事') || _match('请假') || _match('加班'))
|
||||
AppGridItem(label: '人事申请', icon: Icons.beach_access, color: const Color(0xFF8B5CF6), onTap: () => pushPage(context, HrApplyPage(api: widget.api))),
|
||||
],
|
||||
),
|
||||
),
|
||||
..._menuGroups(),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (!widget.embedded) {
|
||||
return Scaffold(appBar: AppBar(title: const Text('工作台')), body: body);
|
||||
}
|
||||
return ColoredBox(
|
||||
color: kPaper,
|
||||
child: Column(
|
||||
children: [
|
||||
WxHeader(
|
||||
title: '工作台',
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
final c = TextEditingController(text: _q);
|
||||
final v = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('搜索应用'),
|
||||
content: TextField(controller: c, autofocus: true, decoration: const InputDecoration(hintText: '待办、投标、报销…')),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx, ''), child: const Text('清除')),
|
||||
FilledButton(onPressed: () => Navigator.pop(ctx, c.text.trim()), child: const Text('搜索')),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (v != null) setState(() => _q = v);
|
||||
},
|
||||
icon: const Icon(Icons.search, color: kInk),
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(child: body),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _menuGroups() {
|
||||
final out = <Widget>[];
|
||||
for (final m in widget.session.menus) {
|
||||
if (_skipGroup(m)) continue;
|
||||
if (m.children.isNotEmpty) {
|
||||
final items = <AppGridItem>[];
|
||||
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;
|
||||
items.add(AppGridItem(
|
||||
label: c.name,
|
||||
icon: iconFor(c.code, c.name),
|
||||
color: colorFor(c.code, c.name, i),
|
||||
onTap: () => openMenuNode(context, widget.session, widget.api, c),
|
||||
));
|
||||
}
|
||||
if (items.isEmpty) continue;
|
||||
out.add(GroupCard(title: m.name, child: AppGrid(items: items)));
|
||||
} else if (!_skipChild(m) && _match(m.name)) {
|
||||
out.add(GroupCard(
|
||||
title: m.name,
|
||||
child: AppGrid(items: [
|
||||
AppGridItem(
|
||||
label: m.name,
|
||||
icon: iconFor(m.code, m.name),
|
||||
color: colorFor(m.code, m.name),
|
||||
onTap: () => openMenuNode(context, widget.session, widget.api, m),
|
||||
),
|
||||
]),
|
||||
));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Widget _stat(String label, String value, VoidCallback onTap) {
|
||||
return Expanded(
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Column(
|
||||
children: [
|
||||
Text(value, style: const TextStyle(fontSize: 22, fontWeight: FontWeight.w700, color: kBind)),
|
||||
const SizedBox(height: 2),
|
||||
Text(label, style: const TextStyle(fontSize: 12, color: kMute)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user