Files
Public/apps/native/lib/pages/system_notice_page.dart
daiyongkang 76f266645d Initial commit: 风影 OA 全栈源码(API/Web/Flutter/IM)
含 Phase 1.1 IM seq 排序、断线重连、多端已读同步与微信式语音转文字 UI。
排除 node_modules、构建产物、安装包与 .env 密钥。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-02 10:04:03 +00:00

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)),
])),
]),
),
),
);
},
),
),
);
}
}