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,267 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../api/oa_client.dart';
|
||||
import '../device/push_bridge.dart';
|
||||
import '../app_config.dart';
|
||||
import '../labels.dart';
|
||||
import '../ota/updater.dart';
|
||||
import '../pages/legal_page.dart';
|
||||
import '../pages/profile_page.dart';
|
||||
import '../session/session.dart';
|
||||
import '../theme.dart';
|
||||
import '../widgets/beian_footer.dart';
|
||||
import '../widgets/desktop_ui.dart';
|
||||
import '../widgets/wecom.dart';
|
||||
|
||||
class MinePage extends StatefulWidget {
|
||||
const MinePage({super.key, required this.session, required this.api, this.desktopStyle = false});
|
||||
final SessionStore session;
|
||||
final OaClient api;
|
||||
final bool desktopStyle;
|
||||
|
||||
@override
|
||||
State<MinePage> createState() => _MinePageState();
|
||||
}
|
||||
|
||||
class _MinePageState extends State<MinePage> {
|
||||
AppRelease? _rel;
|
||||
bool _checking = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.session.addListener(_onSession);
|
||||
_peek();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.session.removeListener(_onSession);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onSession() {
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
Future<void> _peek() async {
|
||||
final rel = await OtaUpdater(widget.api).fetch();
|
||||
if (mounted) setState(() => _rel = rel);
|
||||
}
|
||||
|
||||
Future<void> _logout() async {
|
||||
try {
|
||||
await widget.api
|
||||
.post('/auth/logout', {'refreshToken': widget.session.refreshToken});
|
||||
} catch (_) {}
|
||||
await widget.session.clear();
|
||||
}
|
||||
|
||||
Future<void> _checkUpdate() async {
|
||||
setState(() => _checking = true);
|
||||
try {
|
||||
final rel = await OtaUpdater(widget.api).fetch();
|
||||
if (!mounted) return;
|
||||
setState(() => _rel = rel);
|
||||
if (rel == null) {
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(const SnackBar(content: Text('暂时无法检查更新')));
|
||||
return;
|
||||
}
|
||||
await OtaUpdater(widget.api).prompt(context, rel, manual: true);
|
||||
} finally {
|
||||
if (mounted) setState(() => _checking = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = widget.session;
|
||||
final avatarId = '${s.user['avatarFileId'] ?? ''}';
|
||||
return ColoredBox(
|
||||
color: widget.desktopStyle ? kDeskBg : kPaper,
|
||||
child: Column(
|
||||
children: [
|
||||
widget.desktopStyle ? const DesktopPaneHeader(title: '我') : const WxHeader(title: '我'),
|
||||
Expanded(
|
||||
child: widget.desktopStyle
|
||||
? Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 720),
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.fromLTRB(20, 16, 20, 24),
|
||||
children: _mineItems(s, avatarId),
|
||||
),
|
||||
),
|
||||
)
|
||||
: ListView(
|
||||
children: _mineItems(s, avatarId),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _mineItems(SessionStore s, String avatarId) {
|
||||
return [
|
||||
const SizedBox(height: 12),
|
||||
Material(
|
||||
color: Colors.white,
|
||||
child: InkWell(
|
||||
onTap: () => Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (_) => ProfilePage(session: s, api: widget.api),
|
||||
)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 16),
|
||||
child: Row(
|
||||
children: [
|
||||
SquareAvatar(
|
||||
label: s.displayName,
|
||||
size: 64,
|
||||
fileId: avatarId.isEmpty ? null : avatarId,
|
||||
api: widget.api,
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(s.displayName,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600)),
|
||||
const SizedBox(height: 4),
|
||||
Text('账号 ${s.user['username'] ?? ''}',
|
||||
style: const TextStyle(
|
||||
color: kMute, fontSize: 13)),
|
||||
Text(s.roles.map(zh).join(' · '),
|
||||
style: const TextStyle(
|
||||
color: kMute, fontSize: 12)),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Icon(Icons.chevron_right,
|
||||
color: Color(0xFFC0C0C0)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
CellGroup(
|
||||
children: [
|
||||
if (!widget.desktopStyle)
|
||||
Cell(
|
||||
title: '后台运行与通知',
|
||||
subtitle: '允许自启动和后台,消息才能及时送达',
|
||||
leading: const SquareAvatar(
|
||||
label: '通',
|
||||
color: kBind,
|
||||
icon: Icons.notifications_active_outlined,
|
||||
size: 32),
|
||||
onTap: () async {
|
||||
await PushBridge.requestBattery();
|
||||
await PushBridge.openOemKeepAlive();
|
||||
},
|
||||
),
|
||||
if (!widget.desktopStyle)
|
||||
Cell(
|
||||
title: '检查更新',
|
||||
subtitle: _rel != null && _rel!.newer
|
||||
? '有新版本 ${_rel!.version}'
|
||||
: '当前 ${AppConfig.version} (${AppConfig.build})',
|
||||
leading: const SquareAvatar(
|
||||
label: '升',
|
||||
color: kBind,
|
||||
icon: Icons.system_update_alt,
|
||||
size: 32),
|
||||
trailing: _checking
|
||||
? const SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(strokeWidth: 2))
|
||||
: (_rel != null && _rel!.newer
|
||||
? const UnreadBadge(1, dot: true)
|
||||
: null),
|
||||
onTap: _checkUpdate,
|
||||
),
|
||||
if (widget.desktopStyle)
|
||||
Cell(
|
||||
title: '检查更新',
|
||||
subtitle: _rel != null && _rel!.newer
|
||||
? '有新版本 ${_rel!.version}'
|
||||
: '当前 ${AppConfig.version} (${AppConfig.build})',
|
||||
leading: const SquareAvatar(
|
||||
label: '升',
|
||||
color: kBind,
|
||||
icon: Icons.system_update_alt,
|
||||
size: 32),
|
||||
trailing: _checking
|
||||
? const SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(strokeWidth: 2))
|
||||
: (_rel != null && _rel!.newer
|
||||
? const UnreadBadge(1, dot: true)
|
||||
: null),
|
||||
onTap: _checkUpdate,
|
||||
),
|
||||
Cell(
|
||||
title: '用户协议',
|
||||
leading: const SquareAvatar(
|
||||
label: '协',
|
||||
color: kBind,
|
||||
icon: Icons.article_outlined,
|
||||
size: 32),
|
||||
onTap: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => LegalPage(
|
||||
api: widget.api, kind: 'user-agreement')),
|
||||
),
|
||||
),
|
||||
Cell(
|
||||
title: '隐私政策',
|
||||
leading: const SquareAvatar(
|
||||
label: '私',
|
||||
color: kBind,
|
||||
icon: Icons.privacy_tip_outlined,
|
||||
size: 32),
|
||||
onTap: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) =>
|
||||
LegalPage(api: widget.api, kind: 'privacy')),
|
||||
),
|
||||
showLine: false,
|
||||
),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 8, 12, 8),
|
||||
child: Material(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: InkWell(
|
||||
onTap: _logout,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: const SizedBox(
|
||||
height: 48,
|
||||
child: Center(
|
||||
child: Text('退出登录',
|
||||
style:
|
||||
TextStyle(fontSize: 16, color: kDanger))),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'${AppConfig.brand}\n${AppConfig.version} (${AppConfig.build})',
|
||||
textAlign: TextAlign.center,
|
||||
style:
|
||||
const TextStyle(color: kMute, fontSize: 12, height: 1.5),
|
||||
),
|
||||
const BeianFooter(),
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user