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,122 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../theme.dart';
|
||||
|
||||
/// 微信式语音消息:气泡 + 下方独立白底转写框。
|
||||
class VoiceMessageBlock extends StatelessWidget {
|
||||
const VoiceMessageBlock({
|
||||
super.key,
|
||||
required this.mine,
|
||||
required this.seconds,
|
||||
required this.playing,
|
||||
this.transcript,
|
||||
this.loading = false,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
this.bubbleColor,
|
||||
this.iconColor,
|
||||
this.readLabel,
|
||||
this.bottomMargin = 8,
|
||||
});
|
||||
|
||||
final bool mine;
|
||||
final int seconds;
|
||||
final bool playing;
|
||||
final String? transcript;
|
||||
final bool loading;
|
||||
final VoidCallback? onTap;
|
||||
final VoidCallback? onLongPress;
|
||||
final Color? bubbleColor;
|
||||
final Color? iconColor;
|
||||
final String? readLabel;
|
||||
final double bottomMargin;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bubble = mine ? (bubbleColor ?? kBubbleMine) : Colors.white;
|
||||
final ink = iconColor ?? kInk;
|
||||
final width = (80 + seconds * 4).clamp(80, 180).toDouble();
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: mine ? CrossAxisAlignment.end : CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: onTap,
|
||||
onLongPress: onLongPress,
|
||||
child: Container(
|
||||
margin: EdgeInsets.only(bottom: bottomMargin),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 9),
|
||||
constraints: BoxConstraints(maxWidth: 280, minWidth: width),
|
||||
decoration: BoxDecoration(
|
||||
color: bubble,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
mine ? CrossAxisAlignment.end : CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: width,
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
mine ? MainAxisAlignment.end : MainAxisAlignment.start,
|
||||
children: [
|
||||
if (!mine) ...[
|
||||
Icon(
|
||||
playing
|
||||
? Icons.stop_circle_outlined
|
||||
: Icons.volume_up_outlined,
|
||||
size: 20,
|
||||
color: ink,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text('$seconds″',
|
||||
style: TextStyle(fontSize: 16, color: ink)),
|
||||
] else ...[
|
||||
Text('$seconds″',
|
||||
style: TextStyle(fontSize: 16, color: ink)),
|
||||
const SizedBox(width: 6),
|
||||
Icon(
|
||||
playing
|
||||
? Icons.stop_circle_outlined
|
||||
: Icons.volume_up_outlined,
|
||||
size: 20,
|
||||
color: ink,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
if (readLabel != null)
|
||||
Text(readLabel!,
|
||||
style: const TextStyle(fontSize: 10, color: kMute)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (loading) _transcriptBox(const Text('转文字中…', style: TextStyle(fontSize: 14, color: kMute)))
|
||||
else if (transcript != null && transcript!.isNotEmpty)
|
||||
_transcriptBox(Text(
|
||||
transcript!,
|
||||
style: const TextStyle(fontSize: 15, color: kInk, height: 1.35),
|
||||
)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _transcriptBox(Widget child) {
|
||||
return Container(
|
||||
margin: EdgeInsets.only(bottom: bottomMargin),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||||
constraints: const BoxConstraints(maxWidth: 280),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(color: const Color(0xFFE5E5E5)),
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user