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
@@ -0,0 +1,166 @@
import 'package:flutter/material.dart';
import '../../api/oa_client.dart';
import '../../app_config.dart';
import '../../ota/updater.dart';
import 'desk_legal_page.dart';
import 'desk_profile_edit_page.dart';
import '../../session/session.dart';
import '../desk_theme.dart';
import '../widgets/desk_widgets.dart';
class DeskProfilePage extends StatefulWidget {
const DeskProfilePage({super.key, required this.session, required this.api});
final SessionStore session;
final OaClient api;
@override
State<DeskProfilePage> createState() => _DeskProfilePageState();
}
class _DeskProfilePageState extends State<DeskProfilePage> {
AppRelease? _rel;
bool _checking = false;
@override
void initState() {
super.initState();
widget.session.addListener(() {
if (mounted) setState(() {});
});
_peek();
}
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) await OtaUpdater(widget.api).prompt(context, rel, manual: true);
} finally {
if (mounted) setState(() => _checking = false);
}
}
@override
Widget build(BuildContext context) {
final s = widget.session;
return ColoredBox(
color: kDeskBg,
child: Column(
children: [
const DeskPaneHeader(title: '我'),
Expanded(
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 560),
child: ListView(
padding: const EdgeInsets.all(24),
children: [
Material(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
child: InkWell(
onTap: () => Navigator.of(context).push(MaterialPageRoute(
builder: (_) => DeskProfileEditPage(session: s, api: widget.api),
)),
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
DeskAvatar(
label: s.displayName,
fileId: '${s.user['avatarFileId'] ?? ''}',
api: widget.api,
size: 56,
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(s.displayName, style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600)),
const SizedBox(height: 4),
Text('${s.user['department'] ?? ''} ${s.user['title'] ?? ''}'.trim(),
style: const TextStyle(color: kDeskMute, fontSize: 13)),
],
),
),
const Icon(Icons.chevron_right, color: kDeskMute),
],
),
),
),
),
const SizedBox(height: 12),
_section([
_row('检查更新', trailing: _rel?.newer == true ? '有新版本' : '已是最新', onTap: _checking ? null : _checkUpdate),
_row('用户协议', onTap: () => Navigator.of(context).push(MaterialPageRoute(builder: (_) => DeskLegalPage(api: widget.api, kind: 'terms')))),
_row('隐私政策', onTap: () => Navigator.of(context).push(MaterialPageRoute(builder: (_) => DeskLegalPage(api: widget.api, kind: 'privacy')))),
]),
const SizedBox(height: 12),
_section([
_row('版本', trailing: 'v${AppConfig.version}'),
]),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: OutlinedButton(
style: OutlinedButton.styleFrom(minimumSize: const Size.fromHeight(40)),
onPressed: _logout,
child: const Text('退出登录', style: TextStyle(color: Color(0xFFFA5151))),
),
),
],
),
),
),
),
],
),
);
}
Widget _section(List<Widget> children) {
return Material(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
clipBehavior: Clip.antiAlias,
child: Column(children: children),
);
}
Widget _row(String title, {String? trailing, VoidCallback? onTap}) {
return Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
child: Row(
children: [
Expanded(child: Text(title, style: const TextStyle(fontSize: 14))),
if (trailing != null) Text(trailing, style: const TextStyle(fontSize: 13, color: kDeskMute)),
if (onTap != null) const Icon(Icons.chevron_right, size: 18, color: kDeskMute),
],
),
),
),
);
}
}