详情里的对话框切换动画ok

This commit is contained in:
zhutao
2025-09-04 23:24:48 +08:00
parent 0231dcfe1a
commit 70aa3e6ab6
8 changed files with 368 additions and 41 deletions

View File

@@ -0,0 +1,68 @@
class PlanStepDto {
num? id;
String? stepIcon;
String? stepContent;
String? stepExplain;
num? stepStatus;
PlanStepDto({this.id, this.stepIcon, this.stepContent, this.stepExplain, this.stepStatus});
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map["id"] = id;
map["step_icon"] = stepIcon;
map["step_content"] = stepContent;
map["step_explain"] = stepExplain;
map["step_status"] = stepStatus;
return map;
}
PlanStepDto.fromJson(dynamic json) {
id = json["id"];
stepIcon = json["step_icon"];
stepContent = json["step_content"];
stepExplain = json["step_explain"];
stepStatus = json["step_status"];
}
}
class PlanDetailDto {
String? agentName;
String? summary;
String? dialog;
List<PlanStepDto> stepsList;
List<String> suggestionsList;
PlanDetailDto({
this.agentName,
this.summary,
this.dialog,
List<PlanStepDto>? stepsList,
List<String>? suggestionsList,
}) : stepsList = stepsList ?? [],
suggestionsList = suggestionsList ?? [];
factory PlanDetailDto.fromJson(Map<String, dynamic> json) {
return PlanDetailDto(
agentName: json["agent_name"],
summary: json["summary"],
dialog: json["dialog"],
stepsList: json["steps"] != null
? (json["steps"] as List).map((v) => PlanStepDto.fromJson(v)).toList()
: [],
suggestionsList: json["suggestions"] != null
? List<String>.from(json["suggestions"])
: [],
);
}
Map<String, dynamic> toJson() {
return {
"agent_name": agentName,
"summary": summary,
"dialog": dialog,
"steps": stepsList.map((v) => v.toJson()).toList(),
"suggestions": suggestionsList,
};
}
}

View File

@@ -0,0 +1,10 @@
import 'package:plan/api/network/request.dart';
///初始化计划
Future<int> initPlanApi(String need, int agentId) async {
var res = await Request().post("/plan/init", {
"user_need": need,
"agent_id": agentId,
});
return res['plan_id'];
}

View File

@@ -41,8 +41,8 @@ class _AvatarNameState extends State<AvatarName> {
setState(() {
_isEdit = false;
});
var res = await updateUserInfoApi(name: value);
widget.onUpdate(res);
widget.onUpdate(UserInfo(name: value));
await updateUserInfoApi(name: value);
}
///选择图片

View File

