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