1
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 31 KiB |
@@ -9,7 +9,7 @@ class PlanStepDto {
|
|||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final map = <String, dynamic>{};
|
final map = <String, dynamic>{};
|
||||||
map["id"] = id;
|
map["id"] = id ?? 0;
|
||||||
map["step_icon"] = stepIcon;
|
map["step_icon"] = stepIcon;
|
||||||
map["step_content"] = stepContent;
|
map["step_content"] = stepContent;
|
||||||
map["step_explain"] = stepExplain;
|
map["step_explain"] = stepExplain;
|
||||||
|
|||||||
41
lib/api/dto/plan_item_dto.dart
Normal file
41
lib/api/dto/plan_item_dto.dart
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
class PlanItemDto {
|
||||||
|
int? id;
|
||||||
|
String? agentName;
|
||||||
|
String? summary;
|
||||||
|
int? completedSteps;
|
||||||
|
int? totalSteps;
|
||||||
|
int? planStatus;
|
||||||
|
String? createdAt;
|
||||||
|
|
||||||
|
PlanItemDto({
|
||||||
|
this.id,
|
||||||
|
this.agentName,
|
||||||
|
this.summary,
|
||||||
|
this.completedSteps,
|
||||||
|
this.totalSteps,
|
||||||
|
this.planStatus,
|
||||||
|
this.createdAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final map = <String, dynamic>{};
|
||||||
|
map["plan_id"] = id;
|
||||||
|
map["agent_name"] = agentName;
|
||||||
|
map["summary"] = summary;
|
||||||
|
map["completed_steps"] = completedSteps;
|
||||||
|
map["total_steps"] = totalSteps;
|
||||||
|
map["plan_status"] = planStatus;
|
||||||
|
map["created_at"] = createdAt;
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
PlanItemDto.fromJson(dynamic json) {
|
||||||
|
id = json["plan_id"] ?? 0;
|
||||||
|
agentName = json["agent_name"] ?? "";
|
||||||
|
summary = json["summary"] ?? "";
|
||||||
|
completedSteps = json["completed_steps"] ?? 0;
|
||||||
|
totalSteps = json["total_steps"] ?? 0;
|
||||||
|
planStatus = json["plan_status"] ?? 0;
|
||||||
|
createdAt = json["created_at"] ?? "";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'package:plan/api/dto/plan_detail_dto.dart';
|
||||||
|
import 'package:plan/api/dto/plan_item_dto.dart';
|
||||||
import 'package:plan/api/network/request.dart';
|
import 'package:plan/api/network/request.dart';
|
||||||
|
|
||||||
///初始化计划
|
///初始化计划
|
||||||
@@ -8,3 +10,49 @@ Future<int> initPlanApi(String need, int agentId) async {
|
|||||||
});
|
});
|
||||||
return res['plan_id'];
|
return res['plan_id'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///保存用户计划
|
||||||
|
Future<void> savePlanApi({
|
||||||
|
required String planId,
|
||||||
|
required String summary,
|
||||||
|
required String dialog,
|
||||||
|
required List<PlanStepDto> steps,
|
||||||
|
required List<String> suggestions,
|
||||||
|
}) async {
|
||||||
|
await Request().post("/plan/save_plan", {
|
||||||
|
"plan_id": planId,
|
||||||
|
"summary": summary,
|
||||||
|
"dialog": dialog,
|
||||||
|
"steps": steps.map((e) => e.toJson()).toList(),
|
||||||
|
"suggestions": suggestions,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
///获取计划列表
|
||||||
|
Future<List<PlanItemDto>> getPlanListApi() async {
|
||||||
|
var res = await Request().get("/plan/plan_list");
|
||||||
|
return res['list'].map<PlanItemDto>((e) => PlanItemDto.fromJson(e)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
///编辑计划摘要
|
||||||
|
Future<void> editPlanSummaryApi(int planId, String summary) async {
|
||||||
|
await Request().post("/plan/edit_plan_summary", {
|
||||||
|
"plan_id": planId,
|
||||||
|
"summary": summary,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
///获取计划详情
|
||||||
|
Future<PlanDetailDto> getPlanDetailApi(String planId) async {
|
||||||
|
var res = await Request().get("/plan/plan_detail", {
|
||||||
|
"plan_id": planId,
|
||||||
|
});
|
||||||
|
return PlanDetailDto.fromJson(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
///删除计划
|
||||||
|
Future<void> deletePlanApi(int planId) async {
|
||||||
|
await Request().get("/plan/delete_plan", {
|
||||||
|
"plan_id": planId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,14 +12,14 @@ class PlanFormCard extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _PlanFormCardState extends State<PlanFormCard> {
|
class _PlanFormCardState extends State<PlanFormCard> {
|
||||||
final TextEditingController _inputController = TextEditingController();
|
final TextEditingController _inputController = TextEditingController(text: "");
|
||||||
|
|
||||||
void _handSubmit() {
|
void _handSubmit() {
|
||||||
if (_inputController.text.isEmpty) {
|
if (_inputController.text.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
context.push(
|
context.push(
|
||||||
RoutePaths.planDetail(),
|
RoutePaths.planDetail(0),
|
||||||
extra: {
|
extra: {
|
||||||
"name": _inputController.text,
|
"name": _inputController.text,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ import 'package:remixicon/remixicon.dart';
|
|||||||
import '../widgets/edit_desc_dialog.dart';
|
import '../widgets/edit_desc_dialog.dart';
|
||||||
import 'widgets/avatar_card.dart';
|
import 'widgets/avatar_card.dart';
|
||||||
import 'widgets/coach_message.dart';
|
import 'widgets/coach_message.dart';
|
||||||
|
import 'widgets/plan_list.dart';
|
||||||
import 'widgets/scroll_box.dart';
|
import 'widgets/scroll_box.dart';
|
||||||
|
import 'widgets/suggested.dart';
|
||||||
|
|
||||||
class PlanDetailPage extends StatefulWidget {
|
class PlanDetailPage extends StatefulWidget {
|
||||||
final String? id;
|
final String? id;
|
||||||
@@ -27,6 +29,20 @@ class PlanDetailPage extends StatefulWidget {
|
|||||||
|
|
||||||
class _PlanDetailPageState extends State<PlanDetailPage> {
|
class _PlanDetailPageState extends State<PlanDetailPage> {
|
||||||
bool _isEdit = false;
|
bool _isEdit = false;
|
||||||
|
final ScrollController scrollController = ScrollController();
|
||||||
|
|
||||||
|
///store对象
|
||||||
|
late PlanDetailStore store;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
store = PlanDetailStore(
|
||||||
|
planId: widget.id.toString(),
|
||||||
|
planContent: widget.planName ?? "",
|
||||||
|
scrollController: scrollController,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
///popup菜单
|
///popup菜单
|
||||||
void _onPopupActionSelected(String value) {
|
void _onPopupActionSelected(String value) {
|
||||||
@@ -54,51 +70,9 @@ class _PlanDetailPageState extends State<PlanDetailPage> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ChangeNotifierProvider<PlanDetailStore>(
|
return ChangeNotifierProvider<PlanDetailStore>(
|
||||||
create: (_) {
|
create: (_) {
|
||||||
return PlanDetailStore(
|
return store;
|
||||||
planId: widget.id.toString(),
|
|
||||||
planContent: widget.planName ?? "",
|
|
||||||
showRoleTalk: widget.planName == null,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
child: CupertinoPageScaffold(
|
child: Consumer<PlanDetailStore>(
|
||||||
backgroundColor: Colors.white,
|
|
||||||
navigationBar: CupertinoNavigationBar(
|
|
||||||
middle: Text('计划详情'),
|
|
||||||
trailing: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min, // 关键:Row 只占实际内容宽度
|
|
||||||
children: [
|
|
||||||
AnimatedSwitcher(
|
|
||||||
duration: Duration(milliseconds: 300),
|
|
||||||
transitionBuilder: (child, animation) {
|
|
||||||
// 仅使用渐变动画
|
|
||||||
return FadeTransition(
|
|
||||||
opacity: animation,
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: _isEdit
|
|
||||||
? InkWell(
|
|
||||||
onTap: _cancelEdit,
|
|
||||||
child: Icon(RemixIcons.check_fill),
|
|
||||||
)
|
|
||||||
: PopupAction(
|
|
||||||
onSelected: _onPopupActionSelected,
|
|
||||||
items: [
|
|
||||||
PopupMenuItem(
|
|
||||||
value: 'edit_step',
|
|
||||||
child: Text("编辑步骤"),
|
|
||||||
),
|
|
||||||
PopupMenuItem(
|
|
||||||
value: 'edit_desc',
|
|
||||||
child: Text("编辑摘要"),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
child: Icon(RemixIcons.more_fill),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -110,33 +84,12 @@ class _PlanDetailPageState extends State<PlanDetailPage> {
|
|||||||
child: Container(
|
child: Container(
|
||||||
decoration: shadowDecoration,
|
decoration: shadowDecoration,
|
||||||
child: CustomScrollView(
|
child: CustomScrollView(
|
||||||
|
controller: scrollController,
|
||||||
slivers: [
|
slivers: [
|
||||||
CoachMessage(),
|
CoachMessage(),
|
||||||
// SliverToBoxAdapter(
|
PlanList(),
|
||||||
// child: SizedBox(height: 20),
|
SuggestedTitle(),
|
||||||
// ),
|
SuggestedList(),
|
||||||
// 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,
|
|
||||||
// ),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -146,6 +99,49 @@ class _PlanDetailPageState extends State<PlanDetailPage> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
builder: (context, store, child) {
|
||||||
|
return CupertinoPageScaffold(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
navigationBar: CupertinoNavigationBar(
|
||||||
|
middle: Text(store.planDetail.summary ?? ""),
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min, // 关键:Row 只占实际内容宽度
|
||||||
|
children: [
|
||||||
|
AnimatedSwitcher(
|
||||||
|
duration: Duration(milliseconds: 300),
|
||||||
|
transitionBuilder: (child, animation) {
|
||||||
|
// 仅使用渐变动画
|
||||||
|
return FadeTransition(
|
||||||
|
opacity: animation,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: _isEdit
|
||||||
|
? InkWell(
|
||||||
|
onTap: _cancelEdit,
|
||||||
|
child: Icon(RemixIcons.check_fill),
|
||||||
|
)
|
||||||
|
: PopupAction(
|
||||||
|
onSelected: _onPopupActionSelected,
|
||||||
|
items: [
|
||||||
|
PopupMenuItem(
|
||||||
|
value: 'edit_step',
|
||||||
|
child: Text("编辑步骤"),
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
value: 'edit_desc',
|
||||||
|
child: Text("编辑摘要"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: Icon(RemixIcons.more_fill),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: child!,
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,30 @@
|
|||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||||
import 'package:plan/api/dto/plan_detail_dto.dart';
|
import 'package:plan/api/dto/plan_detail_dto.dart';
|
||||||
import 'package:plan/api/endpoints/plan_api.dart';
|
import 'package:plan/api/endpoints/plan_api.dart';
|
||||||
|
import 'package:plan/utils/stream.dart';
|
||||||
|
|
||||||
class PlanDetailStore extends ChangeNotifier {
|
class PlanDetailStore extends ChangeNotifier {
|
||||||
///构造函数
|
///构造函数
|
||||||
PlanDetailStore({
|
PlanDetailStore({
|
||||||
this.planContent = "",
|
this.planContent = "",
|
||||||
this.planId = "",
|
this.planId = "",
|
||||||
bool showRoleTalk = true,
|
required this.scrollController,
|
||||||
}) : _showRoleTalk = showRoleTalk;
|
}) {
|
||||||
|
//如果没有id进行初始化
|
||||||
|
if (planId == "0") {
|
||||||
|
createPlan();
|
||||||
|
}else{
|
||||||
|
//获取详情
|
||||||
|
getPlanDetail();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///滚动控制器
|
||||||
|
ScrollController scrollController;
|
||||||
|
|
||||||
///角色话语是否显示
|
///角色话语是否显示
|
||||||
bool _showRoleTalk = true;
|
bool _showRoleTalk = false;
|
||||||
|
|
||||||
bool get showRoleTalk => _showRoleTalk;
|
bool get showRoleTalk => _showRoleTalk;
|
||||||
|
|
||||||
@@ -27,11 +40,123 @@ class PlanDetailStore extends ChangeNotifier {
|
|||||||
String planId = "";
|
String planId = "";
|
||||||
|
|
||||||
///计划详情
|
///计划详情
|
||||||
PlanDetailDto planDetail = PlanDetailDto();
|
PlanDetailDto planDetail = PlanDetailDto(summary: "计划详情");
|
||||||
|
|
||||||
|
///流请求工具
|
||||||
|
StreamUtils streamUtils = StreamUtils();
|
||||||
|
|
||||||
///创建计划
|
///创建计划
|
||||||
void createPlan() async {
|
void createPlan() async {
|
||||||
var id = await initPlanApi(planContent, 1);
|
var id = await initPlanApi(planContent, 1);
|
||||||
planId = id.toString();
|
planId = id.toString();
|
||||||
|
// planId = "3";
|
||||||
|
|
||||||
|
///生成摘要---------------------------
|
||||||
|
String summary = "";
|
||||||
|
streamUtils.sendStream(
|
||||||
|
"/plan/make_summary",
|
||||||
|
data: {"plan_id": planId},
|
||||||
|
onCall: (chunk) {
|
||||||
|
summary = chunk['text'];
|
||||||
|
},
|
||||||
|
onEnd: () {
|
||||||
|
planDetail.summary = summary;
|
||||||
|
notifyListeners();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/// 生成对白-------------------------------
|
||||||
|
String dialog = "";
|
||||||
|
await streamUtils.sendStream(
|
||||||
|
"/plan/make_dialog",
|
||||||
|
data: {"plan_id": planId},
|
||||||
|
onCall: (chunk) {
|
||||||
|
dialog = chunk['text'];
|
||||||
|
},
|
||||||
|
onEnd: () {
|
||||||
|
planDetail.dialog = dialog;
|
||||||
|
notifyListeners();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/// 生成步骤-------------------------------
|
||||||
|
await streamUtils.sendStream(
|
||||||
|
"/plan/make_step",
|
||||||
|
data: {"plan_id": planId},
|
||||||
|
onCall: (chunk) {
|
||||||
|
final text = chunk['text'].toString();
|
||||||
|
|
||||||
|
// 拆分出完整句子(以 [NEXT] 结尾)
|
||||||
|
List<String> sentences = text
|
||||||
|
.split("\n")
|
||||||
|
.where((e) => e.contains("[NEXT]"))
|
||||||
|
.map((e) => e.replaceAll("[NEXT]", "").trim())
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
// 直接覆盖 stepsList
|
||||||
|
planDetail.stepsList = sentences.map((s) => PlanStepDto(stepContent: s)).toList();
|
||||||
|
notifyListeners();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/// 生成步骤解释 -------------------------------
|
||||||
|
await streamUtils.sendStream(
|
||||||
|
"/plan/make_step_explain",
|
||||||
|
data: {
|
||||||
|
"plan_id": planId,
|
||||||
|
"steps": planDetail.stepsList.map((e) => e.toJson()).toList(),
|
||||||
|
},
|
||||||
|
onCall: (chunk) {
|
||||||
|
final text = chunk['text'].toString();
|
||||||
|
List<String> sentences = text
|
||||||
|
.split("\n")
|
||||||
|
.where((e) => e.contains("[NEXT]"))
|
||||||
|
.map((e) => e.replaceAll("[NEXT]", "").trim())
|
||||||
|
.toList();
|
||||||
|
for (int i = 0; i < sentences.length; i++) {
|
||||||
|
planDetail.stepsList[i] = planDetail.stepsList[i]..stepExplain = sentences[i];
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
},
|
||||||
|
onEnd: () {},
|
||||||
|
);
|
||||||
|
|
||||||
|
/// 生成建议 ------------------
|
||||||
|
await streamUtils.sendStream(
|
||||||
|
"/plan/make_suggestion",
|
||||||
|
data: {
|
||||||
|
"plan_id": planId,
|
||||||
|
"steps": planDetail.stepsList.map((e) => e.toJson()).toList(),
|
||||||
|
},
|
||||||
|
onCall: (chunk) {
|
||||||
|
final text = chunk['text'].toString();
|
||||||
|
List<String> sentences = text
|
||||||
|
.split("\n")
|
||||||
|
.where((e) => e.contains("[NEXT]"))
|
||||||
|
.map((e) => e.replaceAll("[NEXT]", "").trim())
|
||||||
|
.toList();
|
||||||
|
planDetail.suggestionsList = sentences;
|
||||||
|
notifyListeners();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await savePlan();
|
||||||
|
}
|
||||||
|
|
||||||
|
///保存计划
|
||||||
|
Future<void> savePlan() async {
|
||||||
|
await savePlanApi(
|
||||||
|
planId: planId,
|
||||||
|
summary: planDetail.summary ?? "",
|
||||||
|
dialog: planDetail.dialog ?? "",
|
||||||
|
steps: planDetail.stepsList,
|
||||||
|
suggestions: planDetail.suggestionsList,
|
||||||
|
);
|
||||||
|
EasyLoading.showToast("计划创建成功");
|
||||||
|
}
|
||||||
|
|
||||||
|
///获取详情
|
||||||
|
Future<void> getPlanDetail() async {
|
||||||
|
planDetail = await getPlanDetailApi(planId);
|
||||||
|
notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,9 +15,13 @@ class _AvatarCardState extends State<AvatarCard> with SingleTickerProviderStateM
|
|||||||
late AnimationController _controller;
|
late AnimationController _controller;
|
||||||
late Animation<double> _animation;
|
late Animation<double> _animation;
|
||||||
|
|
||||||
|
///对话框值
|
||||||
|
String _dialog = "";
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
//初始化动画
|
||||||
_controller = AnimationController(
|
_controller = AnimationController(
|
||||||
vsync: this,
|
vsync: this,
|
||||||
duration: Duration(milliseconds: 300),
|
duration: Duration(milliseconds: 300),
|
||||||
@@ -28,6 +32,7 @@ class _AvatarCardState extends State<AvatarCard> with SingleTickerProviderStateM
|
|||||||
curve: Curves.easeInOut,
|
curve: Curves.easeInOut,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
_initDialog();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -36,8 +41,31 @@ class _AvatarCardState extends State<AvatarCard> with SingleTickerProviderStateM
|
|||||||
_controller.dispose();
|
_controller.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///初始化是否显示对话
|
||||||
|
void _initDialog() {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
final store = context.read<PlanDetailStore>();
|
||||||
|
|
||||||
|
void listener() {
|
||||||
|
if (store.planDetail.dialog != null && !store.showRoleTalk) {
|
||||||
|
setState(() {
|
||||||
|
_dialog = store.planDetail.dialog!;
|
||||||
|
});
|
||||||
|
_toggleShow();
|
||||||
|
store.removeListener(listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
store.addListener(listener);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
///切换显示show
|
||||||
void _toggleShow() {
|
void _toggleShow() {
|
||||||
var store = context.read<PlanDetailStore>();
|
var store = context.read<PlanDetailStore>();
|
||||||
|
if (store.planDetail.dialog == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
store.showRoleTalk = !store.showRoleTalk;
|
store.showRoleTalk = !store.showRoleTalk;
|
||||||
if (store.showRoleTalk) {
|
if (store.showRoleTalk) {
|
||||||
@@ -71,10 +99,7 @@ class _AvatarCardState extends State<AvatarCard> with SingleTickerProviderStateM
|
|||||||
borderRadius: BorderRadius.circular(3),
|
borderRadius: BorderRadius.circular(3),
|
||||||
border: Border.all(color: Colors.black, width: 1),
|
border: Border.all(color: Colors.black, width: 1),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(_dialog, style: TextStyle(fontSize: 12)),
|
||||||
"好的,让我们把学习软件开发这个目标分解成最简单的小步骤,这样你明天就能轻松开始行动",
|
|
||||||
style: TextStyle(fontSize: 12),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:plan/widgets/business/delete_row_item.dart';
|
|
||||||
import 'package:remixicon/remixicon.dart';
|
|
||||||
|
|
||||||
class PlanItem extends StatefulWidget {
|
|
||||||
final String title;
|
|
||||||
final String desc;
|
|
||||||
final bool showEdit;
|
|
||||||
final Function(int) onDelete;
|
|
||||||
|
|
||||||
const PlanItem({
|
|
||||||
super.key,
|
|
||||||
required this.title,
|
|
||||||
required this.desc,
|
|
||||||
this.showEdit = false,
|
|
||||||
required this.onDelete,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<PlanItem> createState() => _PlanItemState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _PlanItemState extends State<PlanItem> with AutomaticKeepAliveClientMixin {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
super.build(context);
|
|
||||||
return Container(
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
|
||||||
child: DeleteRowItem(
|
|
||||||
showDelete: widget.showEdit,
|
|
||||||
onDelete: () {},
|
|
||||||
builder: (_, animate) {
|
|
||||||
return [
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
margin: EdgeInsets.only(bottom: 3),
|
|
||||||
child: Text(
|
|
||||||
"完成以上步骤后,给自己倒杯水休息一下。",
|
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
"就从这里开始,写下基本信息只需要2分钟,这是最简单的一部",
|
|
||||||
style: Theme.of(context).textTheme.labelSmall,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizeTransition(
|
|
||||||
axis: Axis.horizontal,
|
|
||||||
sizeFactor: animate,
|
|
||||||
child: Container(
|
|
||||||
margin: EdgeInsets.only(left: 10),
|
|
||||||
child: Opacity(
|
|
||||||
opacity: 0.4,
|
|
||||||
child: Icon(RemixIcons.menu_line),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool get wantKeepAlive => true;
|
|
||||||
}
|
|
||||||
156
lib/page/plan/detail/widgets/plan_list.dart
Normal file
156
lib/page/plan/detail/widgets/plan_list.dart
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:plan/api/dto/plan_detail_dto.dart';
|
||||||
|
import 'package:plan/page/plan/detail/viewmodel/plan_detail_store.dart';
|
||||||
|
import 'package:plan/utils/common.dart';
|
||||||
|
import 'package:plan/widgets/business/delete_row_item.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:remixicon/remixicon.dart';
|
||||||
|
|
||||||
|
class PlanList extends StatefulWidget {
|
||||||
|
const PlanList({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PlanList> createState() => _PlanListState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PlanListState extends State<PlanList> {
|
||||||
|
final GlobalKey<SliverAnimatedListState> _listKey = GlobalKey<SliverAnimatedListState>();
|
||||||
|
List<PlanStepDto> _stepsList = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_initListen();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _initListen() {
|
||||||
|
final store = context.read<PlanDetailStore>();
|
||||||
|
//先初始化,只在详情时才会有意义
|
||||||
|
_stepsList = store.planDetail.stepsList;
|
||||||
|
store.addListener(() {
|
||||||
|
final newList = store.planDetail.stepsList;
|
||||||
|
|
||||||
|
// 找出新增的 item
|
||||||
|
if (newList.length > _stepsList.length) {
|
||||||
|
final addedItems = newList.sublist(_stepsList.length);
|
||||||
|
|
||||||
|
for (var item in addedItems) {
|
||||||
|
final index = _stepsList.length;
|
||||||
|
_stepsList.add(item);
|
||||||
|
|
||||||
|
// 插入动画
|
||||||
|
_listKey.currentState?.insertItem(
|
||||||
|
index,
|
||||||
|
duration: Duration(milliseconds: 300),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
_stepsList = store.planDetail.stepsList;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SliverAnimatedList(
|
||||||
|
key: _listKey,
|
||||||
|
initialItemCount: _stepsList.length,
|
||||||
|
itemBuilder: (_, index, animation) {
|
||||||
|
//动画
|
||||||
|
final curvedAnimation = CurvedAnimation(
|
||||||
|
parent: animation,
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
);
|
||||||
|
return PlanItem(
|
||||||
|
step: _stepsList[index],
|
||||||
|
curvedAnimation: curvedAnimation,
|
||||||
|
onDelete: (id) {},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PlanItem extends StatelessWidget {
|
||||||
|
final PlanStepDto step;
|
||||||
|
final bool showEdit;
|
||||||
|
final CurvedAnimation curvedAnimation;
|
||||||
|
final Function(int) onDelete;
|
||||||
|
|
||||||
|
const PlanItem({
|
||||||
|
super.key,
|
||||||
|
required this.step,
|
||||||
|
required this.curvedAnimation,
|
||||||
|
this.showEdit = false,
|
||||||
|
required this.onDelete,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return FadeTransition(
|
||||||
|
opacity: curvedAnimation,
|
||||||
|
child: SizeTransition(
|
||||||
|
sizeFactor: curvedAnimation,
|
||||||
|
axis: Axis.vertical,
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||||
|
child: DeleteRowItem(
|
||||||
|
showDelete: showEdit,
|
||||||
|
onDelete: () {},
|
||||||
|
builder: (_, animate) {
|
||||||
|
return [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
margin: EdgeInsets.only(bottom: 3),
|
||||||
|
child: Text(
|
||||||
|
step.stepContent ?? "",
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
AnimatedSwitcher(
|
||||||
|
duration: Duration(milliseconds: 300),
|
||||||
|
transitionBuilder: (child, animation) {
|
||||||
|
return FadeTransition(
|
||||||
|
opacity: animation,
|
||||||
|
child: SizeTransition(
|
||||||
|
sizeFactor: animation,
|
||||||
|
axis: Axis.vertical,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: getNotEmpty(step.stepExplain) == null
|
||||||
|
? SizedBox.shrink(key: ValueKey("empty"))
|
||||||
|
: Text(
|
||||||
|
step.stepExplain!,
|
||||||
|
style: Theme.of(context).textTheme.labelSmall,
|
||||||
|
key: ValueKey(step.stepExplain),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizeTransition(
|
||||||
|
axis: Axis.horizontal,
|
||||||
|
sizeFactor: animate,
|
||||||
|
child: Container(
|
||||||
|
margin: EdgeInsets.only(left: 10),
|
||||||
|
child: Opacity(
|
||||||
|
opacity: 0.4,
|
||||||
|
child: Icon(RemixIcons.menu_line),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +1,29 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class ScrollBox extends StatelessWidget {
|
class ScrollBox extends StatelessWidget {
|
||||||
|
final ScrollController? scrollController;
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
|
||||||
const ScrollBox({super.key, required this.child});
|
const ScrollBox({
|
||||||
|
super.key,
|
||||||
|
required this.child,
|
||||||
|
this.scrollController,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ScrollbarTheme(
|
return ScrollbarTheme(
|
||||||
data: ScrollbarThemeData(
|
data: ScrollbarThemeData(
|
||||||
thumbColor: WidgetStateProperty.all(Theme.of(context).colorScheme.surfaceContainerHigh),
|
thumbColor: WidgetStateProperty.all(
|
||||||
|
Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
|
),
|
||||||
thickness: WidgetStateProperty.all(3),
|
thickness: WidgetStateProperty.all(3),
|
||||||
crossAxisMargin: 3,
|
crossAxisMargin: 3,
|
||||||
mainAxisMargin: 2,
|
mainAxisMargin: 2,
|
||||||
radius: const Radius.circular(5),
|
radius: const Radius.circular(5),
|
||||||
),
|
),
|
||||||
child: Scrollbar(
|
child: Scrollbar(
|
||||||
|
controller: scrollController,
|
||||||
child: child,
|
child: child,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:plan/page/plan/detail/viewmodel/plan_detail_store.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
import 'package:remixicon/remixicon.dart';
|
import 'package:remixicon/remixicon.dart';
|
||||||
|
|
||||||
///模块标题
|
///模块标题
|
||||||
@@ -7,66 +9,165 @@ class SuggestedTitle extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
var list = context.select<PlanDetailStore, List<String>>(
|
||||||
margin: const EdgeInsets.only(top: 20, bottom: 5),
|
(store) => store.planDetail.suggestionsList,
|
||||||
child: Opacity(
|
);
|
||||||
opacity: 0.6,
|
if (list.isEmpty) {
|
||||||
child: Row(
|
return const SliverToBoxAdapter(
|
||||||
spacing: 20,
|
child: SizedBox(),
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
);
|
||||||
children: [
|
}
|
||||||
Container(
|
return SliverToBoxAdapter(
|
||||||
width: 15,
|
child: TweenAnimationBuilder<double>(
|
||||||
height: 1,
|
tween: Tween(begin: 0, end: 1), // 从透明到完全显示
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
duration: const Duration(milliseconds: 300),
|
||||||
|
builder: (context, value, child) {
|
||||||
|
return Opacity(
|
||||||
|
opacity: value,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.only(top: 20, bottom: 5),
|
||||||
|
child: Opacity(
|
||||||
|
opacity: 0.6,
|
||||||
|
child: Row(
|
||||||
|
spacing: 20,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 15,
|
||||||
|
height: 1,
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"额外建议",
|
||||||
|
style: Theme.of(context).textTheme.titleSmall,
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
width: 15,
|
||||||
|
height: 1,
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
Text(
|
),
|
||||||
"额外建议",
|
|
||||||
style: Theme.of(context).textTheme.titleSmall,
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
width: 15,
|
|
||||||
height: 1,
|
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SuggestedItem extends StatelessWidget {
|
///额外建议列表
|
||||||
final String title;
|
class SuggestedList extends StatefulWidget {
|
||||||
|
const SuggestedList({super.key});
|
||||||
|
|
||||||
const SuggestedItem({super.key, required this.title});
|
@override
|
||||||
|
State<SuggestedList> createState() => _SuggestedListState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SuggestedListState extends State<SuggestedList> {
|
||||||
|
final GlobalKey<SliverAnimatedListState> _listKey = GlobalKey<SliverAnimatedListState>();
|
||||||
|
List<String> _suggestionsList = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_initListen();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _initListen() {
|
||||||
|
final store = context.read<PlanDetailStore>();
|
||||||
|
//先初始化,只在详情时才会有意义
|
||||||
|
_suggestionsList = store.planDetail.suggestionsList;
|
||||||
|
store.addListener(() {
|
||||||
|
final newList = store.planDetail.suggestionsList;
|
||||||
|
|
||||||
|
// 找出新增的 item
|
||||||
|
if (newList.length > _suggestionsList.length) {
|
||||||
|
final addedItems = newList.sublist(_suggestionsList.length);
|
||||||
|
|
||||||
|
for (var item in addedItems) {
|
||||||
|
final index = _suggestionsList.length;
|
||||||
|
_suggestionsList.add(item);
|
||||||
|
|
||||||
|
// 插入动画
|
||||||
|
_listKey.currentState?.insertItem(
|
||||||
|
index,
|
||||||
|
duration: Duration(milliseconds: 300),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
_suggestionsList = store.planDetail.suggestionsList;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Padding(
|
return SliverAnimatedList(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
key: _listKey,
|
||||||
child: Row(
|
initialItemCount: _suggestionsList.length,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
itemBuilder: (_, index, animation) {
|
||||||
spacing: 10,
|
final curvedAnimation = CurvedAnimation(
|
||||||
children: [
|
parent: animation,
|
||||||
Transform.translate(
|
curve: Curves.easeInOut,
|
||||||
offset: const Offset(0, 5),
|
);
|
||||||
child: Icon(
|
return SuggestedItem(
|
||||||
RemixIcons.lightbulb_flash_fill,
|
title: _suggestionsList[index],
|
||||||
color: Color(0xfff2a529),
|
curvedAnimation: curvedAnimation,
|
||||||
size: 18,
|
);
|
||||||
),
|
},
|
||||||
),
|
);
|
||||||
Expanded(
|
}
|
||||||
child: Opacity(
|
}
|
||||||
opacity: 0.5,
|
|
||||||
child: Text(
|
///建议item
|
||||||
title,
|
class SuggestedItem extends StatelessWidget {
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
final String title;
|
||||||
|
final CurvedAnimation curvedAnimation;
|
||||||
|
|
||||||
|
const SuggestedItem({
|
||||||
|
super.key,
|
||||||
|
required this.title,
|
||||||
|
required this.curvedAnimation,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return FadeTransition(
|
||||||
|
opacity: curvedAnimation,
|
||||||
|
child: SizeTransition(
|
||||||
|
sizeFactor: curvedAnimation,
|
||||||
|
axis: Axis.vertical,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
spacing: 10,
|
||||||
|
children: [
|
||||||
|
Transform.translate(
|
||||||
|
offset: const Offset(0, 5),
|
||||||
|
child: Icon(
|
||||||
|
RemixIcons.lightbulb_flash_fill,
|
||||||
|
color: Color(0xfff2a529),
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
Expanded(
|
||||||
|
child: Opacity(
|
||||||
|
opacity: 0.5,
|
||||||
|
child: Text(
|
||||||
|
title,
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:plan/api/dto/plan_item_dto.dart';
|
||||||
|
import 'package:plan/api/endpoints/plan_api.dart';
|
||||||
import 'package:remixicon/remixicon.dart';
|
import 'package:remixicon/remixicon.dart';
|
||||||
|
|
||||||
import 'widgets/history_item.dart';
|
import 'widgets/history_item.dart';
|
||||||
import '../../../widgets/ui_kit/popup/popup_action.dart';
|
import '../../../widgets/ui_kit/popup/popup_action.dart';
|
||||||
|
import 'widgets/loading_box.dart';
|
||||||
|
|
||||||
class PlanHistoryPage extends StatefulWidget {
|
class PlanHistoryPage extends StatefulWidget {
|
||||||
const PlanHistoryPage({super.key});
|
const PlanHistoryPage({super.key});
|
||||||
@@ -16,12 +19,23 @@ class _PlanHistoryPageState extends State<PlanHistoryPage> {
|
|||||||
///是否显示删除
|
///是否显示删除
|
||||||
bool _isDelete = false;
|
bool _isDelete = false;
|
||||||
|
|
||||||
|
///数据
|
||||||
|
List<PlanItemDto> _record = [];
|
||||||
|
bool _isInit = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_onRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
///刷新
|
///刷新
|
||||||
Future<void> _onRefresh() async {
|
Future<void> _onRefresh() async {
|
||||||
//模拟网络请求
|
var list = await getPlanListApi();
|
||||||
await Future.delayed(Duration(milliseconds: 1000));
|
setState(() {
|
||||||
//结束刷新
|
_record = list;
|
||||||
return Future.value(true);
|
_isInit = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
///popup事件
|
///popup事件
|
||||||
@@ -36,7 +50,14 @@ class _PlanHistoryPageState extends State<PlanHistoryPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
///确认删除
|
///确认删除
|
||||||
void _confirmDelete(int id) {}
|
void _confirmDelete(int id) {
|
||||||
|
print(_record.map((e) => e.toJson()));
|
||||||
|
setState(() {
|
||||||
|
_record.removeWhere((element) => element.id == id);
|
||||||
|
print(_record.map((e) => e.toJson()));
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -44,19 +65,6 @@ class _PlanHistoryPageState extends State<PlanHistoryPage> {
|
|||||||
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
|
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
navigationBar: CupertinoNavigationBar(
|
navigationBar: CupertinoNavigationBar(
|
||||||
middle: const Text("计划历史"),
|
middle: const Text("计划历史"),
|
||||||
trailing: PopupAction(
|
|
||||||
onSelected: _onPopupActionSelected,
|
|
||||||
items: [
|
|
||||||
PopupMenuItem(
|
|
||||||
value: 'edit',
|
|
||||||
child: Text(
|
|
||||||
_isDelete ? "完成" : "编辑",
|
|
||||||
style: TextStyle(color: Colors.black),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
child: Icon(RemixIcons.more_fill),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: CustomScrollView(
|
child: CustomScrollView(
|
||||||
@@ -69,36 +77,28 @@ class _PlanHistoryPageState extends State<PlanHistoryPage> {
|
|||||||
onRefresh: _onRefresh,
|
onRefresh: _onRefresh,
|
||||||
),
|
),
|
||||||
//列表
|
//列表
|
||||||
SliverToBoxAdapter(
|
LoadingBox(
|
||||||
child: Padding(
|
loading: _isInit,
|
||||||
padding: const EdgeInsets.all(15),
|
isEmpty: _record.isEmpty,
|
||||||
child: Container(
|
child: ClipRRect(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 15),
|
child: CustomScrollView(
|
||||||
decoration: BoxDecoration(
|
shrinkWrap: true,
|
||||||
color: Colors.white,
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
borderRadius: BorderRadius.circular(10),
|
slivers: [
|
||||||
),
|
SliverList.separated(
|
||||||
child: CustomScrollView(
|
itemBuilder: (BuildContext context, int index) {
|
||||||
shrinkWrap: true,
|
return HistoryItem(
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
item: _record[index],
|
||||||
slivers: [
|
showDelete: _isDelete,
|
||||||
SliverList.separated(
|
onDelete: _confirmDelete,
|
||||||
itemBuilder: (BuildContext context, int index) {
|
);
|
||||||
return HistoryItem(
|
},
|
||||||
showDelete: _isDelete,
|
separatorBuilder: (BuildContext context, int index) {
|
||||||
onDelete: _confirmDelete,
|
return SizedBox(height: 15);
|
||||||
);
|
},
|
||||||
},
|
itemCount: _record.length,
|
||||||
separatorBuilder: (BuildContext context, int index) {
|
),
|
||||||
return Divider(
|
],
|
||||||
height: 1,
|
|
||||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
itemCount: 5,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,15 +1,23 @@
|
|||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:plan/api/dto/plan_item_dto.dart';
|
||||||
|
import 'package:plan/api/endpoints/plan_api.dart';
|
||||||
import 'package:plan/router/config/route_paths.dart';
|
import 'package:plan/router/config/route_paths.dart';
|
||||||
|
import 'package:plan/utils/format.dart';
|
||||||
import 'package:plan/widgets/business/delete_row_item.dart';
|
import 'package:plan/widgets/business/delete_row_item.dart';
|
||||||
import 'package:remixicon/remixicon.dart';
|
import 'package:remixicon/remixicon.dart';
|
||||||
|
|
||||||
|
import '../../widgets/edit_desc_dialog.dart';
|
||||||
|
|
||||||
class HistoryItem extends StatefulWidget {
|
class HistoryItem extends StatefulWidget {
|
||||||
|
final PlanItemDto item;
|
||||||
final bool showDelete;
|
final bool showDelete;
|
||||||
final Function(int) onDelete;
|
final Function(int) onDelete;
|
||||||
|
|
||||||
const HistoryItem({
|
const HistoryItem({
|
||||||
super.key,
|
super.key,
|
||||||
|
required this.item,
|
||||||
this.showDelete = false,
|
this.showDelete = false,
|
||||||
required this.onDelete,
|
required this.onDelete,
|
||||||
});
|
});
|
||||||
@@ -19,73 +27,139 @@ class HistoryItem extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _HistoryItemState extends State<HistoryItem> {
|
class _HistoryItemState extends State<HistoryItem> {
|
||||||
|
late PlanItemDto _data;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_data = widget.item;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant HistoryItem oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (oldWidget.item != widget.item) {
|
||||||
|
setState(() {
|
||||||
|
_data = widget.item;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
///跳转详情
|
///跳转详情
|
||||||
void _goDetail() {
|
void _goDetail() {
|
||||||
context.push(RoutePaths.planDetail(1));
|
context.push(RoutePaths.planDetail(_data.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
///编辑摘要
|
||||||
|
void _handEdit() {
|
||||||
|
context.pop();
|
||||||
|
showEditDescDialog(
|
||||||
|
context,
|
||||||
|
value: _data.summary ?? "",
|
||||||
|
onConfirm: (value) async {
|
||||||
|
setState(() {
|
||||||
|
_data.summary = value;
|
||||||
|
});
|
||||||
|
await editPlanSummaryApi(_data.id!, value);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
///删除计划
|
||||||
|
void _handDelete() async {
|
||||||
|
context.pop();
|
||||||
|
widget.onDelete(_data.id!);
|
||||||
|
await deletePlanApi(_data.id!);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GestureDetector(
|
return CupertinoContextMenu(
|
||||||
onTap: _goDetail,
|
actions: [
|
||||||
child: Container(
|
// 编辑摘要
|
||||||
width: double.infinity,
|
CupertinoContextMenuAction(
|
||||||
padding: EdgeInsets.symmetric(vertical: 15),
|
onPressed: _handEdit,
|
||||||
color: Colors.white,
|
child: _actionItem(
|
||||||
child: DeleteRowItem(
|
"Edit Summary",
|
||||||
showDelete: widget.showDelete,
|
CupertinoIcons.pencil,
|
||||||
onDelete: () {
|
),
|
||||||
widget.onDelete(0);
|
),
|
||||||
},
|
CupertinoContextMenuAction(
|
||||||
builder: (_, __) {
|
isDestructiveAction: true,
|
||||||
return [
|
onPressed: _handDelete,
|
||||||
Expanded(
|
child: _actionItem(
|
||||||
child: Column(
|
"Delete Summary",
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
CupertinoIcons.delete,
|
||||||
children: [
|
),
|
||||||
Container(
|
),
|
||||||
margin: EdgeInsets.only(bottom: 5),
|
],
|
||||||
child: Text("开始学习软件开发"),
|
child: GestureDetector(
|
||||||
),
|
onTap: _goDetail,
|
||||||
Container(
|
child: Container(
|
||||||
margin: EdgeInsets.only(bottom: 5),
|
width: 400,
|
||||||
child: Text(
|
margin: EdgeInsets.symmetric(horizontal: 15),
|
||||||
"创建于 2025/9/3 9:40:51 教练:W教练",
|
padding: EdgeInsets.symmetric(horizontal: 15),
|
||||||
style: Theme.of(context).textTheme.labelSmall,
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 15),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
margin: EdgeInsets.only(bottom: 5),
|
||||||
|
child: Text(_data.summary ?? ""),
|
||||||
),
|
),
|
||||||
),
|
Container(
|
||||||
Row(
|
margin: const EdgeInsets.only(bottom: 5),
|
||||||
spacing: 10,
|
child: Text(
|
||||||
children: [
|
"${formatDateUS(_data.createdAt!, withTime: true)} · Coach ${_data.agentName ?? ""}",
|
||||||
Expanded(
|
style: Theme.of(context).textTheme.labelSmall, // 小号文字
|
||||||
child: LinearProgressIndicator(
|
),
|
||||||
value: 0.5,
|
),
|
||||||
borderRadius: BorderRadius.circular(5),
|
Row(
|
||||||
|
spacing: 10,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: LinearProgressIndicator(
|
||||||
|
value: (_data.completedSteps! / _data.totalSteps!),
|
||||||
|
borderRadius: BorderRadius.circular(5),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
Text(
|
||||||
Text(
|
"${_data.completedSteps}/${_data.totalSteps}",
|
||||||
"0/7",
|
style: Theme.of(context).textTheme.labelSmall,
|
||||||
style: Theme.of(context).textTheme.labelSmall,
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
Icon(
|
||||||
Icon(
|
RemixIcons.arrow_right_s_line,
|
||||||
RemixIcons.arrow_right_s_line,
|
size: 30,
|
||||||
size: 30,
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
),
|
||||||
),
|
],
|
||||||
];
|
),
|
||||||
},
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _actionItem(String title, IconData icon) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(title),
|
||||||
|
Icon(icon, size: 20), // 右边图标
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
48
lib/page/plan/history/widgets/loading_box.dart
Normal file
48
lib/page/plan/history/widgets/loading_box.dart
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:plan/widgets/ui_kit/empty/index.dart';
|
||||||
|
|
||||||
|
class LoadingBox extends StatelessWidget {
|
||||||
|
final bool loading;
|
||||||
|
final bool isEmpty;
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const LoadingBox({
|
||||||
|
super.key,
|
||||||
|
required this.loading,
|
||||||
|
required this.isEmpty,
|
||||||
|
required this.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
///加载中
|
||||||
|
if (loading) {
|
||||||
|
return SliverFillRemaining(
|
||||||
|
child: Center(
|
||||||
|
child: CupertinoActivityIndicator(
|
||||||
|
radius: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
///空状态
|
||||||
|
if (isEmpty) {
|
||||||
|
return SliverFillRemaining(
|
||||||
|
child: Empty(
|
||||||
|
title: "No data",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
///正常
|
||||||
|
return SliverToBoxAdapter(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 15),
|
||||||
|
child: Container(
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,10 +20,14 @@ void showEditDescDialog(
|
|||||||
title: Text("编辑摘要"),
|
title: Text("编辑摘要"),
|
||||||
content: Padding(
|
content: Padding(
|
||||||
padding: EdgeInsets.only(top: 15),
|
padding: EdgeInsets.only(top: 15),
|
||||||
child: CupertinoTextField(
|
child: Container(
|
||||||
controller: controller,
|
constraints: BoxConstraints(minHeight: 40, maxHeight: 80),
|
||||||
focusNode: focusNode,
|
child: CupertinoTextField(
|
||||||
placeholder: "edit...",
|
controller: controller,
|
||||||
|
maxLines: null,
|
||||||
|
expands: true,
|
||||||
|
keyboardType: TextInputType.multiline,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
@@ -36,8 +40,10 @@ void showEditDescDialog(
|
|||||||
CupertinoDialogAction(
|
CupertinoDialogAction(
|
||||||
isDefaultAction: true,
|
isDefaultAction: true,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.pop();
|
if (controller.text.trim().isNotEmpty) {
|
||||||
onConfirm(controller.text);
|
context.pop();
|
||||||
|
onConfirm(controller.text);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
child: Text('确认'),
|
child: Text('确认'),
|
||||||
),
|
),
|
||||||
|
|||||||
132
lib/page/test/test_page.dart
Normal file
132
lib/page/test/test_page.dart
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class TestPage extends StatefulWidget {
|
||||||
|
const TestPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<TestPage> createState() => _TestPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TestPageState extends State<TestPage> {
|
||||||
|
final GlobalKey<AnimatedListState> _listKey = GlobalKey<AnimatedListState>();
|
||||||
|
List<Item> _list = [
|
||||||
|
Item("测试1", ""),
|
||||||
|
Item("测试2", ""),
|
||||||
|
Item("测试4", ""),
|
||||||
|
Item("测试5", ""),
|
||||||
|
];
|
||||||
|
|
||||||
|
void _add() {
|
||||||
|
_list.add(Item("测试${_list.length + 1}", ""));
|
||||||
|
_listKey.currentState?.insertItem(
|
||||||
|
_list.length - 1,
|
||||||
|
duration: Duration(milliseconds: 200),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _remove() {
|
||||||
|
final index = _list.length - 1; // 删除的索引
|
||||||
|
final removedItem = _list.removeAt(index); // 移除元素
|
||||||
|
|
||||||
|
_listKey.currentState?.removeItem(
|
||||||
|
index,
|
||||||
|
(context, animation) {
|
||||||
|
final curvedAnimation = CurvedAnimation(
|
||||||
|
parent: animation,
|
||||||
|
curve: Curves.easeInOut, // 这里设置曲线
|
||||||
|
);
|
||||||
|
return buildItem(removedItem, curvedAnimation);
|
||||||
|
},
|
||||||
|
duration: Duration(milliseconds: 200),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
void _update() {
|
||||||
|
if (_list.isNotEmpty) {
|
||||||
|
setState(() {
|
||||||
|
_list[0] = Item("测试1", "这这是新描述这是新描述这是新描述这是新描述这是新描述这是新描述这是新描述这是新描述这是新描述是新描述"); // 改变 desc
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text("长度${_list.length}"),
|
||||||
|
actions: [
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: _update,
|
||||||
|
child: Text("更新子元素"),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: _add,
|
||||||
|
child: Text("增加"),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: _remove,
|
||||||
|
child: Text("减少"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: AnimatedList(
|
||||||
|
key: _listKey,
|
||||||
|
itemBuilder: (_, index, animation) {
|
||||||
|
final item = _list[index];
|
||||||
|
// 新增元素的动画
|
||||||
|
final curvedAnimation = CurvedAnimation(
|
||||||
|
parent: animation,
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
);
|
||||||
|
return buildItem(item, curvedAnimation);
|
||||||
|
},
|
||||||
|
initialItemCount: _list.length,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildItem(Item item, CurvedAnimation curvedAnimation) {
|
||||||
|
return FadeTransition(
|
||||||
|
opacity: curvedAnimation,
|
||||||
|
child: SizeTransition(
|
||||||
|
sizeFactor: curvedAnimation,
|
||||||
|
axis: Axis.vertical,
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.all(15),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text("序号${item.title}"),
|
||||||
|
AnimatedSwitcher(
|
||||||
|
duration: Duration(milliseconds: 300),
|
||||||
|
transitionBuilder: (child, animation) {
|
||||||
|
return FadeTransition(
|
||||||
|
opacity: animation,
|
||||||
|
child: SizeTransition(
|
||||||
|
sizeFactor: animation,
|
||||||
|
axis: Axis.vertical,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: item.desc.isEmpty
|
||||||
|
? SizedBox.shrink(key: ValueKey("empty"))
|
||||||
|
: Text(
|
||||||
|
"描述${item.desc}",
|
||||||
|
key: ValueKey(item.desc), // ValueKey 用于 AnimatedSwitcher 判断变化
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Item {
|
||||||
|
final String title;
|
||||||
|
final String desc;
|
||||||
|
|
||||||
|
Item(this.title, this.desc);
|
||||||
|
}
|
||||||
@@ -21,4 +21,6 @@ class RoutePaths {
|
|||||||
|
|
||||||
///计划详情页
|
///计划详情页
|
||||||
static String planDetail([int? id]) => id != null ? "/planDetail/$id" : "/planDetail/:id";
|
static String planDetail([int? id]) => id != null ? "/planDetail/$id" : "/planDetail/:id";
|
||||||
|
|
||||||
|
static const test = "/test";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:plan/page/home/home_page.dart';
|
import 'package:plan/page/home/home_page.dart';
|
||||||
|
import 'package:plan/page/test/test_page.dart';
|
||||||
|
|
||||||
import '../../page/system/agree/agree_page.dart';
|
import '../../page/system/agree/agree_page.dart';
|
||||||
import '../../page/system/login/login_code_page.dart';
|
import '../../page/system/login/login_code_page.dart';
|
||||||
@@ -40,4 +41,10 @@ List<RouteType> baseRoutes = [
|
|||||||
return HomePage();
|
return HomePage();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
RouteType(
|
||||||
|
path: RoutePaths.test,
|
||||||
|
child: (state) {
|
||||||
|
return TestPage();
|
||||||
|
},
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,35 +1,53 @@
|
|||||||
/// 格式化日期时间
|
|
||||||
String formatDateUS(dynamic date, [String format = 'MM/DD/YYYY hh:mm:ss a']) {
|
|
||||||
DateTime dateTime;
|
|
||||||
|
|
||||||
if (date is String) {
|
|
||||||
dateTime = DateTime.tryParse(date) ?? DateTime.now();
|
// /// 格式化日期时间
|
||||||
} else if (date is DateTime) {
|
// String formatDateUS(dynamic date, [String format = 'MM/DD/YYYY hh:mm:ss a']) {
|
||||||
dateTime = date;
|
// DateTime dateTime;
|
||||||
|
//
|
||||||
|
// if (date is String) {
|
||||||
|
// dateTime = DateTime.tryParse(date) ?? DateTime.now();
|
||||||
|
// } else if (date is DateTime) {
|
||||||
|
// dateTime = date;
|
||||||
|
// } else {
|
||||||
|
// dateTime = DateTime.now();
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// final yyyy = dateTime.year.toString();
|
||||||
|
// final MM = dateTime.month.toString().padLeft(2, '0');
|
||||||
|
// final dd = dateTime.day.toString().padLeft(2, '0');
|
||||||
|
//
|
||||||
|
// // 12小时制
|
||||||
|
// final hour12 = (dateTime.hour % 12 == 0 ? 12 : dateTime.hour % 12).toString().padLeft(2, '0');
|
||||||
|
// final HH = dateTime.hour.toString().padLeft(2, '0'); // 24小时制备用
|
||||||
|
// final mm = dateTime.minute.toString().padLeft(2, '0');
|
||||||
|
// final ss = dateTime.second.toString().padLeft(2, '0');
|
||||||
|
// final ampm = dateTime.hour >= 12 ? 'PM' : 'AM';
|
||||||
|
//
|
||||||
|
// String result = format
|
||||||
|
// .replaceFirst(RegExp('YYYY'), yyyy)
|
||||||
|
// .replaceFirst(RegExp('MM'), MM)
|
||||||
|
// .replaceFirst(RegExp('DD'), dd)
|
||||||
|
// .replaceFirst(RegExp('hh'), hour12)
|
||||||
|
// .replaceFirst(RegExp('HH'), HH)
|
||||||
|
// .replaceFirst(RegExp('mm'), mm)
|
||||||
|
// .replaceFirst(RegExp('ss'), ss)
|
||||||
|
// .replaceFirst(RegExp('a'), ampm);
|
||||||
|
//
|
||||||
|
// return result;
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
String formatDateUS(String dateStr, {bool withTime = false}) {
|
||||||
|
// 假设后端返回的格式是 "2025-09-05T15:25:00Z"
|
||||||
|
final date = DateTime.parse(dateStr);
|
||||||
|
|
||||||
|
if (withTime) {
|
||||||
|
return DateFormat("MMM d, yyyy 'at' h:mm a", 'en_US').format(date.toLocal());
|
||||||
|
// 输出: Sep 5, 2025 at 11:25 PM (取本地时区)
|
||||||
} else {
|
} else {
|
||||||
dateTime = DateTime.now();
|
return DateFormat("MMM d, yyyy", 'en_US').format(date.toLocal());
|
||||||
|
// 输出: Sep 5, 2025
|
||||||
}
|
}
|
||||||
|
}
|
||||||
final yyyy = dateTime.year.toString();
|
|
||||||
final MM = dateTime.month.toString().padLeft(2, '0');
|
|
||||||
final dd = dateTime.day.toString().padLeft(2, '0');
|
|
||||||
|
|
||||||
// 12小时制
|
|
||||||
final hour12 = (dateTime.hour % 12 == 0 ? 12 : dateTime.hour % 12).toString().padLeft(2, '0');
|
|
||||||
final HH = dateTime.hour.toString().padLeft(2, '0'); // 24小时制备用
|
|
||||||
final mm = dateTime.minute.toString().padLeft(2, '0');
|
|
||||||
final ss = dateTime.second.toString().padLeft(2, '0');
|
|
||||||
final ampm = dateTime.hour >= 12 ? 'PM' : 'AM';
|
|
||||||
|
|
||||||
String result = format
|
|
||||||
.replaceFirst(RegExp('YYYY'), yyyy)
|
|
||||||
.replaceFirst(RegExp('MM'), MM)
|
|
||||||
.replaceFirst(RegExp('DD'), dd)
|
|
||||||
.replaceFirst(RegExp('hh'), hour12)
|
|
||||||
.replaceFirst(RegExp('HH'), HH)
|
|
||||||
.replaceFirst(RegExp('mm'), mm)
|
|
||||||
.replaceFirst(RegExp('ss'), ss)
|
|
||||||
.replaceFirst(RegExp('a'), ampm);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
@@ -45,7 +45,7 @@ class StreamUtils {
|
|||||||
}
|
}
|
||||||
//数据
|
//数据
|
||||||
var bufferText = ""; //吐出来的内容
|
var bufferText = ""; //吐出来的内容
|
||||||
|
final completer = Completer<void>();
|
||||||
//处理响应
|
//处理响应
|
||||||
response.data?.stream
|
response.data?.stream
|
||||||
.transform(_unit8Transformer())
|
.transform(_unit8Transformer())
|
||||||
@@ -63,6 +63,9 @@ class StreamUtils {
|
|||||||
}
|
}
|
||||||
Map<String, dynamic> dataJSON = jsonDecode(streamStr);
|
Map<String, dynamic> dataJSON = jsonDecode(streamStr);
|
||||||
//提取响应数据
|
//提取响应数据
|
||||||
|
if (dataJSON['choices'].length == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
Map<String, dynamic> choices = dataJSON['choices'][0];
|
Map<String, dynamic> choices = dataJSON['choices'][0];
|
||||||
//提取文字
|
//提取文字
|
||||||
var word = choices['delta']['content'];
|
var word = choices['delta']['content'];
|
||||||
@@ -80,12 +83,17 @@ class StreamUtils {
|
|||||||
},
|
},
|
||||||
onDone: () {
|
onDone: () {
|
||||||
onEnd?.call();
|
onEnd?.call();
|
||||||
|
completer.complete();
|
||||||
},
|
},
|
||||||
onError: (error) {
|
onError: (error) {
|
||||||
onError?.call();
|
onError?.call();
|
||||||
logger.e("流错误: $error");
|
logger.e("流错误: $error");
|
||||||
|
if (!completer.isCompleted) {
|
||||||
|
completer.completeError(error);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
return completer.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 将Uint8List转换为List<int>
|
/// 将Uint8List转换为List<int>
|
||||||
|
|||||||
@@ -456,6 +456,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.2"
|
version: "0.2.2"
|
||||||
|
intl:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: intl
|
||||||
|
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "0.19.0"
|
||||||
leak_tracker:
|
leak_tracker:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ dependencies:
|
|||||||
sign_in_with_apple: ^7.0.1
|
sign_in_with_apple: ^7.0.1
|
||||||
flutter_image_compress: ^2.4.0
|
flutter_image_compress: ^2.4.0
|
||||||
cached_network_image: ^3.4.1
|
cached_network_image: ^3.4.1
|
||||||
|
intl: ^0.19.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user