@@ -8,9 +8,8 @@ import 'package:remixicon/remixicon.dart';
import '../widgets/edit_desc_dialog.dart';
import 'widgets/avatar_card.dart';
import 'widgets/plan_item.dart';
import 'widgets/coach_message.dart';
import 'widgets/scroll_box.dart';
import 'widgets/suggested.dart';
class PlanDetailPage extends StatefulWidget {
final String? id;
@@ -55,7 +54,11 @@ class _PlanDetailPageState extends State<PlanDetailPage> {
Widget build(BuildContext context) {
return ChangeNotifierProvider<PlanDetailStore>(
create: (_) {
return PlanDetailStore();
return PlanDetailStore(
planId: widget.id.toString(),
planContent: widget.planName ?? "",
showRoleTalk: widget.planName == null,
);
},
child: CupertinoPageScaffold(
backgroundColor: Colors.white,
@@ -108,31 +111,32 @@ class _PlanDetailPageState extends State<PlanDetailPage> {
decoration: shadowDecoration,
child: CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: SizedBox(height: 20),
),
SliverList.builder(
itemBuilder: (BuildContext context, int index) {
return PlanItem(
showEdit: _isEdit,
title: "测试 ${index + 1}",
desc: "测测 ${index + 1}",
onDelete: (id) {},
);
},
itemCount: 10,
),
SliverToBoxAdapter(
child: SuggestedTitle(),
),
SliverList.builder(
itemBuilder: (BuildContext context, int index) {
return SuggestedItem(
title: "测试",
);
},
itemCount: 5,
),
CoachMessage(),
// SliverToBoxAdapter(
// child: SizedBox(height: 20),
// ),
// SliverList.builder(
// itemBuilder: (BuildContext context, int index) {
// return PlanItem(
// showEdit: _isEdit,
// title: "测试 ${index + 1}",
// desc: "测测 ${index + 1}",
// onDelete: (id) {},
// );
// },
// itemCount: 10,
// ),
// SliverToBoxAdapter(
// child: SuggestedTitle(),
// ),
// SliverList.builder(
// itemBuilder: (BuildContext context, int index) {
// return SuggestedItem(
// title: "测试",
// );
// },
// itemCount: 5,
// ),
],
),
),

View File

@@ -1,6 +1,15 @@
import 'package:flutter/cupertino.dart';
import 'package:plan/api/dto/plan_detail_dto.dart';
import 'package:plan/api/endpoints/plan_api.dart';
class PlanDetailStore extends ChangeNotifier {
///构造函数
PlanDetailStore({
this.planContent = "",
this.planId = "",
bool showRoleTalk = true,
}) : _showRoleTalk = showRoleTalk;
///角色话语是否显示
bool _showRoleTalk = true;
@@ -10,4 +19,19 @@ class PlanDetailStore extends ChangeNotifier {
_showRoleTalk = value;
notifyListeners();
}
///计划的内容,只有新增时才会有
String planContent = "";
///计划id
String planId = "";
///计划详情
PlanDetailDto planDetail = PlanDetailDto();
///创建计划
void createPlan() async {
var id = await initPlanApi(planContent, 1);
planId = id.toString();
}
}

View File

@@ -1,6 +1,9 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:remixicon/remixicon.dart';
import '../viewmodel/plan_detail_store.dart';
class AvatarCard extends StatefulWidget {
const AvatarCard({super.key});
@@ -9,23 +12,35 @@ class AvatarCard extends StatefulWidget {
}
class _AvatarCardState extends State<AvatarCard> with SingleTickerProviderStateMixin {
bool _isShow = false;
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(milliseconds: 400),
duration: Duration(milliseconds: 300),
);
_animation = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
),
);
}
@override
void dispose() {
super.dispose();
_controller.dispose();
}
void _toggleShow() {
var store = context.read<PlanDetailStore>();
setState(() {
_isShow = !_isShow;
if (_isShow) {
store.showRoleTalk = !store.showRoleTalk;
if (store.showRoleTalk) {
_controller.forward();
} else {
_controller.reverse();
@@ -41,11 +56,10 @@ class _AvatarCardState extends State<AvatarCard> with SingleTickerProviderStateM
offset: Offset(0, 50),
child: Column(
children: [
Transform(
alignment: Alignment.center,
transform: Matrix4.identity()..translate(40.0, 40.0)..scale(0.2),
BothSizeTransition(
animation: _animation,
offset: Offset(50, 50),
child: Container(
color: Colors.red,
padding: EdgeInsets.only(bottom: 10),
child: InkWell(
onTap: _toggleShow,
@@ -84,10 +98,14 @@ class _AvatarCardState extends State<AvatarCard> with SingleTickerProviderStateM
Positioned(
top: 20,
child: Transform.translate(
offset: Offset(50, -10),
offset: Offset(40, -10),
child: GestureDetector(
onTap: _toggleShow,
child: Icon(RemixIcons.message_2_line, size: 26),
child: BothSizeTransition(
animation: ReverseAnimation(_animation),
offset: Offset(-50, -50),
child: Icon(RemixIcons.message_2_line),
),
),
),
),
@@ -101,6 +119,7 @@ class _AvatarCardState extends State<AvatarCard> with SingleTickerProviderStateM
}
}
///聊天气泡三角
class BubblePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
@@ -135,3 +154,47 @@ class BubblePainter extends CustomPainter {
return true;
}
}
///缩放动画
class BothSizeTransition extends StatelessWidget {
final Animation<double> animation;
final Widget child;
final Offset offset;
const BothSizeTransition({
super.key,
required this.animation,
this.offset = Offset.zero,
required this.child,
});
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: animation,
builder: (_, child) {
return Align(
alignment: Alignment(0.7, 1),
child: Opacity(
opacity: animation.value,
child: Transform(
alignment: Alignment(0.5, 1),
transform: Matrix4.identity()
..translate(
offset.dx * (1 - animation.value),
offset.dy * (1 - animation.value),
)
..scale(animation.value, 1.0),
child: SizeTransition(
sizeFactor: animation,
axis: Axis.vertical,
child: child,
),
),
),
);
},
child: child,
);
}
}

View File

@@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
import 'package:plan/page/plan/detail/viewmodel/plan_detail_store.dart';
import 'package:provider/provider.dart';
class CoachMessage extends StatefulWidget {
const CoachMessage({super.key});
@override
State<CoachMessage> createState() => _CoachMessageState();
}
class _CoachMessageState extends State<CoachMessage> {
@override
Widget build(BuildContext context) {
var store = context.read<PlanDetailStore>();
if (store.planContent.isEmpty) {
return SliverToBoxAdapter();
}
return SliverToBoxAdapter(
child: Container(
padding: EdgeInsets.all(20),
child: Column(
children: [
Text(
"你的教练正在拆分",
style: Theme.of(context).textTheme.bodyMedium,
),
Text(
'"${store.planContent}"',
style: Theme.of(context).textTheme.titleMedium,
),
],
),
),
);
}
}

121
lib/utils/stream.dart Normal file
View File

@@ -0,0 +1,121 @@
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'package:dio/dio.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:logger/logger.dart';
import '../config/env.dart';
import '../providers/app_store.dart';
///封装流式请求
class StreamUtils {
var logger = Logger();
Dio dio = Dio();
///发送流式请求
/// - [url] 请求的URL
/// - [data] 请求的数据,默认为空
/// - [onCall] 可选的回调函数,用于处理每个数据块
/// - [onEnd] 可选的回调函数,用于处理请求结束
/// - [onError] 可选的回调函数,用于处理请求错误
Future<void> sendStream(
String url, {
Map<String, dynamic> data = const {},
Function(Map<String, dynamic> chunk)? onCall,
Function()? onEnd,
Function()? onError,
}) async {
String token = await AppStore.getToken();
Response response = await dio.post(
"${Config.baseUrl()}$url",
data: data,
options: Options(
responseType: ResponseType.stream,
headers: {
'Content-Type': 'application/json',
'Authorization': "Bearer $token",
},
),
);
if (await _isValidResponse(response)) {
onError?.call();
return;
}
//数据
var bufferText = ""; //吐出来的内容
//处理响应
response.data?.stream
.transform(_unit8Transformer())
.transform(utf8.decoder)
.listen(
(chunk) {
List<String> chunkList = chunk.split("\n");
for (var element in chunkList) {
String streamStr = element.replaceAll("data: ", '').trim();
try {
if (streamStr.isNotEmpty) {
if (streamStr == '[DONE]') {
// 流结束标志,忽略不处理
return;
}
Map<String, dynamic> dataJSON = jsonDecode(streamStr);
//提取响应数据
Map<String, dynamic> choices = dataJSON['choices'][0];
//提取文字
var word = choices['delta']['content'];
//回调
if (word != null) {
bufferText += word;
}
onCall?.call({...choices, "text": bufferText});
}
} catch (e) {
onError?.call();
logger.e("流错误响应: $e");
}
}
},
onDone: () {
onEnd?.call();
},
onError: (error) {
onError?.call();
logger.e("流错误: $error");
},
);
}
/// 将Uint8List转换为List<int>
StreamTransformer<Uint8List, List<int>> _unit8Transformer() {
StreamTransformer<Uint8List, List<int>> unit8Transformer = StreamTransformer.fromHandlers(
handleData: (data, sink) {
sink.add(List<int>.from(data));
},
);
return unit8Transformer;
}
// 判断是否是正常请求
Future<bool> _isValidResponse(Response response) async {
final contentType = response.headers.value('content-type') ?? '';
if (contentType.contains('application/json')) {
final data = response.data;
if (data is ResponseBody) {
// 把流里的所有字节收集起来
final bytes = await data.stream.fold<List<int>>(
[],
(previous, element) => previous..addAll(element),
);
var res = jsonDecode(utf8.decode(bytes));
if (res['code'] == 0) {
EasyLoading.showToast(res['message']);
}
}
return true;
}
return false;
}
}