This commit is contained in:
zhutao
2025-09-05 18:00:26 +08:00
parent 70aa3e6ab6
commit 193d29b0ce
23 changed files with 1071 additions and 338 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 31 KiB

View File

@@ -9,7 +9,7 @@ class PlanStepDto {
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map["id"] = id;
map["id"] = id ?? 0;
map["step_icon"] = stepIcon;
map["step_content"] = stepContent;
map["step_explain"] = stepExplain;

View 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"] ?? "";
}
}

View File

@@ -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';
///初始化计划
@@ -8,3 +10,49 @@ Future<int> initPlanApi(String need, int agentId) async {
});
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,
});
}

View File

@@ -12,14 +12,14 @@ class PlanFormCard extends StatefulWidget {
}
class _PlanFormCardState extends State<PlanFormCard> {
final TextEditingController _inputController = TextEditingController();
final TextEditingController _inputController = TextEditingController(text: "");
void _handSubmit() {
if (_inputController.text.isEmpty) {
return;
}
context.push(
RoutePaths.planDetail(),
RoutePaths.planDetail(0),
extra: {
"name": _inputController.text,
},

View File

@@ -9,7 +9,9 @@ import 'package:remixicon/remixicon.dart';
import '../widgets/edit_desc_dialog.dart';
import 'widgets/avatar_card.dart';
import 'widgets/coach_message.dart';
import 'widgets/plan_list.dart';
import 'widgets/scroll_box.dart';
import 'widgets/suggested.dart';
class PlanDetailPage extends StatefulWidget {
final String? id;
@@ -27,6 +29,20 @@ class PlanDetailPage extends StatefulWidget {
class _PlanDetailPageState extends State<PlanDetailPage> {
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菜单
void _onPopupActionSelected(String value) {
@@ -54,16 +70,40 @@ class _PlanDetailPageState extends State<PlanDetailPage> {
Widget build(BuildContext context) {
return ChangeNotifierProvider<PlanDetailStore>(
create: (_) {
return PlanDetailStore(
planId: widget.id.toString(),
planContent: widget.planName ?? "",
showRoleTalk: widget.planName == null,
);
return store;
},
child: CupertinoPageScaffold(
child: Consumer<PlanDetailStore>(
child: SafeArea(
child: Column(
children: [
AvatarCard(),
Expanded(
child: Padding(
padding: EdgeInsets.all(15),
child: ScrollBox(
child: Container(
decoration: shadowDecoration,
child: CustomScrollView(
controller: scrollController,
slivers: [
CoachMessage(),
PlanList(),
SuggestedTitle(),
SuggestedList(),
],
),
),
),
),
),
],
),
),
builder: (context, store, child) {
return CupertinoPageScaffold(
backgroundColor: Colors.white,
navigationBar: CupertinoNavigationBar(
middle: Text('计划详情'),
middle: Text(store.planDetail.summary ?? ""),
trailing: Row(
mainAxisSize: MainAxisSize.min, // 关键Row 只占实际内容宽度
children: [
@@ -99,53 +139,9 @@ class _PlanDetailPageState extends State<PlanDetailPage> {
],
),
),
child: SafeArea(
child: Column(
children: [
AvatarCard(),
Expanded(
child: Padding(
padding: EdgeInsets.all(15),
child: ScrollBox(
child: Container(
decoration: shadowDecoration,
child: CustomScrollView(
slivers: [
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,
// ),
],
),
),
),
),
),
],
),
),
child: child!,
);
},
),
);
}

View File

@@ -1,17 +1,30 @@
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/endpoints/plan_api.dart';
import 'package:plan/utils/stream.dart';
class PlanDetailStore extends ChangeNotifier {
///构造函数
PlanDetailStore({
this.planContent = "",
this.planId = "",
bool showRoleTalk = true,
}) : _showRoleTalk = showRoleTalk;
required this.scrollController,
}) {
//如果没有id进行初始化
if (planId == "0") {
createPlan();
}else{
//获取详情
getPlanDetail();
}
}
///滚动控制器
ScrollController scrollController;
///角色话语是否显示
bool _showRoleTalk = true;
bool _showRoleTalk = false;
bool get showRoleTalk => _showRoleTalk;
@@ -27,11 +40,123 @@ class PlanDetailStore extends ChangeNotifier {
String planId = "";
///计划详情
PlanDetailDto planDetail = PlanDetailDto();
PlanDetailDto planDetail = PlanDetailDto(summary: "计划详情");
///流请求工具
StreamUtils streamUtils = StreamUtils();
///创建计划
void createPlan() async {
var id = await initPlanApi(planContent, 1);
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();
}
}

View File

@@ -15,9 +15,13 @@ class _AvatarCardState extends State<AvatarCard> with SingleTickerProviderStateM
late AnimationController _controller;
late Animation<double> _animation;
///对话框值
String _dialog = "";
@override
void initState() {
super.initState();
//初始化动画
_controller = AnimationController(
vsync: this,
duration: Duration(milliseconds: 300),
@@ -28,6 +32,7 @@ class _AvatarCardState extends State<AvatarCard> with SingleTickerProviderStateM
curve: Curves.easeInOut,
),
);
_initDialog();
}
@override
@@ -36,8 +41,31 @@ class _AvatarCardState extends State<AvatarCard> with SingleTickerProviderStateM
_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() {
var store = context.read<PlanDetailStore>();
if (store.planDetail.dialog == null) {
return;
}
setState(() {
store.showRoleTalk = !store.showRoleTalk;
if (store.showRoleTalk) {
@@ -71,10 +99,7 @@ class _AvatarCardState extends State<AvatarCard> with SingleTickerProviderStateM
borderRadius: BorderRadius.circular(3),
border: Border.all(color: Colors.black, width: 1),
),
child: Text(
"好的,让我们把学习软件开发这个目标分解成最简单的小步骤,这样你明天就能轻松开始行动",
style: TextStyle(fontSize: 12),
),
child: Text(_dialog, style: TextStyle(fontSize: 12)),
),
Positioned(
bottom: 0,

View File

@@ -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;
}

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

View File

@@ -1,21 +1,29 @@
import 'package:flutter/material.dart';
class ScrollBox extends StatelessWidget {
final ScrollController? scrollController;
final Widget child;
const ScrollBox({super.key, required this.child});
const ScrollBox({
super.key,
required this.child,
this.scrollController,
});
@override
Widget build(BuildContext context) {
return ScrollbarTheme(
data: ScrollbarThemeData(
thumbColor: WidgetStateProperty.all(Theme.of(context).colorScheme.surfaceContainerHigh),
thumbColor: WidgetStateProperty.all(
Theme.of(context).colorScheme.surfaceContainerHigh,
),
thickness: WidgetStateProperty.all(3),
crossAxisMargin: 3,
mainAxisMargin: 2,
radius: const Radius.circular(5),
),
child: Scrollbar(
controller: scrollController,
child: child,
),
);

View File

@@ -1,4 +1,6 @@
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';
///模块标题
@@ -7,7 +9,25 @@ class SuggestedTitle extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
var list = context.select<PlanDetailStore, List<String>>(
(store) => store.planDetail.suggestionsList,
);
if (list.isEmpty) {
return const SliverToBoxAdapter(
child: SizedBox(),
);
}
return SliverToBoxAdapter(
child: TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: 1), // 从透明到完全显示
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,
@@ -32,18 +52,97 @@ class SuggestedTitle extends StatelessWidget {
],
),
),
),
),
);
}
}
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
Widget build(BuildContext context) {
return Padding(
return SliverAnimatedList(
key: _listKey,
initialItemCount: _suggestionsList.length,
itemBuilder: (_, index, animation) {
final curvedAnimation = CurvedAnimation(
parent: animation,
curve: Curves.easeInOut,
);
return SuggestedItem(
title: _suggestionsList[index],
curvedAnimation: curvedAnimation,
);
},
);
}
}
///建议item
class SuggestedItem extends StatelessWidget {
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,
@@ -68,6 +167,8 @@ class SuggestedItem extends StatelessWidget {
),
],
),
),
),
);
}
}

