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,162 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../api/oa_client.dart';
|
||||
import '../im/chat_local_store.dart';
|
||||
import '../nav/notice_route.dart';
|
||||
import '../session/session.dart';
|
||||
import '../theme.dart';
|
||||
import '../widgets/wecom.dart';
|
||||
|
||||
class SystemNoticePage extends StatefulWidget {
|
||||
const SystemNoticePage(
|
||||
{super.key,
|
||||
required this.session,
|
||||
required this.api,
|
||||
this.conversationId});
|
||||
final SessionStore session;
|
||||
final OaClient api;
|
||||
final String? conversationId;
|
||||
@override
|
||||
State<SystemNoticePage> createState() => _SystemNoticePageState();
|
||||
}
|
||||
|
||||
class _SystemNoticePageState extends State<SystemNoticePage> {
|
||||
List<Map<String, dynamic>> _items = [];
|
||||
bool _loading = true;
|
||||
|
||||
Map<String, dynamic> _metaOf(Map<String, dynamic> row) {
|
||||
final raw = row['meta'];
|
||||
if (raw is Map) return Map<String, dynamic>.from(raw);
|
||||
if (raw is String && raw.isNotEmpty) {
|
||||
try {
|
||||
final decoded = jsonDecode(raw);
|
||||
if (decoded is Map) return Map<String, dynamic>.from(decoded);
|
||||
} catch (_) {}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_load();
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
try {
|
||||
final raw = await widget.api.get('/im/messages', query: {
|
||||
'peerId': '0',
|
||||
if (widget.conversationId != null)
|
||||
'conversationId': widget.conversationId!,
|
||||
});
|
||||
final remote = asMaps(raw);
|
||||
final cid = widget.conversationId ??
|
||||
(raw is Map ? '${raw['conversationId'] ?? ''}' : '');
|
||||
final local = await ChatLocalStore.load(cid);
|
||||
final byKey = <String, Map<String, dynamic>>{};
|
||||
for (final e in [...local, ...remote]) {
|
||||
final fp = '${e['fingerprint'] ?? ''}';
|
||||
byKey[fp.isNotEmpty ? 'fp:$fp' : 'id:${e['id'] ?? ''}'] = e;
|
||||
}
|
||||
final merged = byKey.values.toList()
|
||||
..sort((a, b) =>
|
||||
'${a['createdAt'] ?? ''}'.compareTo('${b['createdAt'] ?? ''}'));
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_items = merged;
|
||||
_loading = false;
|
||||
});
|
||||
await ChatLocalStore.save(cid, merged);
|
||||
if (widget.conversationId != null) {
|
||||
await widget.api
|
||||
.post('/im/read', {'conversationId': widget.conversationId});
|
||||
}
|
||||
} catch (_) {
|
||||
if (mounted) setState(() => _loading = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF5F6F8),
|
||||
appBar: AppBar(title: const Text('系统通知'), backgroundColor: Colors.white),
|
||||
body: _loading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: RefreshIndicator(
|
||||
onRefresh: _load,
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.fromLTRB(14, 14, 14, 28),
|
||||
itemCount: _items.isEmpty ? 1 : _items.length,
|
||||
itemBuilder: (context, index) {
|
||||
if (_items.isEmpty)
|
||||
return const Padding(
|
||||
padding: EdgeInsets.only(top: 100),
|
||||
child: Center(
|
||||
child: Text('暂无系统通知',
|
||||
style: TextStyle(color: kMute))));
|
||||
final r = _items[index];
|
||||
final title = '${r['title'] ?? r['body'] ?? '系统通知'}';
|
||||
final body = '${r['body'] ?? ''}';
|
||||
final time = '${r['createdAt'] ?? ''}'
|
||||
.replaceFirst('T', ' ')
|
||||
.replaceFirst(RegExp(r'\.\d+Z$'), '');
|
||||
return Card(
|
||||
elevation: 0,
|
||||
margin: const EdgeInsets.only(bottom: 10),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
final meta = _metaOf(r);
|
||||
openNoticeTarget(
|
||||
context,
|
||||
widget.api,
|
||||
session: widget.session,
|
||||
kind: '${meta['kind'] ?? r['kind'] ?? 'todo'}',
|
||||
bizType: '${meta['bizType'] ?? ''}',
|
||||
bizId: '${meta['bizId'] ?? ''}',
|
||||
title:
|
||||
'${meta['title'] ?? r['title'] ?? r['body'] ?? '系统通知'}',
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(14, 12, 14, 12),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SquareAvatar(
|
||||
label: '系',
|
||||
color: Color(0xFF07C160),
|
||||
icon: Icons.notifications,
|
||||
size: 40),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600)),
|
||||
if (body.isNotEmpty && body != title) ...[
|
||||
const SizedBox(height: 5),
|
||||
Text(body,
|
||||
style: const TextStyle(color: kMute))
|
||||
],
|
||||
const SizedBox(height: 7),
|
||||
Text(time,
|
||||
style: const TextStyle(
|
||||
fontSize: 12, color: kMute)),
|
||||
])),
|
||||
]),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user