76f266645d
含 Phase 1.1 IM seq 排序、断线重连、多端已读同步与微信式语音转文字 UI。 排除 node_modules、构建产物、安装包与 .env 密钥。 Co-authored-by: Cursor <cursoragent@cursor.com>
282 lines
11 KiB
Dart
282 lines
11 KiB
Dart
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)),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|