View File

@@ -1,9 +1,12 @@
import 'package:flutter/cupertino.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 'widgets/history_item.dart';
import '../../../widgets/ui_kit/popup/popup_action.dart';
import 'widgets/loading_box.dart';
class PlanHistoryPage extends StatefulWidget {
const PlanHistoryPage({super.key});
@@ -16,12 +19,23 @@ class _PlanHistoryPageState extends State<PlanHistoryPage> {
///是否显示删除
bool _isDelete = false;
///数据
List<PlanItemDto> _record = [];
bool _isInit = true;
@override
void initState() {
super.initState();
_onRefresh();
}
///刷新
Future<void> _onRefresh() async {
//模拟网络请求
await Future.delayed(Duration(milliseconds: 1000));
//结束刷新
return Future.value(true);
var list = await getPlanListApi();
setState(() {
_record = list;
_isInit = false;
});
}
///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
Widget build(BuildContext context) {
@@ -44,19 +65,6 @@ class _PlanHistoryPageState extends State<PlanHistoryPage> {
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
navigationBar: CupertinoNavigationBar(
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: CustomScrollView(
@@ -69,15 +77,10 @@ class _PlanHistoryPageState extends State<PlanHistoryPage> {
onRefresh: _onRefresh,
),
//列表
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(15),
child: Container(
padding: EdgeInsets.symmetric(horizontal: 15),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
),
LoadingBox(
loading: _isInit,
isEmpty: _record.isEmpty,
child: ClipRRect(
child: CustomScrollView(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
@@ -85,23 +88,20 @@ class _PlanHistoryPageState extends State<PlanHistoryPage> {
SliverList.separated(
itemBuilder: (BuildContext context, int index) {
return HistoryItem(
item: _record[index],
showDelete: _isDelete,
onDelete: _confirmDelete,
);
},
separatorBuilder: (BuildContext context, int index) {
return Divider(
height: 1,
color: Theme.of(context).colorScheme.surfaceContainer,
);
return SizedBox(height: 15);
},
itemCount: 5,
itemCount: _record.length,
),
],
),
),
),
),
],
),
),

View File

@@ -1,15 +1,23 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.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/utils/format.dart';
import 'package:plan/widgets/business/delete_row_item.dart';
import 'package:remixicon/remixicon.dart';
import '../../widgets/edit_desc_dialog.dart';
class HistoryItem extends StatefulWidget {
final PlanItemDto item;
final bool showDelete;
final Function(int) onDelete;
const HistoryItem({
super.key,
required this.item,
this.showDelete = false,
required this.onDelete,
});
@@ -19,44 +27,99 @@ class HistoryItem extends StatefulWidget {
}
class _HistoryItemState extends State<HistoryItem> {
late PlanItemDto _data;
@override
void 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() {
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
Widget build(BuildContext context) {
return GestureDetector(
return CupertinoContextMenu(
actions: [
// 编辑摘要
CupertinoContextMenuAction(
onPressed: _handEdit,
child: _actionItem(
"Edit Summary",
CupertinoIcons.pencil,
),
),
CupertinoContextMenuAction(
isDestructiveAction: true,
onPressed: _handDelete,
child: _actionItem(
"Delete Summary",
CupertinoIcons.delete,
),
),
],
child: GestureDetector(
onTap: _goDetail,
child: Container(
width: double.infinity,
padding: EdgeInsets.symmetric(vertical: 15),
width: 400,
margin: EdgeInsets.symmetric(horizontal: 15),
padding: EdgeInsets.symmetric(horizontal: 15),
decoration: BoxDecoration(
color: Colors.white,
child: DeleteRowItem(
showDelete: widget.showDelete,
onDelete: () {
widget.onDelete(0);
},
builder: (_, __) {
return [
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("开始学习软件开发"),
child: Text(_data.summary ?? ""),
),
Container(
margin: EdgeInsets.only(bottom: 5),
margin: const EdgeInsets.only(bottom: 5),
child: Text(
"创建于 2025/9/3 9:40:51 教练:W教练",
style: Theme.of(context).textTheme.labelSmall,
"${formatDateUS(_data.createdAt!, withTime: true)} · Coach ${_data.agentName ?? ""}",
style: Theme.of(context).textTheme.labelSmall, // 小号文字
),
),
Row(
@@ -64,12 +127,12 @@ class _HistoryItemState extends State<HistoryItem> {
children: [
Expanded(
child: LinearProgressIndicator(
value: 0.5,
value: (_data.completedSteps! / _data.totalSteps!),
borderRadius: BorderRadius.circular(5),
),
),
Text(
"0/7",
"${_data.completedSteps}/${_data.totalSteps}",
style: Theme.of(context).textTheme.labelSmall,
),
],
@@ -82,10 +145,21 @@ class _HistoryItemState extends State<HistoryItem> {
size: 30,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
];
},
],
),
),
),
),
);
}
Widget _actionItem(String title, IconData icon) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(title),
Icon(icon, size: 20), // 右边图标
],
);
}
}

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

View File

@@ -20,10 +20,14 @@ void showEditDescDialog(
title: Text("编辑摘要"),
content: Padding(
padding: EdgeInsets.only(top: 15),
child: Container(
constraints: BoxConstraints(minHeight: 40, maxHeight: 80),
child: CupertinoTextField(
controller: controller,
focusNode: focusNode,
placeholder: "edit...",
maxLines: null,
expands: true,
keyboardType: TextInputType.multiline,
),
),
),
actions: [
@@ -36,8 +40,10 @@ void showEditDescDialog(
CupertinoDialogAction(
isDefaultAction: true,
onPressed: () {
if (controller.text.trim().isNotEmpty) {
context.pop();
onConfirm(controller.text);
}
},
child: Text('确认'),
),

View 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);
}

View File

@@ -21,4 +21,6 @@ class RoutePaths {
///计划详情页
static String planDetail([int? id]) => id != null ? "/planDetail/$id" : "/planDetail/:id";
static const test = "/test";
}

View File

@@ -1,4 +1,5 @@
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/login/login_code_page.dart';
@@ -40,4 +41,10 @@ List<RouteType> baseRoutes = [
return HomePage();
},
),
RouteType(
path: RoutePaths.test,
child: (state) {
return TestPage();
},
),
];

View File

@@ -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) {
dateTime = date;
// /// 格式化日期时间
// 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) {
// 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 {
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;
}

View File

@@ -45,7 +45,7 @@ class StreamUtils {
}
//数据
var bufferText = ""; //吐出来的内容
final completer = Completer<void>();
//处理响应
response.data?.stream
.transform(_unit8Transformer())
@@ -63,6 +63,9 @@ class StreamUtils {
}
Map<String, dynamic> dataJSON = jsonDecode(streamStr);
//提取响应数据
if (dataJSON['choices'].length == 0) {
continue;
}
Map<String, dynamic> choices = dataJSON['choices'][0];
//提取文字
var word = choices['delta']['content'];
@@ -80,12 +83,17 @@ class StreamUtils {
},
onDone: () {
onEnd?.call();
completer.complete();
},
onError: (error) {
onError?.call();
logger.e("流错误: $error");
if (!completer.isCompleted) {
completer.completeError(error);
}
},
);
return completer.future;
}
/// 将Uint8List转换为List<int>

View File

@@ -456,6 +456,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
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:
dependency: transitive
description:

View File

@@ -29,6 +29,7 @@ dependencies:
sign_in_with_apple: ^7.0.1
flutter_image_compress: ^2.4.0
cached_network_image: ^3.4.1
intl: ^0.19.0
dev_dependencies:
flutter_test: