Files
tongxunruanjian/mobile/lib/screens/call_screen.dart
T
KIMI b95159f7eb 新增 mobile/:畅联手机端 Flutter 工程(B-58)
- 登录:工号+密码走 account-service /api/login,自动登录、被踢下线回登录页
- 消息:会话列表(未读角标/免打扰/时间)、单聊群聊、文字/语音/图片/文件消息、失败重发、历史分页
- 通讯录:新的同事/我的群聊入口、按部门分组(好友 ex 字段)、搜索
- 通话:一对一语音通话(信令走 OpenIM 自定义消息 + LiveKit,token 走 /api/rtc_token)
- 界面按确认效果图 m1/m2/m3 实现,主色 #3B87F5,四态视图齐全,无英文残留
2026-08-09 01:28:12 +08:00

186 lines
5.6 KiB
Dart

import 'package:flutter/material.dart';
import '../nav.dart';
import '../services/call_service.dart';
import '../theme.dart';
import '../utils/format.dart';
import '../widgets/avatar.dart';
/// 一对一语音通话界面:呼出 / 来电 / 通话中三种状态。
/// 界面只读 CallService 的状态,信令与房间管理都在 service 里。
class CallScreen extends StatefulWidget {
const CallScreen({super.key});
@override
State<CallScreen> createState() => _CallScreenState();
}
class _CallScreenState extends State<CallScreen> {
CallService get _call => CallService.instance;
@override
void initState() {
super.initState();
// 通话状态变化时刷新界面;回到空闲态时关闭页面
_call.addListener(_onCallChanged);
}
@override
void dispose() {
_call.removeListener(_onCallChanged);
super.dispose();
}
void _onCallChanged() {
if (!mounted) return;
if (_call.phase == CallPhase.idle) {
final hint = _call.takeEndHint();
Navigator.of(context).maybePop();
if (hint != null) {
// 页面关闭后,用全局导航键把提示弹到底层页面上
WidgetsBinding.instance.addPostFrameCallback((_) {
final ctx = navigatorKey.currentContext;
if (ctx != null) {
ScaffoldMessenger.maybeOf(ctx)?.showSnackBar(
SnackBar(content: Text(hint), duration: const Duration(seconds: 2)),
);
}
});
}
return;
}
setState(() {});
}
@override
Widget build(BuildContext context) {
final phase = _call.phase;
return Scaffold(
backgroundColor: const Color(0xFF2B3038),
body: SafeArea(
child: Column(
children: [
const SizedBox(height: AppGap.x12),
NameAvatar(name: _call.peerName, size: 88),
const SizedBox(height: AppGap.x4),
Text(
_call.peerName,
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: Colors.white),
),
const SizedBox(height: AppGap.x2),
Text(
_statusText(phase),
style: const TextStyle(fontSize: AppFont.sub, color: Color(0xFFB0B6BF)),
),
const Spacer(),
_buttons(phase),
const SizedBox(height: AppGap.x12),
],
),
),
);
}
String _statusText(CallPhase phase) {
switch (phase) {
case CallPhase.outgoing:
return '正在呼叫,等待对方接听…';
case CallPhase.incoming:
return '邀请你语音通话';
case CallPhase.incall:
return FormatUtils.callDuration(_call.callSeconds);
case CallPhase.idle:
return '通话已结束';
}
}
Widget _buttons(CallPhase phase) {
switch (phase) {
case CallPhase.outgoing:
return _roundButton(icon: Icons.call_end, color: AppColors.danger, label: '取消', onTap: _call.leave);
case CallPhase.incoming:
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_roundButton(icon: Icons.call_end, color: AppColors.danger, label: '拒绝', onTap: _call.reject),
_roundButton(icon: Icons.call, color: const Color(0xFF2BA245), label: '接听', onTap: _call.accept),
],
);
case CallPhase.incall:
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_toggleButton(
icon: _call.micOn ? Icons.mic : Icons.mic_off,
label: _call.micOn ? '静音' : '已静音',
active: !_call.micOn,
onTap: _call.toggleMic,
),
_roundButton(icon: Icons.call_end, color: AppColors.danger, label: '挂断', onTap: _call.hangup),
_toggleButton(
icon: Icons.volume_up,
label: _call.speakerOn ? '免提开' : '免提',
active: _call.speakerOn,
onTap: _call.toggleSpeaker,
),
],
);
case CallPhase.idle:
return const SizedBox.shrink();
}
}
Widget _roundButton({
required IconData icon,
required Color color,
required String label,
required VoidCallback onTap,
}) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(32),
child: Container(
width: 64,
height: 64,
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
child: Icon(icon, size: 30, color: Colors.white),
),
),
const SizedBox(height: AppGap.x2),
Text(label, style: const TextStyle(fontSize: AppFont.sub, color: Color(0xFFB0B6BF))),
],
);
}
Widget _toggleButton({
required IconData icon,
required String label,
required bool active,
required VoidCallback onTap,
}) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(32),
child: Container(
width: 64,
height: 64,
decoration: BoxDecoration(
color: active ? Colors.white : const Color(0xFF3A4048),
shape: BoxShape.circle,
),
child: Icon(icon, size: 28, color: active ? AppColors.textPrimary : Colors.white),
),
),
const SizedBox(height: AppGap.x2),
Text(label, style: const TextStyle(fontSize: AppFont.sub, color: Color(0xFFB0B6BF))),
],
);
}
}