初始化

This commit is contained in:
谢宇宁
2026-09-15 15:44:13 +07:00
commit a23ba685e9
1065 changed files with 101122 additions and 0 deletions
+38
View File
@@ -0,0 +1,38 @@
import 'package:flutter/material.dart';
import 'package:hgdj/hj_utils/api_service/ai_service.dart';
import 'package:hgdj/tools_base/base_list_controller.dart';
import 'ai_sub_type/ai_function_logic.dart';
import 'models/ai_mod_list_model.dart';
class AIFaceCategoryLogic extends ListBaseLogic<AICategoryMod> {
final AiType type; //图片换脸 / 视频换脸
TabController? tabCtr;
AIFaceCategoryLogic({this.type = AiType.imageChangeFace});
@override
void onReady() {
super.onReady();
loadData();
}
@override
void onClose() {
tabCtr?.dispose();
super.onClose();
}
void loadData() async {
dataList ??= [];
final resp = await AIService.getModelListV2(type: type);
if (resp != null) {
dataList?.addAll(resp.categoryList ?? []);
// 重新创建前先释放旧 controller,避免重复 loadData 时泄漏
tabCtr?.dispose();
tabCtr = TabController(length: dataList?.length ?? 0, vsync: this);
}
update();
}
}
+97
View File
@@ -0,0 +1,97 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgdj/hj_utils/screen.dart';
import 'package:hgdj/tools_base/loading/loading_center_widget.dart';
import 'package:hgdj/tools_base/widget/keep_alive_widget.dart';
import '../../assets_tool/app_colors.dart';
import '../../tools_base/indicator/custom_tab_indicator.dart';
import 'ai_change_face_logic.dart';
import 'ai_face_sub_page.dart';
import 'ai_sub_type/ai_function_logic.dart';
/// AI 换脸首页:根据 type 区分图片换脸 / 视频换脸,按分类列出 Tab,每 Tab 一个子页
class AIChangeFacePage extends StatelessWidget {
final AiType type; //图片换脸 / 视频换脸
const AIChangeFacePage({super.key, this.type = AiType.imageChangeFace});
// 图片/视频换脸两个子页作为 AI 首页并存 Tab 同时存活,用 type 做 tag 隔离两个 logic 实例
String get _tag => 'ai_change_face_${type.name}';
@override
Widget build(BuildContext context) {
return GetBuilder<AIFaceCategoryLogic>(
tag: _tag,
init: AIFaceCategoryLogic(type: type),
builder: (logic) {
if (logic.isLoading) return LoadingCenterWidget();
if (logic.isEmptyData) return CErrorWidget();
return Column(
children: [
12.sizeBoxH,
_buildTabbar(logic),
12.sizeBoxH,
Expanded(
child: TabBarView(
controller: logic.tabCtr,
children: List.generate(
logic.dataList?.length ?? 0,
(index) => AIFaceChangeSubPage(
type: type, //图片换脸 / 视频换脸
mod: logic.dataList?[index],
).keepAlive,
),
),
)
],
);
},
);
}
Widget _buildTabbar(AIFaceCategoryLogic logic) {
// 内层 GetBuilder 仅做局部刷新,复用同 tag 的 logic 实例
return GetBuilder<AIFaceCategoryLogic>(
tag: _tag,
id: 'tab',
builder: (_) {
return TabBar(
padding: EdgeInsets.symmetric(horizontal: 16),
tabAlignment: TabAlignment.start,
controller: logic.tabCtr,
isScrollable: true,
onTap: (value) => logic.update(['tab']),
tabs: List.generate(
logic.dataList?.length ?? 0,
(index) => Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(logic.dataList?[index].name ?? ''),
Container(
margin: EdgeInsets.symmetric(horizontal: 14),
width: 1,
height: 8,
color: (index != ((logic.dataList?.length ?? 0) - 1))
? Colors.white.withValues(alpha: .3)
: Colors.transparent,
)
],
),
),
labelPadding: EdgeInsets.symmetric(horizontal: 0, vertical: 4),
labelColor: Colors.white,
labelStyle: const TextStyle(fontSize: 12),
unselectedLabelColor: Colors.white.withValues(alpha: 0.70),
unselectedLabelStyle: const TextStyle(fontSize: 12),
indicatorPadding: EdgeInsets.only(right: 30),
indicator: CustomIndicator(
width: 16,
height: 4,
borderRadius: BorderRadius.circular(16),
color: AppColors.actionRed,
),
);
},
);
}
}
@@ -0,0 +1,90 @@
import 'package:flutter/widgets.dart';
import 'package:get/get.dart';
import 'package:hgdj/tools_base/toast.dart';
import '../../../alert/mine/vip_level_dialog.dart';
import '../../../config/config.dart';
import '../../../hj_utils/api_service/ai_service.dart';
import '../../../tools_base/global_store/store.dart';
import '../../../tools_base/loading/loading_helper.dart';
import '../ai_sub_type/ai_function_logic.dart';
import '../models/ai_change_face_video_model.dart';
import '../widgets/ai_mod_cell.dart';
/// ai绘画
class AiPaintLogic extends AiFunctionBaseLogic {
late final TextEditingController titleCtr = TextEditingController();
final aspectRatios = ["1:1", "4:3", "3:4", "16:9", "9:16"];
int selectIndex = 0; //比例选择
bool isFold = false;
int chooseItemIndex = 0; //图片选择
List<AiChangeFaceVideoMod> models = [];
AiPaintLogic(super.modList);
@override
void onInit() {
super.onInit();
models = modList?.aiTextToImgMod ?? [];
}
@override
void onClose() {
titleCtr.dispose();
super.onClose();
}
@override
Future<void> submit() async {
if ((globalStore.wallet?.amount ?? 0) <
(int.tryParse(Config.aiDrawPrice) ?? 0)) {
showVipLevelDialog(
"当前金币不足",
buttonTitle: '我知道了',
desc: '充值金币 即可继续生成',
vipEvent: () => Get.back(),
);
return;
}
if (titleCtr.text.isEmpty) {
showToast('请输入绘画描述~');
return;
}
if (models.isEmpty) {
showToast('暂无模版,请退出重试');
return;
}
LoadingHelper.showLoading();
final result = await AIService.generateTextToImage(
aspectRatios[selectIndex],
models[chooseItemIndex].styleType ?? 0,
titleCtr.text,
shareStatus: shareToAiSquare,
shareTitle: titleCtr.text,
);
LoadingHelper.dismissLoading();
if (result) {
showToast("提交成功~");
selectIndex = 0;
chooseItemIndex = 0;
isFold = false;
titleCtr.text = '';
update();
} else {
showToast("提交失败");
}
}
Widget instanceChild(int index) {
return AIModCell(
mod: models[index],
isSelected: chooseItemIndex == index,
onTap: () {
chooseItemIndex = index;
update();
},
);
}
}
+300
View File
@@ -0,0 +1,300 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgdj/assets_tool/app_colors.dart';
import 'package:hgdj/assets_tool/images.dart';
import 'package:hgdj/config/config.dart';
import 'package:hgdj/hj_utils/screen.dart';
import '../../../tools_base/widget/net_image_widget.dart';
import '../models/ai_mod_list_model.dart';
import '../widgets/ai_draw_text_field.dart';
import 'ai_paint_logic.dart';
//ai绘画页面
class AiPaintPage extends StatelessWidget {
final AiModList? aiModList;
const AiPaintPage({super.key, this.aiModList});
@override
Widget build(BuildContext context) {
return Container(
color: Color(0xff030F18),
child: GetBuilder<AiPaintLogic>(
init: AiPaintLogic(aiModList),
builder: (logic) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
children: [
Expanded(
child: SingleChildScrollView(
keyboardDismissBehavior:
ScrollViewKeyboardDismissBehavior.onDrag,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
12.sizeBoxH,
Row(
children: [
Text(
"形象描述(必填)",
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
],
),
10.sizeBoxH,
Container(
margin: EdgeInsets.symmetric(horizontal: 2),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: .05),
borderRadius: BorderRadius.all(Radius.circular(5)),
boxShadow: [
BoxShadow(
color: Colors.white.withValues(alpha: .05),
blurRadius: 1, //阴影模糊程度
spreadRadius: 1.0, //阴影扩散程度
)
],
),
child: AIDrawTextField(
controller: logic.titleCtr,
hintText: "示例:“女,大学生,身高165cm,体重50kg,穿着JK制服.”",
maxLength: 100,
height: 112,
),
),
14.sizeBoxH,
Text(
"选择长宽比例",
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
20.sizeBoxH,
SizedBox(
height: 28,
child: SingleChildScrollView(
child: Row(
children: List.generate(
logic.aspectRatios.length,
(index) => Padding(
padding: EdgeInsets.only(right: 12),
child: InkWell(
enableFeedback: false,
onTap: () {
logic.selectIndex = index;
logic.update();
},
child: Container(
padding:
EdgeInsets.symmetric(horizontal: 8),
alignment: Alignment.center,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: logic.selectIndex == index
? [
AppColors.actionRed,
AppColors.actionRed
]
: [
Colors.white
.withValues(alpha: .05),
Colors.white
.withValues(alpha: .05),
]),
borderRadius: BorderRadius.circular(4),
color: logic.selectIndex == index
? null
: Color(0xFF7C7C7C),
),
height: 28,
child: Text(
logic.aspectRatios[index],
style: TextStyle(
color: logic.selectIndex == index
? Colors.white
: Colors.white
.withValues(alpha: .5),
fontSize: logic.selectIndex == index
? 14
: 12,
fontWeight: FontWeight.w400,
),
),
),
),
),
),
),
),
),
18.sizeBoxH,
0.5.line,
18.sizeBoxH,
Container(
padding: EdgeInsets.only(bottom: logic.isFold ? 0 : 10),
margin: EdgeInsets.symmetric(horizontal: 1),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.05),
borderRadius: BorderRadius.all(Radius.circular(5)),
boxShadow: [
BoxShadow(
color: Colors.white.withValues(alpha: .05),
blurRadius: 1, //阴影模糊程度
spreadRadius: 1.0, //阴影扩散程度
)
],
),
child: _getStyleView(logic),
),
18.sizeBoxH,
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"处理一张照片的费用是 ",
style: TextStyle(
color: Color(0xff999999),
fontSize: 12,
),
),
Text(
'${(int.tryParse(Config.aiDrawPrice) ?? 0)}金币',
style: TextStyle(
color: AppColors.actionRed,
fontSize: 12,
),
)
],
),
InkWell(
enableFeedback: false,
onTap: () => logic.submit(),
child: Container(
height: 44,
margin: EdgeInsets.symmetric(vertical: 12),
alignment: Alignment.center,
decoration: BoxDecoration(
color: AppColors.actionRed,
borderRadius: BorderRadius.circular(3),
),
child: Text(
"立即提交",
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w500,
),
),
),
),
],
),
),
),
],
),
),
),
);
}
Widget _getStyleView(AiPaintLogic logic) {
return logic.isFold
? InkWell(
enableFeedback: false,
onTap: () {
logic.isFold = !logic.isFold;
logic.update();
},
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 10.0, vertical: 10),
child: Row(
children: [
_buildStyleTitle(),
Spacer(),
NetworkImageLoader(
imageUrl: logic.models.length > 1
? logic.models[logic.chooseItemIndex].cover ?? ""
: '',
width: 32,
height: 32,
),
10.sizeBoxW,
Transform.rotate(
angle: pi,
child: Image.asset('ai_draw_narrow.png'.aiPath, width: 10),
),
],
),
),
)
: Column(
children: [
InkWell(
enableFeedback: false,
onTap: () {
logic.isFold = !logic.isFold;
logic.update();
},
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 10.0, vertical: 10),
child: Row(
children: [
_buildStyleTitle(),
Spacer(),
Image.asset('ai_draw_narrow.png'.aiPath, width: 10),
],
),
),
),
GridView.builder(
physics: NeverScrollableScrollPhysics(),
shrinkWrap: true,
padding: EdgeInsets.symmetric(horizontal: 10),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
childAspectRatio: 105 / 131,
),
itemCount: logic.models.length,
itemBuilder: (context, index) {
return logic.instanceChild(index);
},
),
],
);
}
_buildStyleTitle() {
return Row(
children: [
Image.asset(
'ai_draw_style.png'.aiPath,
width: 15,
color: Colors.white,
),
5.sizeBoxW,
Text(
"风格",
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
],
);
}
}
+73
View File
@@ -0,0 +1,73 @@
import 'package:hgdj/hj_utils/api_service/ai_service.dart';
import 'package:hgdj/tools_base/base_list_controller.dart';
import 'ai_sub_type/ai_function_logic.dart';
import 'models/ai_mod_list_model.dart';
class AIFaceTemplateLogic extends ListBaseLogic<TemplateModel> {
final AICategoryMod? mod;
final AiType type; //图片换脸 / 视频换脸
List<String> sortType = ['上架时间', '使用次数', '价格排序']; //排序
int sortIndex = 0; //排序 0-上架时间, 1-使用次数, 2-价格排序
bool ascending = true; //是否是升序
int get crossAxisCount => type.isVideoFace ? 2 : 3;
double get spacing => type.isVideoFace ? 7 : 5;
double get ratio => type.isVideoFace ? 168 / 95 : 111 / 148;
AIFaceTemplateLogic({this.mod, this.type = AiType.imageChangeFace});
@override
void onReady() {
super.onReady();
loadData();
}
void loadData() async {
dataList ??= [];
final resp =
await AIService.getModelListV2(type: type, categoryId: mod?.id ?? '');
if (resp != null) {
dataList?.addAll(resp.templateList ?? []);
//默认上架时间升序
dataList?.sort((obj1, obj2) => obj1.timestamp.compareTo(obj2.timestamp));
}
update();
}
//排序
void changeSortAction(int index) {
if (index == sortIndex) {
ascending = !ascending;
} else {
sortIndex = index;
ascending = true;
}
//本地排序
switch (sortIndex) {
case 0:
dataList?.sort(
(obj1, obj2) => ascending
? obj1.timestamp.compareTo(obj2.timestamp)
: obj2.timestamp.compareTo(obj1.timestamp),
);
case 1:
dataList?.sort(
(obj1, obj2) => ascending
? (obj1.usedCount ?? 0).compareTo(obj2.usedCount ?? 0)
: (obj2.usedCount ?? 0).compareTo(obj1.usedCount ?? 0),
);
case 2:
dataList?.sort(
(obj1, obj2) => ascending
? (obj1.coin ?? 0).compareTo(obj2.coin ?? 0)
: (obj2.coin ?? 0).compareTo(obj1.coin ?? 0),
);
default:
}
update();
}
}
+128
View File
@@ -0,0 +1,128 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgdj/assets_tool/images.dart';
import 'package:hgdj/hj_utils/screen.dart';
import 'package:hgdj/tools_base/loading/loading_center_widget.dart';
import 'package:hgdj/tools_base/widget/net_image_widget.dart';
import '../../assets_tool/app_colors.dart';
import 'ai_face_sub_logic.dart';
import 'ai_mod_detail_page.dart';
import 'ai_sub_type/ai_function_logic.dart';
import 'models/ai_mod_list_model.dart';
/// AI 换脸分类子页:顶部 3 个排序按钮 + 网格列表,按 type 区分图片/视频换脸
class AIFaceChangeSubPage extends StatelessWidget {
final AICategoryMod? mod;
final AiType type; //图片换脸 / 视频换脸
const AIFaceChangeSubPage(
{super.key, this.mod, this.type = AiType.imageChangeFace});
// 多分类子页作为 TabBarView 的 keepAlive children 并存,用 type+分类id 隔离各自 logic 实例
String get _tag => 'ai_face_sub_${type.name}_${mod?.id}';
@override
Widget build(BuildContext context) {
return GetBuilder<AIFaceTemplateLogic>(
tag: _tag,
init: AIFaceTemplateLogic(mod: mod, type: type),
builder: (logic) {
if (logic.isLoading) return LoadingCenterWidget();
if (logic.isEmptyData) {
return CErrorWidget(retryOnTap: () => logic.loadData());
}
return Column(
children: [
Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: _buildSortView(logic),
),
12.sizeBoxH,
Expanded(
child: GridView.builder(
padding: EdgeInsets.fromLTRB(16, 0, 16, 20),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: logic.crossAxisCount,
mainAxisSpacing: logic.spacing,
crossAxisSpacing: logic.spacing,
childAspectRatio: logic.ratio,
),
itemCount: logic.dataList?.length ?? 0,
itemBuilder: (context, index) {
TemplateModel? model = logic.dataList?[index];
return InkWell(
enableFeedback: false,
onTap: () => Get.to(AIModDetailPage(model!)),
child: NetworkImageLoader(imageUrl: model?.cover ?? ''),
);
},
),
),
],
);
},
);
}
Widget _buildSortView(AIFaceTemplateLogic logic) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildSortItem(logic.sortType[0], logic.ascending, 0, logic),
20.sizeBoxW,
_buildSortItem(logic.sortType[1], logic.ascending, 1, logic),
20.sizeBoxW,
_buildSortItem(logic.sortType[2], logic.ascending, 2, logic),
],
);
}
Widget _buildSortItem(
String title, bool ascending, int sortIndex, AIFaceTemplateLogic logic) {
final bool sel = logic.sortIndex == sortIndex;
return InkWell(
enableFeedback: false,
onTap: () => logic.changeSortAction(sortIndex),
child: Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 3),
decoration: BoxDecoration(
color: sel ? AppColors.actionRed : Color(0x1AFFFFFF),
borderRadius: BorderRadius.circular(3),
),
child: Row(
children: [
Text(
title,
style: TextStyle(
fontSize: 12,
color: Colors.white,
fontWeight: FontWeight.w500,
height: 1.5),
),
5.sizeBoxW,
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildArrow(active: sel && ascending),
4.sizeBoxH,
Transform.rotate(
angle: -pi, child: _buildArrow(active: sel && !ascending)),
],
),
],
),
),
);
}
Widget _buildArrow({required bool active}) {
return Image.asset(
'ai_narrow.png'.aiPath,
width: 4.8,
color: active ? Color(0xffFFD460) : null,
);
}
}
@@ -0,0 +1,77 @@
import 'package:get/get.dart';
import 'package:hgdj/hj_utils/api_service/ai_service.dart';
import 'package:hgdj/tools_base/debug_log.dart';
import 'package:hgdj/tools_base/loading/loading_alert_widget.dart';
import 'package:hgdj/tools_base/toast.dart';
import '../models/ai_girl_resp_model.dart';
import '../widgets/ai_girl_sheet.dart';
import 'ai_h5_page.dart';
class AIGrilFriendLogic extends GetxController {
AIGirlFriendBalanceModel? balanceModel;
AIGirlFriendUrlModel? urlModel;
AIGirlFriendCurrencys? currencys;
RxBool open = true.obs;
RxString tips = ''.obs;
@override
void onReady() {
super.onReady();
loadData();
onFetchChargeList();
}
loadData() async {
try {
balanceModel = await AIService.getBalance({});
open.value = true;
} catch (e) {
debugLog(e);
open.value = false;
}
update(['balance']);
}
//跳转url
onFetchJumpUrl() async {
try {
LoadingAlertWidget.show();
urlModel = await AIService.getMateUrl();
LoadingAlertWidget.cancel();
} catch (e) {
LoadingAlertWidget.cancel();
debugLog(e);
}
if (urlModel?.url != null) {
await Get.to(AiH5Page(webUrl: urlModel?.url));
loadData();
} else {
showToast('未知链接');
}
}
//充值操作
onRechargeBottomSheet() async {
if (currencys != null && currencys?.list?.isNotEmpty == true) {
Get.bottomSheet(AIGirlSheet(list: currencys?.list),
isScrollControlled: true);
} else {
await onFetchChargeList();
if (currencys?.list?.isNotEmpty != true) {
showToast("暂未配置充值数据");
}
}
}
//获取充值列表
Future onFetchChargeList({bool showLoading = false}) async {
if (showLoading) LoadingAlertWidget.show();
try {
currencys = await AIService.getMateCurrencies();
} catch (e) {
debugLog(e);
}
if (showLoading) LoadingAlertWidget.cancel();
}
}
@@ -0,0 +1,155 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgdj/assets_tool/images.dart';
import '../../../hj_utils/widget_util.dart';
import '../../../tools_base/unique_tag_mixin.dart';
import 'ai_girl_friend_logic.dart';
// Ai女友
class AIGrilFriendPage extends StatefulWidget {
const AIGrilFriendPage({super.key});
@override
State<AIGrilFriendPage> createState() => _AIGrilFriendPageState();
}
class _AIGrilFriendPageState extends State<AIGrilFriendPage>
with UniqueTagMixin {
@override
Widget build(BuildContext context) {
return GetBuilder<AIGrilFriendLogic>(
tag: uniqueTag,
init: AIGrilFriendLogic(),
builder: (logic) => Scaffold(
body: Stack(
alignment: Alignment.center,
children: [
Image.asset(
'ai_girl_friend_bg.webp'.aiPath,
fit: BoxFit.fill,
width: Get.width,
height: Get.height,
),
Positioned(
bottom: 125,
left: 10,
right: 10,
child: GestureDetector(
onTap: () => logic.onFetchJumpUrl(),
child: Image.asset(
'ai_girl_friend_btn.webp'.aiPath,
),
),
),
Positioned(
top: 0,
height: 101,
child: Stack(
children: [
Image.asset(
'ai_girl_friend_top.webp'.aiPath,
width: Get.width,
fit: BoxFit.fitWidth,
),
_buildBanlance(logic),
],
),
),
Positioned.fill(
child: Obx(() => logic.open.value
? Container()
: Stack(
children: [
Container(
color: Colors.black.withValues(alpha: .75),
child: Center(
child: Obx(
() => Text(
logic.tips.value,
style:
textStyle(17, Colors.white, FontWeight.w500),
),
)),
),
Positioned(
left: 10,
top: 60,
child: GestureDetector(
child: GestureDetector(
onTap: () => Get.back(),
child: const Icon(Icons.arrow_back_ios,
size: 24, color: Colors.white),
),
),
)
],
)),
),
],
),
),
);
}
Widget _buildBanlance(AIGrilFriendLogic logic) {
return Positioned(
left: 10,
right: 10,
bottom: 14,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
GestureDetector(
child: GestureDetector(
onTap: () => Get.back(),
child: const Icon(Icons.arrow_back_ios,
size: 24, color: Colors.white),
),
),
Container(
padding: EdgeInsets.all(1),
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(100),
border: Border.all(color: Color(0xff2A5298)),
),
child: Row(
children: [
const SizedBox(width: 12),
GetBuilder<AIGrilFriendLogic>(
id: 'balance',
builder: (_) {
if (_.balanceModel != null) {
return Text(
'积分:${_.balanceModel?.balance ?? 0}',
style: textStyle(18, Colors.white, FontWeight.w400),
);
} else {
return const SizedBox(
width: 22,
child: CupertinoActivityIndicator(
color: Colors.white,
radius: 8,
),
);
}
},
),
const SizedBox(width: 10),
GestureDetector(
onTap: () => logic.onRechargeBottomSheet(),
child: Image.asset(
'ai_girl_friend_topup.webp'.aiPath,
width: 76,
),
)
],
),
)
],
),
);
}
}
+60
View File
@@ -0,0 +1,60 @@
import 'dart:collection';
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:get/get.dart';
import 'package:hgdj/assets_tool/app_colors.dart';
import '../../../hj_utils/widget_util.dart';
import '../../web_page/h5_webview_settings.dart';
class AiH5Page extends StatefulWidget {
final String? webUrl;
const AiH5Page({super.key, this.webUrl});
@override
State<StatefulWidget> createState() {
return _YHYSH5ViewState();
}
}
class _YHYSH5ViewState extends State<AiH5Page> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Stack(
fit: StackFit.expand,
children: [
InAppWebView(
initialUrlRequest:
URLRequest(url: WebUri(widget.webUrl ?? "")), //h5的url
initialSettings: h5WebViewSettings,
initialUserScripts: UnmodifiableListView<UserScript>([]),
),
Positioned(
bottom: 70,
right: 10,
width: 44,
height: 44,
child: GestureDetector(
onTap: () => Get.back(),
child: Container(
alignment: Alignment.center,
decoration: BoxDecoration(
color: AppColors.primaryHighColor,
borderRadius: BorderRadius.circular(22),
),
child: Text(
'退出',
style: textStyle(14, Colors.black, FontWeight.w500),
),
),
),
),
],
),
),
);
}
}
+98
View File
@@ -0,0 +1,98 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgdj/hj_page/pre_sale/pre_sale_provider.dart';
import 'package:hgdj/hj_utils/api_service/ai_service.dart';
import 'package:hgdj/tools_base/global_store/store.dart';
import 'package:hgdj/tools_base/widget/keep_alive_widget.dart';
import '../../config/config.dart';
import '../../hj_model/splash/domain_source_model.dart';
import './ai_change_face_page.dart';
import './ai_draw/ai_paint_page.dart';
import './ai_image_to_video/ai_main_itv_page.dart';
import './ai_novel/ai_novel_page.dart';
import './ai_sub_type/ai_function_logic.dart';
import './ai_sub_type/ai_strip_sub_page.dart';
import './models/ai_mod_list_model.dart';
import 'ai_square/ai_square_page.dart';
class AiHomeLogic extends GetxController
with GetSingleTickerProviderStateMixin {
bool isShowLoading = true;
final AiType? aiType;
// int tabIndex;
AiHomeLogic({this.aiType});
AiModList? aiModList;
late List<AISwitchConf> menus = [];
late TabController tabCtr;
// 跳转相关模块的亚模块
void jumpSubModule(int index) {
tabCtr.index = index;
}
@override
void onInit() {
super.onInit();
List<AISwitchConf>? aiTypes = Config.aiTypes;
//1.删除未开启的
aiTypes?.removeWhere((model) => model.isOpen == false);
//2.排序
aiTypes?.sort(
(m1, m2) => (m1.sort ?? 0).compareTo(m2.sort ?? 0),
);
menus.addAll(aiTypes ?? []);
//3.获取到默认进来的排序
int? initialIndex = aiTypes?.indexWhere((model) => model.aiType == aiType);
initialIndex = max(0, initialIndex ?? 0);
tabCtr = TabController(
initialIndex: initialIndex, length: menus.length, vsync: this)
..addListener(() {
if (!tabCtr.indexIsChanging) {
update(['tab']);
}
});
}
@override
void onReady() {
super.onReady();
presaleProvider.refreshStatus(); //刷新预售权益
globalStore.refreshWallet(); //刷新钱包
loadData();
}
void loadData() async {
aiModList = await AIService.getModelList();
aiModList ??= AiModList();
isShowLoading = false;
update();
}
Widget subPage(AISwitchConf model) {
switch (model.aiType) {
case AiType.imageToVideo:
return AiMainItvPage(aiModList: aiModList).keepAlive; //图生视频
case AiType.aiPaint:
return AiPaintPage(aiModList: aiModList).keepAlive; //ai绘画
case AiType.autoStrip:
return AIStripSubPage().keepAlive; //脱衣
case AiType.videoChangeFace:
return AIChangeFacePage(type: AiType.videoChangeFace).keepAlive; //视频换脸
case AiType.imageChangeFace:
return AIChangeFacePage(type: AiType.imageChangeFace).keepAlive; //图片换脸
case AiType.aiNovel:
return AINovelSubPage().keepAlive; //ai小说
case AiType.aiMate:
return AiSquarePage().keepAlive; //ai女友
default:
return Container();
}
}
}
+138
View File
@@ -0,0 +1,138 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgdj/assets_tool/app_colors.dart';
import 'package:hgdj/hj_utils/screen.dart';
import 'package:hgdj/tools_base/loading/loading_center_widget.dart';
import 'package:hgdj/tools_base/widget/keep_alive_widget.dart';
import '../../hj_utils/widget_util.dart';
import 'ai_home_logic.dart';
import 'ai_record/ai_main_record_page.dart';
/// AI 科技首页:顶部功能 tab 网格 + 各功能子页
class AiHomePage extends StatelessWidget {
const AiHomePage({super.key});
@override
Widget build(BuildContext context) {
return GetBuilder<AiHomeLogic>(
init: AiHomeLogic(),
builder: (logic) {
return Stack(
children: [
Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
title: Text('AI科技'),
backgroundColor: AppColors.primaryColor,
actions: [
Center(
child: InkWell(
enableFeedback: false,
onTap: () => Get.to(
() => AIMainRecordPage(
aiType: logic.menus[logic.tabCtr.index].aiType,
),
),
child: Text(
'记录',
style: textStyle(
14,
Colors.white.withValues(alpha: 0.55),
FontWeight.w400),
),
),
),
16.sizeBoxW
],
),
body: _buildContent(logic))
],
);
});
}
/// 内容区:加载中 / 加载失败 / tab 网格 + 子页
Widget _buildContent(AiHomeLogic logic) {
if (logic.isShowLoading) return const LoadingCenterWidget();
if (logic.aiModList == null) {
return CErrorWidget(retryOnTap: () => logic.loadData());
}
return Column(
children: [
_buildTabBar(logic),
Expanded(
child: TabBarView(
controller: logic.tabCtr,
children: logic.menus
.map<Widget>((e) => logic.subPage(e).keepAlive)
.toList(),
),
)
],
);
}
/// 顶部功能 tab 网格(id:'tab' 局部刷新选中态)
Widget _buildTabBar(AiHomeLogic logic) {
return GetBuilder<AiHomeLogic>(
init: logic,
id: 'tab',
builder: (_) {
return GridView.builder(
shrinkWrap: true,
padding: EdgeInsets.symmetric(horizontal: 16),
physics: NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 105 / 26,
crossAxisSpacing: 14,
mainAxisSpacing: 14,
),
itemCount: logic.menus.length,
itemBuilder: (context, index) {
final isSelected = logic.tabCtr.index == index;
final menu = logic.menus[index];
return InkWell(
enableFeedback: false,
onTap: () {
logic.tabCtr.animateTo(index);
logic.update(['tab']);
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 250),
curve: Curves.easeOut,
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(3),
color: isSelected
? Color(0xffF68804)
: Colors.white.withValues(alpha: .1),
border: isSelected
? null
: Border.all(
width: 1, color: Colors.white.withValues(alpha: .35)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
menu.img ?? '',
width: 16,
color: Colors.white,
),
4.sizeBoxW,
Text(
menu.aiTypeName ?? '',
style: textStyle(12, Colors.white, FontWeight.w400),
),
],
),
),
);
},
);
},
);
}
}
@@ -0,0 +1,78 @@
// 图片转视频
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgdj/config/config.dart';
import 'package:hgdj/hj_utils/api_service/ai_service.dart';
import 'package:hgdj/tools_base/file_upload/file_upload_tool.dart';
import 'package:hgdj/tools_base/global_store/store.dart';
import 'package:hgdj/tools_base/loading/loading_alert_widget.dart';
import 'package:hgdj/tools_base/toast.dart';
import 'package:hgdj/tools_base/widget/common_alert.dart';
import '../../../alert/mine/vip_level_dialog.dart';
import '../../../hj_model/splash/ads_model.dart';
class AiItvLogic extends GetxController {
final localPicList = <String>[]; //已选本地图片路径,与 PicPicker 共享
final AdsInfoModel? model;
double get aspectRatio => 408 / 310;
bool shareToAiSquare = true;
/// 图片转视频模版
TextEditingController editingCtr = TextEditingController();
AiItvLogic({this.model});
Future<void> submit() async {
if ((globalStore.wallet?.amount ?? 0) <
(int.tryParse(Config.aiVideoPrice) ?? 0)) {
showVipLevelDialog(
"当前金币不足",
buttonTitle: '我知道了',
desc: '充值金币 即可继续生成',
vipEvent: () => Get.back(),
);
return;
}
aiImageToVideo();
}
//AI 图片转视频
aiImageToVideo() async {
if (localPicList.isEmpty) {
CommonAlert.show(title: "提示", content: "请选择图片", showCancel: false);
return;
}
await FileUploadTool().uploadImagesWithProgress(
localPicList,
onFailure: () => showToast("图片上传失败"),
onSuccess: (imageArr) async {
LoadingAlertWidget.show(title: "正在更新数据...");
try {
final result = await AIService.generateImgVideo(
imageArr.first,
shareStatus: shareToAiSquare,
shareTitle: editingCtr.text,
mid: model?.id,
);
if (result) {
showToast("提交成功~");
localPicList.clear();
globalStore.refreshWallet();
update();
} else {
showToast("提交失败");
}
} on DioException catch (e) {
showToast(e.message.toString());
} catch (e) {
showToast(e.toString());
} finally {
LoadingAlertWidget.cancel();
}
},
);
}
}
@@ -0,0 +1,156 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgdj/assets_tool/app_colors.dart';
import 'package:hgdj/config/config.dart';
import 'package:hgdj/hj_utils/screen.dart';
import 'package:hgdj/tools_base/banner/ads_banner_widget.dart';
import '../../../hj_model/splash/ads_model.dart';
import '../widgets/pic_picker.dart';
import 'ai_itv_logic.dart';
class AiItvPage extends StatelessWidget {
final AdsInfoModel? model;
const AiItvPage({super.key, this.model});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Color(0xff030F18),
appBar: AppBar(
title: Text(
model?.title ?? '图生视频',
style: TextStyle(
color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600),
),
centerTitle: false,
titleSpacing: -20,
),
body: GetBuilder<AiItvLogic>(
init: AiItvLogic(model: model),
global: false,
builder: (logic) {
return Column(
children: [
Expanded(
child: SingleChildScrollView(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.fromLTRB(0, 12, 0, 0),
child: Row(
children: [
PicPicker(
width: 111,
height: 111,
picList: logic.localPicList,
),
SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"注意事项:",
style: TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
4.sizeBoxH,
Text(
'''1、素材仅供AI使用,绝无外泄风险,请放心使用. \n2、素材需清晰,小于2MB,上传间隔大于60秒. \n3、本功能不支持多人图片 \n4、生成失败退回金币,若违规作废. 5、禁止使用未成年图片!''',
style: TextStyle(
color: Color(0xff656565),
fontSize: 11,
),
),
],
),
),
],
),
),
18.sizeBoxH,
0.5.line,
18.sizeBoxH,
Text(
"案列鉴赏",
style: TextStyle(
fontSize: 18,
color: Colors.white,
fontWeight: FontWeight.w600),
),
12.sizeBoxH,
ClipRRect(
borderRadius: BorderRadius.circular(10),
child: AspectRatio(
aspectRatio: 343 / 224,
child: model == null
? SizedBox.shrink()
: AIBannerWidget(
models: [model!],
),
),
),
18.sizeBoxH,
0.5.line,
18.sizeBoxH,
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"处理一张照片的费用是 ",
style: TextStyle(
color: Colors.white.withValues(alpha: .6),
fontSize: 14),
),
Text(
"${Config.aiVideoPrice}金币",
style: TextStyle(
color: AppColors.actionRed,
fontSize: 14,
),
),
],
),
18.sizeBoxH,
InkWell(
enableFeedback: false,
onTap: () => logic.submit(),
child: Container(
height: 44,
// margin: EdgeInsets.symmetric(horizontal: 16),
alignment: Alignment.center,
decoration: BoxDecoration(
color: AppColors.actionRed,
borderRadius: BorderRadius.circular(3),
),
child: Text(
"立即提交",
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w500,
),
),
),
),
// AiShareToSquare(
// logic.editingCtr,
// valueChanged: (value) => logic.shareToAiSquare = value,
// ),
// 20.sizeBoxH,
],
),
),
),
],
);
}),
);
}
}
@@ -0,0 +1,65 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../hj_model/splash/ads_model.dart';
import '../../../tools_base/loading/loading_center_widget.dart';
import '../../../tools_base/widget/net_image_widget.dart';
import '../models/ai_mod_list_model.dart';
import 'ai_itv_page.dart';
/// AI 图生视频主页:模板两列网格
class AiMainItvPage extends StatelessWidget {
final AiModList? aiModList;
const AiMainItvPage({super.key, this.aiModList});
@override
Widget build(BuildContext context) {
if (aiModList == null) return LoadingCenterWidget(); //数据未就绪
final list = aiModList!.aiImgToVideoMod;
if (list == null || list.isEmpty) return CErrorWidget(); //无模板数据
return GridView.builder(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 167 / 223,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
itemCount: list.length,
itemBuilder: (context, index) => _buildItem(list[index]),
);
}
/// 单个模板卡片:封面图 + 底部标题胶囊
Widget _buildItem(AdsInfoModel item) {
return InkWell(
enableFeedback: false,
onTap: () => Get.to(AiItvPage(model: item)),
child: Stack(
alignment: Alignment.center,
children: [
Positioned.fill(
child: NetworkImageLoader(imageUrl: item.newUrl ?? ''),
),
Positioned(
bottom: 6,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(999),
gradient: LinearGradient(colors: [
Color(0xffFF4D4D),
Color(0xffFF6E6E),
]),
),
child: Text(
item.title ?? '',
style: TextStyle(color: Colors.white, fontSize: 14),
),
),
)
],
),
);
}
}
+312
View File
@@ -0,0 +1,312 @@
import 'package:easy_rich_text/easy_rich_text.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgdj/assets_tool/app_colors.dart';
import 'package:hgdj/assets_tool/images.dart';
import 'package:hgdj/hj_page/pre_sale/pre_sale_provider.dart';
import 'package:hgdj/hj_utils/screen.dart';
import 'package:hgdj/tools_base/global_store/store.dart';
import 'package:hgdj/tools_base/widget/net_image_widget.dart';
import 'package:provider/provider.dart';
import '../../hj_model/video_model.dart';
import '../../hj_utils/widget_util.dart';
import '../mine/widgets/gradient_text.dart';
import '../video/simple_video_player_page.dart';
import 'ai_sub_type/ai_function_logic.dart';
import 'models/ai_mod_list_model.dart';
import 'widgets/pic_picker.dart';
//ai换脸模版详情页
class AIModDetailPage extends StatelessWidget {
final TemplateModel modMod;
const AIModDetailPage(this.modMod, {super.key});
@override
Widget build(BuildContext context) {
return GetBuilder<AiFaceDetailLogic>(
init: AiFaceDetailLogic(null, mod: modMod),
builder: (logic) {
return Scaffold(
appBar: AppBar(
title: Text(logic.mod.title ?? ''),
centerTitle: false,
titleSpacing: -10,
),
body: Stack(
children: [
Column(
children: [
Expanded(child: _buildContent(logic)),
72.sizeBoxH,
],
),
Positioned(
bottom: 0,
left: 0,
right: 0,
child: InkWell(
enableFeedback: false,
onTap: () => logic.submit(),
child: Container(
height: 44,
alignment: Alignment.center,
margin: EdgeInsets.fromLTRB(16, 0, 16, 12),
decoration: BoxDecoration(
color: AppColors.actionRed,
borderRadius: BorderRadius.circular(3),
),
child: Text(
'立即提交',
style: TextStyle(
fontSize: 16,
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
),
),
)
],
),
);
});
}
Widget _buildContent(AiFaceDetailLogic logic) {
return SingleChildScrollView(
padding: EdgeInsets.fromLTRB(16, 12, 16, 60),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Stack(
alignment: Alignment.center,
children: [
AspectRatio(
aspectRatio: 343 / 193,
child: NetworkImageLoader(
imageUrl: logic.mod.cover ?? '',
borderRadius: 8,
fit: logic.isChangeVideo ? BoxFit.fitWidth : BoxFit.fitHeight,
),
),
//视频换脸
if (logic.isChangeVideo)
InkWell(
enableFeedback: false,
onTap: () {
VideoModel videoModel = VideoModel()
..sourceURL = logic.mod.m3u8Url;
Get.to(
SimpleVideoPlayerPage(
videoUrl: videoModel.realVideoUrl,
title: logic.mod.title ?? '',
),
);
},
child: Image.asset(
'circle_play.webp'.videoPath,
width: 50,
),
)
],
),
18.sizeBoxH,
.5.line,
18.sizeBoxH,
Text(
"注意事项:",
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
8.sizeBoxH,
Text(
'''1. 选择一张人脸清晰,不得有任何遮挡的照片上传(注意:只含一个人物和脸部,图片不能过暗)
2. 选择一个心仪的视频或图片模板,点击生成,生成时间需要3-5分钟,耐心等待。(图片模板可自行上传)
3. 在右上角记录查看生成进度,生成成功后可以点击进行下载,也可以在线观看。
4. 按照上方操作,有问题随时联系在线客服进行处理。
5. 不支持多人图片,禁止未成年人图片''',
style: TextStyle(
color: Color(0xff656565),
fontSize: 12,
height: 18 / 12,
),
),
12.sizeBoxH,
_buildExample(),
18.sizeBoxH,
0.5.line,
18.sizeBoxH,
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
"上传脸部信息",
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
12.sizeBoxW,
Text(
"图片大小请低于2Mb",
style: TextStyle(
color: Color(0xff656565),
fontSize: 12,
fontWeight: FontWeight.w400,
),
),
],
),
12.sizeBoxH,
PicPicker(
width: 111,
height: 111,
picList: logic.localPicList,
),
18.sizeBoxH,
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [_priceInfo(logic)],
),
],
),
);
}
// 免费次数 / 处理费用 / 抵扣券(视频换脸)信息,钱包变化用 Consumer 局部刷新
Widget _priceInfo(AiFaceDetailLogic logic) {
return Consumer<GlobalStore>(builder: (_, provider, __) {
final total = provider.wallet?.aiUndressFreeTimes ?? 0;
final presaleCount = presaleProvider.remain?.todayAiUndressCount ?? 0;
final todayFreeCount = provider.wallet?.todayAiFreeTimes ?? 0;
//今日免费次数
final toDaytotal = todayFreeCount + presaleCount;
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (!logic.isChangeVideo) ...[
Text(
'你当前免费体验为$total次数',
style: TextStyle(
color: Color(0xff989898),
fontSize: 12,
),
),
12.sizeBoxH,
Text(
'当日免费$toDaytotal次数',
style: TextStyle(
color: Color(0xff989898),
fontSize: 12,
),
),
],
12.sizeBoxH,
EasyRichText(
'处理一张照片的费用是 ${logic.price}金币',
defaultStyle: TextStyle(
color: Color(0xff989898),
fontSize: 14,
fontWeight: FontWeight.w400),
patternList: [
EasyRichTextPattern(
targetString: '${logic.price}金币',
style: TextStyle(color: AppColors.actionRed),
)
],
),
if (logic.isChangeVideo) ...[
if (logic.coupon != null) ...[
12.sizeBoxH,
EasyRichText(
'已抵扣 ${logic.coupon?.goodsValue ?? 0}金币',
defaultStyle: TextStyle(
color: Colors.white.withValues(alpha: .55),
fontSize: 14,
fontWeight: FontWeight.w400),
patternList: [
EasyRichTextPattern(
targetString: '${logic.coupon?.goodsValue ?? 0}金币',
style: TextStyle(color: AppColors.actionRed),
)
],
),
],
12.sizeBoxH,
Row(
children: [
InkWell(
enableFeedback: false,
onTap: () => logic.showCoupon(),
child: GradientText(
logic.coupon == null ? '使用抵扣券' : '更换抵扣券',
gradient: LinearGradient(
colors: [
Color(0xff35DEBC),
Color(0xff22BB9C),
],
),
style: TextStyle(fontSize: 14),
),
),
if (logic.coupon != null) ...[
12.sizeBoxW,
InkWell(
enableFeedback: false,
onTap: () {
//取消优惠券
logic.coupon = null;
logic.update();
},
child: Text(
'取消',
style: textStyle(14, Colors.white.withValues(alpha: .55),
FontWeight.w400),
),
)
]
],
),
],
],
);
});
}
Widget _buildExample() {
return Row(children: [
_buildExampleItem('正面无遮挡', "ai_changeface_right.webp".aiPath),
20.sizeBoxW,
_buildExampleItem('不遮挡脸部', "ai_changeface_wrong_1.webp".aiPath),
20.sizeBoxW,
_buildExampleItem('不遮挡眼睛', "ai_changeface_wrong_2.webp".aiPath),
]);
}
Widget _buildExampleItem(String title, String img) {
return Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
AspectRatio(
aspectRatio: 1,
child: Image.asset(img),
),
14.sizeBoxH,
Text(
title,
style: TextStyle(
color: Color(0xff989898),
fontSize: 14,
),
),
],
),
);
}
}
@@ -0,0 +1,22 @@
import 'package:flutter/material.dart';
import '../models/ai_record_model.dart';
class AiNovelDetailPage extends StatelessWidget {
final AiRecordModel model;
const AiNovelDetailPage(this.model, {super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('小说详情')),
body: SingleChildScrollView(
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 12),
child: Text(
model.content ?? '',
style: TextStyle(color: Colors.white.withValues(alpha: .8), fontSize: 14, height: 1.5),
),
),
);
}
}
@@ -0,0 +1,56 @@
//ai小说
import 'package:flutter/material.dart';
import 'package:hgdj/tools_base/loading/loading_helper.dart';
import 'package:hgdj/tools_base/toast.dart';
import '../../../../hj_utils/api_service/ai_service.dart';
import '../ai_sub_type/ai_function_logic.dart';
class AINovelLogic extends AiFunctionBaseLogic {
late final personTextCtr = TextEditingController();
late final addressTextCtr = TextEditingController();
late final detailTextCtr = TextEditingController();
late final storylineTextCtr = TextEditingController();
final mods = <String>['AI小艺', 'AI小萌'];
int selectIndex = 0;
AINovelLogic(super.modList);
@override
void onClose() {
personTextCtr.dispose();
addressTextCtr.dispose();
detailTextCtr.dispose();
storylineTextCtr.dispose();
super.onClose();
}
@override
Future<void> submit() async {
if (storylineTextCtr.text.isEmpty) {
showToast('请输入故事情节描述~');
return;
}
LoadingHelper.showLoading();
final result = await AIService.generateNovel(
storylineTextCtr.text,
characterSetting: personTextCtr.text,
locationScene: addressTextCtr.text,
details: detailTextCtr.text,
modelType: selectIndex + 1,
);
LoadingHelper.dismissLoading();
if (result) {
showToast('提交成功~');
storylineTextCtr.clear();
personTextCtr.clear();
addressTextCtr.clear();
detailTextCtr.clear();
selectIndex = 0;
update();
} else {
showToast('提交失败');
}
}
}
+216
View File
@@ -0,0 +1,216 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgdj/assets_tool/app_colors.dart';
import 'package:hgdj/config/config.dart';
import 'package:hgdj/hj_utils/screen.dart';
import '../widgets/ai_draw_text_field.dart';
import 'ai_novel_logic.dart';
class AINovelSubPage extends StatelessWidget {
const AINovelSubPage({super.key});
@override
Widget build(BuildContext context) {
return Container(
color: Color(0xff030F18),
// 用默认 globalglobal:false 下 widget 销毁不会 Get.deleteonClose 不触发,输入框 controller 释放不掉
child: GetBuilder<AINovelLogic>(
init: AINovelLogic(null),
builder: (logic) {
return Column(
children: [
Expanded(
child: SingleChildScrollView(
keyboardDismissBehavior:
ScrollViewKeyboardDismissBehavior.onDrag,
padding: EdgeInsets.symmetric(horizontal: 6).copyWith(top: 12),
child: Column(
children: [
_buildInputView(
logic.personTextCtr,
),
12.sizeBoxH,
_buildInputView(
logic.addressTextCtr,
title: '地点场景(选填)',
hint: '示例:办公室、酒吧',
),
12.sizeBoxH,
_buildInputView(
logic.storylineTextCtr,
title: '故事情节(必填)',
hint: '故事大致情节,例如:35岁女强人在酒吧遇到跳钢管舞的男模后,欲火焚身、欲罢不能。',
maxLength: 200,
),
12.sizeBoxH,
_buildInputView(
logic.detailTextCtr,
title: '细节说明(选填)',
hint: '例如:请详细描写女主身材美貌,与霸总的做爱过程',
maxLength: 200,
),
12.sizeBoxH,
Align(
alignment: Alignment.centerLeft,
child: Text(
"选择模型",
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
),
10.sizeBoxH,
SizedBox(
height: 28,
child: SingleChildScrollView(
child: Row(
children: List.generate(
logic.mods.length,
(index) => Padding(
padding: EdgeInsets.only(right: 12),
child: InkWell(
enableFeedback: false,
onTap: () {
logic.selectIndex = index;
logic.update();
},
child: Container(
padding: EdgeInsets.symmetric(horizontal: 8),
alignment: Alignment.center,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: logic.selectIndex == index
? [
AppColors.actionRed,
AppColors.actionRed
]
: [
Colors.white
.withValues(alpha: .05),
Colors.white
.withValues(alpha: .05),
]),
borderRadius: BorderRadius.circular(4),
color: logic.selectIndex == index
? null
: Color(0xFF7C7C7C),
),
height: 28,
child: Text(
logic.mods[index],
style: TextStyle(
color: logic.selectIndex == index
? Colors.white
: Colors.white.withValues(alpha: .5),
fontSize: 14,
fontWeight: FontWeight.w400,
),
),
),
),
),
),
),
),
),
18.sizeBoxH,
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"生成一次AI小说费用是 ",
style: TextStyle(
color: Colors.white.withValues(alpha: .9),
fontSize: 14,
),
),
Text(
'${Config.aiNovelPrice}金币',
style: TextStyle(
color: AppColors.actionRed,
fontSize: 14,
),
)
],
),
InkWell(
enableFeedback: false,
onTap: () => logic.submit(),
child: Container(
height: 44,
margin: EdgeInsets.symmetric(vertical: 12),
alignment: Alignment.center,
decoration: BoxDecoration(
color: AppColors.actionRed,
borderRadius: BorderRadius.circular(3),
),
child: Text(
"立即提交",
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w500,
),
),
),
),
],
),
)),
// 0.5.line,
// 10.sizeBoxH,
],
).paddingSymmetric(horizontal: 10);
},
),
);
}
_buildInputView(
TextEditingController textCtr, {
String title = '人物设定(选填)',
String hint = '示例:总裁、公主、小萝莉.',
int maxLength = 20,
}) {
return Column(
children: [
Row(
children: [
Text(
title,
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
],
),
10.sizeBoxH,
Container(
margin: EdgeInsets.symmetric(horizontal: 2),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.05),
borderRadius: BorderRadius.all(Radius.circular(5)),
boxShadow: [
BoxShadow(
color: Colors.white.withValues(alpha: .05),
blurRadius: 1, //阴影模糊程度
spreadRadius: 1.0, //阴影扩散程度
)
],
),
child: AIDrawTextField(
controller: textCtr,
hintText: hint,
maxLength: maxLength,
height: 112,
),
),
],
);
}
}
@@ -0,0 +1,37 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../config/config.dart';
import '../../../hj_model/splash/domain_source_model.dart';
import '../ai_sub_type/ai_function_logic.dart';
class AIMainRecordLogic extends GetxController with GetTickerProviderStateMixin {
final AiType? aiType;
AIMainRecordLogic({this.aiType});
final List<AISwitchConf> menus = [];
late TabController tabCtr;
@override
void onInit() {
super.onInit();
// 拷贝一份再过滤/排序,避免原地改动全局 Config.aiTypes 影响其他页面
final aiTypes = List<AISwitchConf>.from(Config.aiTypes ?? []);
//1.删除未开启的和 AI 女友
aiTypes.removeWhere((model) => model.isOpen == false || model.aiType == AiType.aiMate);
//2.排序
aiTypes.sort((m1, m2) => (m1.sort ?? 0).compareTo(m2.sort ?? 0));
menus.addAll(aiTypes);
//3.定位默认进入的 tab
final initialIndex = max(0, aiTypes.indexWhere((model) => model.aiType == aiType));
tabCtr = TabController(initialIndex: initialIndex, length: menus.length, vsync: this);
}
@override
void onClose() {
tabCtr.dispose();
super.onClose();
}
}
@@ -0,0 +1,79 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgdj/tools_base/indicator/custom_tab_indicator.dart';
import 'package:hgdj/tools_base/widget/keep_alive_widget.dart';
import '../../../hj_model/splash/domain_source_model.dart';
import '../ai_sub_type/ai_function_logic.dart';
import 'ai_main_record_logic.dart';
import 'ai_record_page.dart';
/// AI 生成记录首页:按已开启的 AI 功能列 Tab,每 Tab 一个对应类型的记录子页
class AIMainRecordPage extends StatelessWidget {
final AiType? aiType;
const AIMainRecordPage({super.key, this.aiType});
@override
Widget build(BuildContext context) {
return GetBuilder<AIMainRecordLogic>(
init: AIMainRecordLogic(aiType: aiType),
builder: (logic) {
return Scaffold(
appBar: AppBar(title: Text('生成记录')),
body: Column(
children: [
_buildTabbar(logic),
Expanded(
child: TabBarView(
controller: logic.tabCtr,
children: logic.menus.map((e) => _subPage(e)).toList(),
),
),
],
),
);
},
);
}
// 所有 AI 功能统一用 AiRecordPage,内部按 aiType 区分接口/列表/item
Widget _subPage(AISwitchConf model) {
final type = model.aiType;
if (type == null) return Container();
return AiRecordPage(aiType: type).keepAlive;
}
Widget _buildTabbar(AIMainRecordLogic logic) {
return Container(
height: 36,
color: Colors.black,
child: TabBar(
tabAlignment: TabAlignment.center,
controller: logic.tabCtr,
tabs: List.generate(
logic.tabCtr.length,
(index) => Padding(
padding: const EdgeInsets.fromLTRB(12, 12, 12, 4),
child: Text(logic.menus[index].aiTypeName ?? ""),
),
),
isScrollable: true,
labelPadding: EdgeInsets.zero,
labelColor: Colors.white.withValues(alpha: .9),
labelStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
unselectedLabelColor: Colors.white.withValues(alpha: .35),
unselectedLabelStyle:
const TextStyle(fontSize: 14, fontWeight: FontWeight.w400),
indicator: CustomIndicator(
isGradient: true,
width: 13,
height: 3,
borderRadius: BorderRadius.circular(1.5),
),
),
);
}
}
@@ -0,0 +1,118 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgdj/hj_model/list_base_model.dart';
import 'package:hgdj/hj_utils/api_service/ai_service.dart';
import 'package:hgdj/tools_base/toast.dart';
import 'package:hgdj/tools_base/widget/common_alert.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import '../ai_sub_type/ai_function_logic.dart';
import '../models/ai_record_model.dart';
import '../widgets/ai_image_record.dart';
import '../widgets/ai_novel_record.dart';
import '../widgets/ai_paint_record.dart';
import '../widgets/ai_video_record.dart';
/// AI 生成记录 logic:一个类按 aiType 区分业务(接口 / 删除 / item widget
/// 覆盖脱衣 / 视频换脸 / 图片换脸 / 小说 / 图生视频 / 绘画六类
class AIRecordLogic extends GetxController {
AIRecordLogic(this.aiType, {this.sort = 1});
final AiType aiType;
final int sort; // 1排队 2成功 3失败,按状态固定,由所属状态页构造传入
final dataSource = <AiRecordModel>[];
int page = 1;
RefreshController? refreshCtr;
bool isLoading = true;
@override
void onReady() {
super.onReady();
fetchPageData();
}
Future<void> fetchPageData({bool isRefresh = true}) async {
if (isRefresh) page = 1;
try {
final res = await _fetchList(page);
isLoading = false;
if (isRefresh) {
dataSource.clear();
refreshCtr?.refreshCompleted();
}
dataSource.addAll(res?.list ?? []);
(res?.hasNext ?? false)
? refreshCtr?.loadComplete()
: refreshCtr?.loadNoData();
page += 1;
} catch (e) {
isLoading = false;
refreshCtr?.refreshCompleted();
refreshCtr?.loadComplete();
}
update();
}
// 按 aiType 调对应记录接口(status 即 sort:1排队 2成功 3失败)
Future<ListBaseModel<AiRecordModel>?> _fetchList(int page) {
switch (aiType) {
case AiType.videoChangeFace: // 视频换脸:sort(1/2/3) 映射成接口状态码(0/1/-1)
return AIService.getChangeFaceList(page, 10, [0, 1, -1][sort - 1]);
case AiType.imageChangeFace: // 图片换脸
return AIService.getImgList(page, 10, sort);
case AiType.aiNovel: // 小说
return AIService.getNovelList(page, 10, sort);
case AiType.imageToVideo: // 图生视频
return AIService.getImgVideoList(page, 10, sort);
case AiType.aiPaint: // ai绘画(文生图)
return AIService.getTextToImageList(page, 10, sort);
default: // 脱衣
return AIService.getUndressList(page, 10, sort);
}
}
Widget instanceChild(int index) {
final model = dataSource[index];
switch (aiType) {
case AiType.aiNovel:
return AINovelRecord(model,
onDeleteCallback: () => deleteRecord(index));
case AiType.imageToVideo:
return AIVideoRecord(model,
status: sort, onDeleteCallback: () => deleteRecord(index));
case AiType.aiPaint:
return AIPaintRecord(model,
status: sort, onDeleteCallback: () => deleteRecord(index));
default: //脱衣 / 视频换脸 / 图片换脸
return AIImageRecord(model,
aiType: aiType, onDeleteCallback: () => deleteRecord(index));
}
}
Future<bool> deleteRecord(int index) async {
if (!await CommonAlert.show(content: '是否删除该生成记录?')) return false;
if (!await _deleteBill(dataSource[index].id)) return false;
dataSource.removeAt(index);
showToast('删除成功');
update();
return true;
}
// 按 aiType 调对应删除接口
Future<bool> _deleteBill(String? id) {
switch (aiType) {
case AiType.videoChangeFace:
return AIService.deleteChangeFace(id);
case AiType.imageChangeFace:
return AIService.deleteImg(id);
case AiType.aiNovel:
return AIService.deleteNovel(id);
case AiType.imageToVideo:
return AIService.deleteImgVideo(id);
case AiType.aiPaint:
return AIService.deleteTextToImage(id);
default:
return AIService.deleteUndress(id);
}
}
}
@@ -0,0 +1,122 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgdj/hj_utils/screen.dart';
import 'package:hgdj/tools_base/loading/loading_center_widget.dart';
import 'package:hgdj/tools_base/refresh/pull_refresh.dart';
import 'package:hgdj/tools_base/widget/keep_alive_widget.dart';
import '../ai_sub_type/ai_function_logic.dart';
import 'ai_record_logic.dart';
const _subTypes = ['排队', '成功', '失败'];
/// 某个 AI 功能的生成记录子页:排队/成功/失败 3 个 keepalive 状态页,可横滑切换
class AiRecordPage extends StatefulWidget {
final AiType aiType; // 脱衣/视频换脸/图片换脸/小说
const AiRecordPage({super.key, required this.aiType});
@override
State<AiRecordPage> createState() => _AiRecordPageState();
}
class _AiRecordPageState extends State<AiRecordPage>
with SingleTickerProviderStateMixin {
late final TabController _tabCtr =
TabController(length: _subTypes.length, vsync: this);
@override
void dispose() {
_tabCtr.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
_buildTabBar(),
Expanded(
child: TabBarView(
controller: _tabCtr,
children: List.generate(
_subTypes.length,
(index) =>
_AiRecordStatusList(aiType: widget.aiType, status: index + 1)
.keepAlive,
),
),
)
],
);
}
// 排队/成功/失败 状态 Tab
Widget _buildTabBar() {
return TabBar(
controller: _tabCtr,
tabs: List.generate(
_subTypes.length,
(index) => Container(
padding: EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
border: index != 2
? Border(
right: BorderSide(
width: .5, color: Colors.white.withValues(alpha: .1)),
)
: null,
),
child: Text(_subTypes[index]),
),
),
isScrollable: true,
padding: EdgeInsets.only(top: 10, left: 7),
labelPadding: EdgeInsets.zero,
tabAlignment: TabAlignment.start,
unselectedLabelStyle:
TextStyle(color: Colors.white.withValues(alpha: .35), fontSize: 14),
labelStyle:
TextStyle(color: Colors.white.withValues(alpha: .9), fontSize: 14),
indicator: const BoxDecoration(),
);
}
}
/// 单个状态(排队/成功/失败)的记录列表,独立 logic 实例 + 独立分页/刷新
class _AiRecordStatusList extends StatelessWidget {
final AiType aiType;
final int status; // 1排队 2成功 3失败
const _AiRecordStatusList({required this.aiType, required this.status});
String get _tag => 'airec_${aiType.name}_$status';
@override
Widget build(BuildContext context) {
return GetBuilder<AIRecordLogic>(
tag: _tag,
init: AIRecordLogic(aiType, sort: status),
builder: (logic) {
return pullYsRefresh(
onInit: (ctr) => logic.refreshCtr = ctr,
onRefresh: (_) => logic.fetchPageData(),
onLoading: (_) => logic.fetchPageData(isRefresh: false),
child: _buildList(logic),
);
},
);
}
Widget _buildList(AIRecordLogic logic) {
if (logic.isLoading) return LoadingCenterWidget();
if (logic.dataSource.isEmpty) return CErrorWidget();
return ListView.separated(
padding: EdgeInsets.only(left: 16, right: 16, top: 18),
separatorBuilder: (_, index) => 12.sizeBoxH,
itemCount: logic.dataSource.length,
itemBuilder: (BuildContext context, int index) =>
logic.instanceChild(index),
);
}
}
@@ -0,0 +1,52 @@
import 'package:get/get.dart';
import 'package:hgdj/hj_utils/api_service/ai_service.dart';
import 'package:hgdj/tools_base/base_list_controller.dart';
import '../ai_home_logic.dart';
import '../ai_mod_detail_page.dart';
import '../ai_sub_type/ai_function_logic.dart';
import '../models/ai_square_model.dart';
class AiSquareLogic extends ListBaseLogic<AISquareItemModel> {
@override
void onReady() {
super.onReady();
loadData();
}
//isRefresh=true 下拉刷新 / false 上拉加载更多
void loadData({bool isRefresh = true}) =>
fetchData(isRefresh: isRefresh, fetch: _fetch);
Future<(List<AISquareItemModel>?, bool)> _fetch(int page) async {
final resp = await AIService.getPlazaList(page);
return (resp?.list, resp?.hasNext ?? false);
}
//「做同款」:脱衣/图生视频/绘画直接切到对应 Tab,换脸类先查模版还在不在
void makeTheSameStyle(AISquareItemModel model) {
final logic = Get.find<AiHomeLogic>();
switch (AiTypeCode.fromServerCode(model.type)) {
case AiType.autoStrip:
logic.jumpSubModule(3);
case AiType.imageToVideo:
logic.jumpSubModule(1);
case AiType.aiPaint:
logic.jumpSubModule(2);
default:
jumpToChangeFace(model);
}
}
//模版还在就进详情,被下架了退回换脸 Tab
void jumpToChangeFace(AISquareItemModel model) async {
final mod = await AIService.getModelInfo(model.template, model.type);
if (mod != null) {
Get.to(() => AIModDetailPage(mod));
return;
}
final isImageFace =
AiTypeCode.fromServerCode(model.type) == AiType.imageChangeFace;
Get.find<AiHomeLogic>().jumpSubModule(isImageFace ? 5 : 4);
}
}
@@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgdj/hj_utils/screen.dart';
import 'package:hgdj/tools_base/loading/loading_center_widget.dart';
import 'package:hgdj/tools_base/refresh/pull_refresh.dart';
import '../widgets/ai_square_cell.dart';
import 'ai_square_logic.dart';
class AiSquarePage extends StatelessWidget {
AiSquarePage({super.key});
@override
Widget build(BuildContext context) {
return GetBuilder<AiSquareLogic>(
init: AiSquareLogic(),
builder: (logic) {
return Scaffold(
body: pullYsRefresh(
onRefresh: (c) => logic.loadData(),
onLoading: (c) => logic.loadData(isRefresh: false),
onInit: (ctr) => logic.refreshCtr = ctr,
child: _buildContent(logic),
),
);
},
);
}
Widget _buildContent(AiSquareLogic logic) {
if (logic.isLoading) return LoadingCenterWidget();
if (logic.isEmptyData) {
return CErrorWidget(retryOnTap: () => logic.loadData());
}
final list = logic.dataList!;
return ListView.separated(
padding: const EdgeInsets.symmetric(vertical: 12),
separatorBuilder: (context, index) => 18.sizeBoxH,
itemCount: list.length,
itemBuilder: (ctx, index) => AISquareCell(
model: list[index],
onTap: () => logic.makeTheSameStyle(list[index]),
),
);
}
}
@@ -0,0 +1,208 @@
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgdj/config/config.dart';
import 'package:hgdj/hj_page/pre_sale/pre_sale_provider.dart';
import 'package:hgdj/hj_utils/api_service/ai_service.dart';
import 'package:hgdj/tools_base/file_upload/file_upload_tool.dart';
import 'package:hgdj/tools_base/global_store/store.dart';
import 'package:hgdj/tools_base/loading/loading_alert_widget.dart';
import 'package:hgdj/tools_base/toast.dart';
import 'package:hgdj/tools_base/widget/common_alert.dart';
import '../../../alert/mine/vip_level_dialog.dart';
import '../../../routers/jump_router.dart';
import '../../mine/mine_vip/coupon_model.dart';
import '../models/ai_mod_list_model.dart';
import '../widgets/ai_coupon_sheet.dart';
enum AiType {
imageToVideo, //图生视频
autoStrip, //智能脱衣
videoChangeFace, //视频换脸
imageChangeFace, //图片换脸
aiMate, //ai女友
aiPaint, //ai绘画
aiNovel, //ai小说
}
/// 与服务端 type 编码互转,只适用于 /aiplaza/list 这套 1..6
/// 注意:aiSwitchConf 的 type 是另一套编码(1脱衣/2视频换脸/3图片换脸,见 domain_source_model 的 _aiTypeConfMap),别混用
/// aiMate 走独立接口、后端没给编码,故映射为 null
extension AiTypeCode on AiType {
//换脸模版列表按图片/视频分别取 type,网格列数与宽高比也跟着这个走
bool get isVideoFace => this == AiType.videoChangeFace;
static AiType? fromServerCode(int? code) => switch (code) {
1 => AiType.imageChangeFace,
2 => AiType.videoChangeFace,
3 => AiType.autoStrip,
4 => AiType.imageToVideo,
5 => AiType.aiPaint,
6 => AiType.aiNovel,
_ => null,
};
}
/// AI 各功能 logic 的公共部分:图片载体 + 计费校验 + 上传下单模板
abstract class AiFunctionBaseLogic extends GetxController {
AiFunctionBaseLogic(this.modList);
AiModList? modList; //接口下发的模版数据
final localPicList = <String>[]; //已选本地图片路径,与 PicPicker 共享
bool shareToAiSquare = true;
final editingCtr = TextEditingController();
//VIP 门槛文案,配合各功能里注释掉的会员校验使用
final hint = '您还不是VIP无法使用AI脱衣';
@override
void onClose() {
editingCtr.dispose();
super.onClose();
}
/// 提交入口,各功能自行实现校验与下单
Future<void> submit();
//免费次数或金币够不够本次消费
bool canPay(int price) {
final freeCount = (globalStore.wallet?.todayAiFreeTimes ?? 0) +
(presaleProvider.remain?.todayAiUndressCount ?? 0);
if ((globalStore.wallet?.amount ?? 0) < price && freeCount <= 0) {
showVipLevelDialog("当前免费次数不足或金币余额不足", buttonTitle: '去充值', vipEvent: () {
Get.back();
pushToWalletPage(tabPosition: 1);
});
return false;
}
return true;
}
//校验有没有选图,必须在扣免费次数之前调,否则次数会白扣
bool checkPic() {
if (localPicList.isNotEmpty) return true;
CommonAlert.show(title: "提示", content: "请选择图片", showCancel: false);
return false;
}
/// 上传 [pics] → [generate] 下单 → 统一处理 loading / toast / 清态
/// 提交成功后会清空 localPicList,上传途中怕被清就传副本
Future<void> upload(
List<String> pics,
Future<bool> Function(List<String> urls) generate, {
VoidCallback? onDone,
}) async {
await FileUploadTool().uploadImagesWithProgress(
pics,
onFailure: () => showToast("图片上传失败"),
onSuccess: (urls) async {
LoadingAlertWidget.show(title: "正在更新数据...");
try {
if (await generate(urls)) {
showToast("提交成功~");
localPicList.clear();
onDone?.call();
globalStore.refreshWallet();
update();
} else {
showToast("提交失败");
}
} on DioException catch (e) {
showToast(e.message.toString());
} catch (e) {
showToast(e.toString());
} finally {
LoadingAlertWidget.cancel();
}
},
);
}
}
// 智能脱衣
class AiStripLogic extends AiFunctionBaseLogic {
AiStripLogic(super.modList);
double get aspectRatio => 408 / 310;
@override
void onReady() {
super.onReady();
loadData();
}
//获取脱衣模版
Future<void> loadData() async {
modList = await AIService.getModelList();
update();
}
@override
Future<void> submit() async {
if (!checkPic()) return;
//预售期有免费次数先走免费,否则查金币
if (presaleProvider.isOpen &&
presaleProvider.hasLimit &&
(presaleProvider.remain?.todayAiUndressCount ?? 0) > 0) {
presaleProvider.remain?.todayAiUndressCount =
(presaleProvider.remain?.todayAiUndressCount ?? 0) - 1;
} else {
if (!canPay(int.tryParse(Config.aiUndressPrice) ?? 0)) return;
}
//传副本:提交成功回调里会清空 localPicList
await upload(
[...localPicList],
(urls) =>
AIService.generateUndress(urls, shareToAiSquare, editingCtr.text));
}
}
// 换脸详情
class AiFaceDetailLogic extends AiFunctionBaseLogic {
AiFaceDetailLogic(super.modList, {required this.mod});
final TemplateModel mod; // 换脸模版
AICouponModel? coupon; //折扣券
bool get isChangeVideo => mod.moduleType == 1; // 是否为视频
// 视频/图片换脸都用模版里的价格
int get price => globalStore.isVIP ? (mod.vipCoin ?? 0) : (mod.coin ?? 0);
@override
String get hint => '您还不是充值VIP无法使用AI换脸';
@override
Future<void> submit() async {
if (!checkPic()) return;
//预售期图片换脸有免费次数先走免费,否则查金币
if (presaleProvider.isOpen &&
presaleProvider.hasLimit &&
(presaleProvider.remain?.todayAiUndressCount ?? 0) > 0 &&
!isChangeVideo) {
presaleProvider.remain?.todayAiUndressCount =
(presaleProvider.remain?.todayAiUndressCount ?? 0) - 1;
} else {
if (!canPay(price)) return;
}
await upload(
localPicList,
(urls) => isChangeVideo
? AIService.generateChangeFace(
urls, mod.id, coupon?.id, shareToAiSquare, editingCtr.text) //视频换脸
: AIService.generateImg(urls.firstOrNull ?? '', mod.id ?? '',
shareToAiSquare, editingCtr.text), //图片换脸
onDone: () => coupon = null,
);
}
Future<void> showCoupon() async {
final AICouponModel? model = await Get.bottomSheet(AICouponSheet());
if (model != null) {
coupon = model;
update();
}
}
}
@@ -0,0 +1,197 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgdj/assets_tool/app_colors.dart';
import 'package:hgdj/config/config.dart';
import 'package:hgdj/hj_page/pre_sale/pre_sale_provider.dart';
import 'package:hgdj/hj_utils/screen.dart';
import 'package:hgdj/routers/jump_router.dart';
import 'package:hgdj/tools_base/banner/ads_banner_widget.dart';
import 'package:hgdj/tools_base/global_store/store.dart';
import 'package:hgdj/tools_base/loading/loading_center_widget.dart';
import 'package:provider/provider.dart';
import '../widgets/pic_picker.dart';
import 'ai_function_logic.dart';
//ai脱衣
class AIStripSubPage extends StatelessWidget {
const AIStripSubPage({super.key});
@override
Widget build(BuildContext context) {
return GetBuilder<AiStripLogic>(
init: AiStripLogic(null),
builder: (logic) {
if (logic.modList == null) return LoadingCenterWidget();
if (logic.modList!.aiUndressMod?.isEmpty == true) return CErrorWidget();
return Container(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Column(
children: [
Expanded(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
12.sizeBoxH,
_uploadHeader(logic),
18.sizeBoxH,
0.5.line,
18.sizeBoxH,
Text(
"案例鉴赏",
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
12.sizeBoxH,
_buildBanner(logic),
18.sizeBoxH,
0.5.line,
18.sizeBoxH,
_priceView(),
18.sizeBoxH,
_buildBtn(logic),
],
),
),
),
],
),
);
},
);
}
// 上传图片 + 注意事项
Widget _uploadHeader(AiStripLogic logic) {
return Row(
children: [
PicPicker(
width: 110,
height: 110,
picList: logic.localPicList,
),
12.sizeBoxW,
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"注意事项:",
style: TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
4.sizeBoxH,
Text(
'''1、素材仅供AI使用,绝无外泄风险,请放心使用.
2、素材需清晰,小于2MB,上传间隔大于60秒.
3、本功能不支持多人图片
4、生成失败退回金币,若违规作废. 5、禁止使用未成年图片!''',
style: TextStyle(
color: Color(0xff656565),
fontSize: 10,
),
),
],
),
),
],
);
}
// 费用信息:免费次数 + 单价
Widget _priceView() {
return Consumer<GlobalStore>(builder: (_, provider, __) {
final total = provider.wallet?.aiUndressFreeTimes ?? 0;
final presaleCount = presaleProvider.remain?.todayAiUndressCount ?? 0;
final todayFreeCount = provider.wallet?.todayAiFreeTimes ?? 0;
//今日免费次数
final toDaytotal = todayFreeCount + presaleCount;
return Column(
children: [
Text(
'你当前免费体验为$total次数',
style: TextStyle(
color: Color(0xff989898),
fontSize: 12,
),
),
12.sizeBoxH,
Text(
'当日免费$toDaytotal次数',
style: TextStyle(
color: Color(0xff989898),
fontSize: 12,
),
),
12.sizeBoxH,
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"处理一张照片的费用是 ",
style: TextStyle(
color: Color(0xff989898),
fontSize: 12,
),
),
Text(
"${Config.aiUndressPrice}金币",
style: TextStyle(
color: AppColors.actionRed,
fontSize: 12,
),
),
],
),
],
);
});
}
// 案例轮播
Widget _buildBanner(AiStripLogic logic) {
return AspectRatio(
aspectRatio: logic.aspectRatio,
child: AdsBannerWidget(
logic.modList?.aiUndressMod ?? [],
isIndicatorUnderCenter: true,
color: AppColors.actionRed.withValues(alpha: .3),
selectColor: AppColors.actionRed,
isCircle: false,
onItemClick: (index) {
final ad = (logic.modList!.aiUndressMod ?? [])[index];
pushToPageByLink(ad.href);
},
),
);
}
Widget _buildBtn(AiStripLogic logic) {
return GestureDetector(
onTap: () => logic.submit(),
child: Container(
height: 44,
alignment: Alignment.center,
decoration: BoxDecoration(
color: AppColors.actionRed,
borderRadius: BorderRadius.circular(3),
),
child: Text(
"立即提交",
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
);
}
}
@@ -0,0 +1,35 @@
//AI 模版项:脱衣/换脸/绘画列表共用
class AiChangeFaceVideoMod {
String? id;
String? title;
String? sourceURL;
int? status;
int? playTime;
String? cover;
String? type;
int? moduleType;
int? hotValue;
String? hotMark;
int? coin;
int? vipCoin;
String? newUrl; // 图生视频结果
int? styleType; // ai绘画
AiChangeFaceVideoMod.fromJson(dynamic json) {
json ??= {};
id = json['id'];
title = json['title'];
sourceURL = json['sourceURL'];
status = json['status'];
playTime = json['playTime'];
cover = json['cover'];
type = json['type'];
moduleType = json['moduleType'];
hotValue = json['hotValue'];
hotMark = json['hotMark'];
coin = json['coin'];
vipCoin = json['vipCoin'];
newUrl = json['newUrl'];
styleType = json['styleType'];
}
}
@@ -0,0 +1,48 @@
//ai女友余额
class AIGirlFriendBalanceModel {
num? balance;
AIGirlFriendBalanceModel.fromJson(dynamic json) {
json ??= {};
balance = json['balance'];
}
}
//ai跳转url
class AIGirlFriendUrlModel {
String? url;
AIGirlFriendUrlModel.fromJson(dynamic json) {
json ??= {};
url = json['url'] ?? json['authUrl'];
}
}
//ai女友货币档位列表
class AIGirlFriendCurrencys {
List<AIGirlFriendCurrency>? list;
AIGirlFriendCurrencys.fromJson(dynamic json) {
json ??= {};
list = (json['list'] as List?)?.map((e) => AIGirlFriendCurrency.fromJson(e)).toList();
}
}
class AIGirlFriendCurrency {
String? id; //货币id
String? name; //货币名称
num? coins; //购买货币数(对应积分数量)
num? price; //价格(对应需要支付的金币数量),
String? couponDesc; //优惠描述
int? type;
AIGirlFriendCurrency.fromJson(dynamic json) {
json ??= {};
id = json['id'];
name = json['name'];
coins = json['coins'];
price = json['price'];
couponDesc = json['couponDesc'];
type = json['type'];
}
}
@@ -0,0 +1,107 @@
import 'package:hgdj/hj_utils/text_util.dart';
import '../../../hj_model/splash/ads_model.dart';
import 'ai_change_face_video_model.dart';
//ai脱衣模版
class AiModList {
/// 脱衣模版
List<AdsInfoModel>? aiUndressMod;
/// 图片转视频模版
List<AdsInfoModel>? aiImgToVideoMod;
//ai绘画模版
List<AiChangeFaceVideoMod>? aiTextToImgMod;
AiModList();
AiModList.fromJson(dynamic json) {
json ??= {};
aiUndressMod = (json['aiUndressMod'] as List?)
?.map((e) => AdsInfoModel.fromJson(e))
.toList();
aiImgToVideoMod = (json['aiImgToVideoMod'] as List?)
?.map((e) => AdsInfoModel.fromJson(e))
.toList();
aiTextToImgMod = (json['aiTextToImgMod'] as List?)
?.map((e) => AiChangeFaceVideoMod.fromJson(e))
.toList();
}
}
//ai换脸模版
class AiChangeModList {
String? categoryId;
List<AICategoryMod>? categoryList;
List<TemplateModel>? templateList;
AiChangeModList.fromJson(dynamic json) {
json ??= {};
categoryId = json['categoryId'];
categoryList = (json['categoryList'] as List?)
?.map((e) => AICategoryMod.fromJson(e))
.toList();
templateList = (json['templateList'] as List?)
?.map((e) => TemplateModel.fromJson(e))
.toList();
}
}
class AICategoryMod {
int? appId;
String? createdAt;
String? id;
String? name;
int? sortCode;
int? status;
List<String>? templateIds;
int? type;
String? updatedAt;
AICategoryMod.fromJson(dynamic json) {
json ??= {};
appId = json['appId'];
createdAt = json['createdAt'];
id = json['id'];
name = json['name'];
sortCode = json['sortCode'];
status = json['status'];
templateIds = parseStringList(json['templateIds']);
type = json['type'];
updatedAt = json['updatedAt'];
}
}
class TemplateModel {
String? categoryId; //分类id
int? coin; //价格(金豆)
String? cover; //封面
String? createdAt; //创建时间
String? id;
int? moduleType; //换脸模版类型 0 图片 1 视频
String? mp4Url; //视频mp4地址
String? m3u8Url;
String? title;
int? usedCount; //模版使用次数
int? vipCoin;
/// 上架时间戳,模版列表按它排序;时间解析不了按 0 排最前
int get timestamp =>
DateTime.tryParse(createdAt ?? '')?.millisecondsSinceEpoch ?? 0;
TemplateModel.fromJson(dynamic json) {
json ??= {};
categoryId = json['categoryId'];
coin = json['coin'];
cover = json['cover'];
createdAt = json['createdAt'];
id = json['id'];
m3u8Url = json['m3u8Url'];
moduleType = json['moduleType'];
mp4Url = json['mp4Url'];
title = json['title'];
usedCount = json['usedCount'];
vipCoin = json['vipCoin'];
}
}
@@ -0,0 +1,77 @@
/*
ai 视频换脸的数据注释:
modCover 模版图片
modMp4Url 模版视频
picture换脸图片(用户提交的)
cover换脸后封面大图
url换脸后视频地址
*/
import 'package:hgdj/hj_utils/text_util.dart';
import '../../../config/address.dart';
class AiRecordModel {
String? id;
int? uid;
String? originPic;
String? imgUrl;
String? newImgUrl;
String? styleUrl;
String? text;
List<String>? originPics;
List<String>? newPic;
List<String>? picture;
int? coin;
int? status;
String? remark;
String? url;
String? updateAct;
String? createdAt;
String? updatedAt;
String? vidId;
String? modPic;
String? modCover;
String? modMp4Url;
String? cover;
String? content;
String? characterSetting; //小说人物设定/故事背景等
String? description; //故事情节描述
String? details; //细节说明/其他要求
String? locationScene; //地点场景
/// 换脸结果视频地址。query 用 cdn= 而不是 VideoModel 的 c=,两边接口不同,别顺手统一
String get realVideoUrl =>
"${Address.baseApiPath}/vid/h5/m3u8/$url?token=${Address.token}&cdn=${Address.cdnAddress}";
AiRecordModel.fromJson(dynamic json) {
json ??= {};
id = json['id'];
uid = json['uid'];
originPic = json['originPic'];
newImgUrl = json['newImgUrl'];
imgUrl = json['imgUrl'];
styleUrl = json['styleUrl'];
text = json['text'];
originPics = parseStringList(json['originPics']);
picture = parseStringList(json['picture']);
newPic = parseStringList(json['newPic']);
coin = json['coin'];
status = json['status'];
remark = json['remark'];
url = json['url'];
updateAct = json['updateAct'];
createdAt = json['createdAt'];
updatedAt = json['updatedAt'];
vidId = json['vidId'];
modPic = json['modPic'];
modCover = json['modCover'];
modMp4Url = json['modMp4Url'];
cover = json['cover'];
content = json['content'];
characterSetting = json['characterSetting'];
description = json['description'];
details = json['details'];
locationScene = json['locationScene'];
}
}
@@ -0,0 +1,64 @@
import '../../../hj_model/video_model.dart';
class AISquareItemModel {
String? createdAt;
String? gender;
String? generateImage;
String? generateVideo;
String? generateVideoCover;
String? id;
String? name;
String? originContent;
String? originalImage;
String? originalVideo;
String? originalVideoCover;
String? portrait;
String? reason;
String? reviewAt;
int? sortCode;
int? status;
String? template;
String? title;
/// 1-ai图片换脸 2-ai视频换脸 3-ai脱衣 4-ai图生视频 5-ai绘画
int? type;
int? uid;
String? updatedAt;
/// 生成结果视频的播放地址,借 VideoModel 拼(拼接规则只在那一处维护)
String get realGenerateVideoUrl => (VideoModel()..sourceURL = generateVideo).realVideoUrl;
String get typeString => switch (type) {
1 => '图片换脸',
2 => '视频换脸',
3 => 'AI脱衣',
4 => '图生视频',
5 => 'AI绘画',
_ => '',
};
AISquareItemModel.fromJson(dynamic json) {
json ??= {};
createdAt = json['createdAt'];
gender = json['gender'];
generateImage = json['generateImage'];
generateVideo = json['generateVideo'];
generateVideoCover = json['generateVideoCover'];
id = json['id'];
name = json['name'];
originContent = json['originContent'];
originalImage = json['originalImage'];
originalVideo = json['originalVideo'];
originalVideoCover = json['originalVideoCover'];
portrait = json['portrait'];
reason = json['reason'];
reviewAt = json['reviewAt'];
sortCode = json['sortCode'];
status = json['status'];
template = json['template'];
title = json['title'];
type = json['type'];
uid = json['uid'];
updatedAt = json['updatedAt'];
}
}
+134
View File
@@ -0,0 +1,134 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgdj/assets_tool/app_colors.dart';
import 'package:hgdj/assets_tool/images.dart';
import 'package:hgdj/hj_utils/api_service/mine_service.dart';
import 'package:hgdj/hj_utils/screen.dart';
import 'package:hgdj/tools_base/loading/loading_center_widget.dart';
import 'package:hgdj/tools_base/refresh/pull_refresh.dart';
import 'package:hgdj/tools_base/widget/sheet_handle_bar.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import '../../../hj_utils/widget_util.dart';
import '../../mine/mine_vip/coupon_model.dart';
/// 抵扣券选择弹窗:选中一张后 [Get.back] 回传 [AICouponModel]
class AICouponSheet extends StatefulWidget {
const AICouponSheet({super.key});
@override
State<AICouponSheet> createState() => _AICouponSheetState();
}
class _AICouponSheetState extends State<AICouponSheet> {
int curPage = 1;
List<AICouponModel>? dataList; //null = 还在首屏加载
RefreshController? refreshCtr;
@override
void initState() {
super.initState();
loadData();
}
Future<void> loadData({int page = 1}) async {
const size = 10; //请求条数和「还有没有下一页」的判断必须用同一个值
final res = await MineService.backPack(page, limit: size);
if (res != null) {
if (page == 1) dataList = []; //刷新成功才清空,失败保留旧数据
(dataList ??= []).addAll(res);
curPage = page;
}
dataList ??= []; //首屏失败也要退出 loading,否则永远转圈
setState(() {});
if (page == 1) refreshCtr?.refreshCompleted();
(res?.length ?? 0) < size
? refreshCtr?.loadNoData()
: refreshCtr?.loadComplete();
}
Future<void> loadMore() => loadData(page: curPage + 1);
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 18),
decoration: const BoxDecoration(
color: AppColors.primaryColor,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
child: Column(
children: [
const SheetHandleBar(),
18.sizeBoxH,
Text('使用抵扣券',
style: textStyle(
18, Colors.white.withValues(alpha: .9), FontWeight.w600)),
18.sizeBoxH,
Expanded(child: _list()),
],
),
);
}
Widget _list() {
if (dataList == null) return LoadingCenterWidget();
if (dataList!.isEmpty) return CErrorWidget(retryOnTap: loadData);
return pullYsRefresh(
onInit: (ctr) => refreshCtr = ctr,
onRefresh: (_) => loadData(),
onLoading: (_) => loadMore(),
child: ListView.separated(
itemCount: dataList!.length,
separatorBuilder: (_, __) => 12.sizeBoxH,
itemBuilder: (_, index) => _couponItem(dataList![index]),
),
);
}
Widget _couponItem(AICouponModel model) {
//券面是暖色底图,三处文字统一用这个深棕
const couponText = Color(0xff7E4444);
return InkWell(
enableFeedback: false,
onTap: () => Get.back(result: model),
child: Container(
alignment: Alignment.center,
height: 68,
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('ai_coupon_bg.webp'.aiPath), fit: BoxFit.fill),
),
child: Row(
children: [
18.sizeBoxW,
Text('${model.goodsName}',
style: textStyle(18, couponText, FontWeight.w600)),
12.sizeBoxW,
Expanded(
child: Text(
'${model.goodsDesc}',
style: textStyle(14, couponText, FontWeight.w400),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
12.sizeBoxW,
Container(
height: 26,
padding: const EdgeInsets.symmetric(horizontal: 12),
alignment: Alignment.center,
decoration: BoxDecoration(
color: const Color(0xffFFDCB3),
borderRadius: BorderRadius.circular(22),
),
child: Text('立即使用',
style: textStyle(12, couponText, FontWeight.w400)),
),
18.sizeBoxW,
],
),
),
);
}
}
@@ -0,0 +1,78 @@
import 'package:flutter/material.dart';
/// AI 输入框:多行 + 右下角字数统计。
/// controller 归外部 logic 持有,本 widget 只摘自己挂的监听
class AIDrawTextField extends StatefulWidget {
final TextEditingController controller;
final String hintText;
final int maxLength;
final double height;
const AIDrawTextField({
super.key,
required this.controller,
this.hintText = '主人来两句嘛~',
this.maxLength = 500,
this.height = 181,
});
@override
State<AIDrawTextField> createState() => _AIDrawTextFieldState();
}
class _AIDrawTextFieldState extends State<AIDrawTextField> {
@override
void initState() {
super.initState();
widget.controller.addListener(_onTextChanged);
}
@override
void dispose() {
widget.controller.removeListener(_onTextChanged);
super.dispose();
}
//刷新底部字数统计
void _onTextChanged() => setState(() {});
@override
Widget build(BuildContext context) {
return Container(
height: widget.height,
padding: const EdgeInsets.only(left: 10, right: 6),
child: Column(
children: [
Expanded(
child: TextField(
controller: widget.controller,
style: TextStyle(color: Colors.white.withValues(alpha: .8), fontSize: 12),
maxLength: widget.maxLength,
maxLines: null,
decoration: InputDecoration(
hintText: widget.hintText,
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(vertical: 8, horizontal: 2),
counterText: '', //自己在下面画统计,藏掉系统那个
hintStyle: TextStyle(
color: Colors.white.withValues(alpha: .6),
fontSize: 12,
fontWeight: FontWeight.w400,
),
hintMaxLines: 10,
),
),
),
Container(
padding: const EdgeInsets.only(bottom: 10),
alignment: Alignment.centerRight,
child: Text(
'${widget.controller.text.length}/${widget.maxLength}',
style: TextStyle(color: Colors.white.withValues(alpha: .5), fontSize: 12),
),
),
],
),
);
}
}
+215
View File
@@ -0,0 +1,215 @@
import 'package:easy_rich_text/easy_rich_text.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgdj/assets_tool/app_colors.dart';
import 'package:hgdj/hj_utils/api_service/ai_service.dart';
import 'package:hgdj/routers/jump_router.dart';
import 'package:hgdj/tools_base/debug_log.dart';
import 'package:hgdj/tools_base/loading/loading_alert_widget.dart';
import 'package:hgdj/tools_base/toast.dart';
import 'package:hgdj/tools_base/widget/sheet_handle_bar.dart';
import '../../../hj_utils/widget_util.dart';
import '../models/ai_girl_resp_model.dart';
/// AI 女友充值积分弹窗
class AIGirlSheet extends StatefulWidget {
final List<AIGirlFriendCurrency>? list;
const AIGirlSheet({super.key, this.list});
@override
State<AIGirlSheet> createState() => _AIGirlSheetState();
}
class _AIGirlSheetState extends State<AIGirlSheet> {
int selectIndex = 0;
//TapGestureRecognizer 必须自己释放,建一次复用,别在 build 里 new
final _serviceTap = TapGestureRecognizer()..onTap = pushToCustomService;
List<AIGirlFriendCurrency> get _items => widget.list ?? [];
//列表可能为空,取不到就没得选
AIGirlFriendCurrency? get _selected =>
selectIndex < _items.length ? _items[selectIndex] : null;
@override
void dispose() {
_serviceTap.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Material(
color: Colors.transparent,
child: Container(
decoration: const BoxDecoration(
color: Color(0xff1D2236),
borderRadius: BorderRadius.vertical(top: Radius.circular(18)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 18),
const SheetHandleBar(color: Color(0x1AFFFFFF)),
Padding(
padding: const EdgeInsets.only(top: 18, bottom: 12),
child: Text('AI女友',
style: textStyle(20, Colors.white, FontWeight.w600)),
),
Text('充值积分',
style: textStyle(
14, Colors.white.withValues(alpha: .8), FontWeight.w400)),
GridView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 18),
shrinkWrap: true,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 106 / 60,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
),
itemCount: _items.length,
itemBuilder: (_, index) => _item(index),
),
//小贴士整块左右各留 28
Padding(
padding: const EdgeInsets.symmetric(horizontal: 28),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('支付小贴士:',
style: textStyle(14, Colors.white, FontWeight.w500)),
const SizedBox(height: 6),
Text(
'1.因超时支付无法到账,请重新发起。\n2.连续发起且未支付,账号可能被加入黑名单\n3.充值成功后会在1~5分钟内到账,可重新刷新进入当前页面',
style: textStyle(12, Colors.white.withValues(alpha: .6),
FontWeight.w400),
),
],
),
),
const SizedBox(height: 18),
GestureDetector(
onTap: _onPay,
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
height: 44,
alignment: Alignment.center,
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [
Color(0xff1E3C72),
Color(0xff5F40B6),
Color(0xff1E3C72),
Color(0xff2A5298)
],
begin: Alignment.centerLeft,
end: Alignment.centerRight,
),
borderRadius: BorderRadius.circular(22),
),
child: Text(
'${(_selected?.price ?? 0).truncate()}/立即支付',
style: textStyle(16, Colors.white, FontWeight.w400),
),
),
),
const SizedBox(height: 18),
EasyRichText(
'支付中如有问题 请联系在线客服',
textAlign: TextAlign.center,
defaultStyle: textStyle(
12, Colors.white.withValues(alpha: .6), FontWeight.w400),
patternList: [
EasyRichTextPattern(
targetString: '在线客服',
matchOption: 'first',
style: const TextStyle(color: AppColors.primaryHighColor),
recognizer: _serviceTap,
),
],
),
const SizedBox(height: 30),
],
),
),
);
}
Widget _item(int index) {
final item = _items[index];
final isSelected = selectIndex == index;
return GestureDetector(
onTap: () => setState(() => selectIndex = index),
child: Container(
decoration: BoxDecoration(
color: isSelected
? const Color(0xff4039A1)
: Colors.white.withValues(alpha: .05),
borderRadius: BorderRadius.circular(6),
border: Border.all(
color: isSelected
? const Color(0xff30509C)
: Colors.white.withValues(alpha: .1),
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'${item.coins ?? 0}积分',
style: textStyle(
16,
isSelected
? const Color(0xffAFABFF)
: const Color(0xff7D95D5),
FontWeight.w400),
),
const SizedBox(height: 4),
Text(
'${(item.price ?? 0).truncate()}金币',
style: textStyle(
12,
isSelected
? const Color(0x4DF3F5FF)
: Colors.white.withValues(alpha: .3),
FontWeight.w400,
),
),
],
),
),
);
}
Future<void> _onPay() async {
final item = _selected;
if (item == null) return;
try {
LoadingAlertWidget.show();
final value = await AIService.exchangeMate({'id': item.id ?? ''});
LoadingAlertWidget.cancel();
if (value is String) {
showToast(value);
return;
}
if (value.code == 200) {
showToast('充值成功');
Get.back();
return;
}
if (value.code == 8000) {
//金币不足
showToast('金币不足,请充值');
pushToWalletPage(tabPosition: 1);
}
} catch (e) {
LoadingAlertWidget.cancel();
debugLog(e);
}
}
}
+193
View File
@@ -0,0 +1,193 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:hgdj/hj_model/video_model.dart';
import 'package:hgdj/hj_utils/const.dart';
import 'package:hgdj/tools_base/toast.dart';
import 'package:hgdj/tools_base/video_download/video_cache_store.dart';
import 'package:hgdj/tools_base/video_download/video_download_manager.dart';
import 'package:hgdj/tools_base/widget/image_browser_page.dart';
import 'package:permission_handler/permission_handler.dart';
import '../../video/simple_video_player_page.dart';
import '../ai_sub_type/ai_function_logic.dart';
import '../models/ai_record_model.dart';
import 'ai_record_parts.dart';
// StatusGenning AiChangeFaceStatus = 0 // 未完成
// StatusComplete AiChangeFaceStatus = 1 // 已完成
// StatusRefund AiChangeFaceStatus = -1 // 已退款
// StatusSubmit AiChangeFaceStatus = 2 // 已提交
// ai图片换脸
// Processing = 1 // 1、进行中
// SUCCESS = 2 // 2、成功
// FAILURE = 3 // 3、失败
// REFUND = 4 // 4、退款
// StatusSubmit = 5 //5,提交
// ai脱衣
// Processing = 1 // 1、进行中
// SUCCESS = 2 // 2、成功
// FAILURE = 3 // 3、失败
// REFUND = 4 // 4、退款
// PartSuccess = 5 // 5、部分成功
// SubmitOrder = 6 // 6、已提交
/// 脱衣 / 视频换脸 / 图片换脸 三类记录,版式都是「(模版 +) 素材 = 结果」
class AIImageRecord extends StatelessWidget {
final AiRecordModel model;
final AiType aiType;
final VoidCallback? onDeleteCallback;
const AIImageRecord(this.model,
{super.key, this.aiType = AiType.autoStrip, this.onDeleteCallback});
bool get isVideoFace => aiType.isVideoFace;
bool get isImageFace => aiType == AiType.imageChangeFace;
//图片/视频换脸才有模版
bool get hasTemplate => isVideoFace || isImageFace;
/// 各业务的 status 编码不同(见文件顶部注释),统一映射成三态,没列到的一律按生成中处理
AIRecordState get _state {
if (isVideoFace) {
return switch (model.status) {
1 => AIRecordState.success,
-1 => AIRecordState.failed,
_ => AIRecordState.queuing,
};
}
if (isImageFace) {
return switch (model.status) {
2 => AIRecordState.success,
3 || 4 => AIRecordState.failed,
_ => AIRecordState.queuing,
};
}
//脱衣,以及其余走这套 UI 的类型
return switch (model.status) {
2 => AIRecordState.success,
3 || 4 || 5 => AIRecordState.failed,
_ => AIRecordState.queuing,
};
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: _itemClickHandler,
child: AIRecordCard(
state: _state,
time: model.createdAt,
remark: model.remark,
onDownload: _saveHandler,
onDelete: () => onDeleteCallback?.call(),
child: _buildItemCell(),
),
);
}
//失败态的「失败原因」弹窗由 AIRecordCard 统一处理,这里只管成功态
void _itemClickHandler() {
if (_state != AIRecordState.success) return;
if (isVideoFace) {
Get.to(SimpleVideoPlayerPage(videoUrl: model.realVideoUrl, title: ''));
return;
}
final images = model.newPic ?? [];
if (images.isEmpty) return;
ImageBrowserPage.open([images.first], showSaveButton: true);
}
//素材图:视频换脸取 picture、图片换脸只有 originPic、其余取 originPics
String get _sourceCover {
if (isVideoFace) return model.picture?.firstOrNull ?? model.originPic ?? '';
if (isImageFace) return model.originPic ?? '';
return model.originPics?.firstOrNull ?? model.originPic ?? '';
}
//生成中/失败时结果位回退展示素材封面,成功才是真正结果图
String get _resultCover {
if (_state == AIRecordState.success) {
//视频换脸有的成功订单后端不回 cover,退回素材封面,别让结果位空着露占位图
if (isVideoFace)
return model.cover?.isNotEmpty == true ? model.cover! : _sourceCover;
return model.newPic?.firstOrNull ?? '';
}
if (isVideoFace) return model.picture?.firstOrNull ?? '';
return model.originPics?.firstOrNull ?? model.originPic ?? '';
}
Future<void> _saveHandler() async {
if (!hasTemplate) {
final images = model.newPic ?? [];
if (images.isEmpty) return;
ImageBrowserPage.open([images.first], showSaveButton: true);
return;
}
final isCached = await VideoCacheStore.instance
.isExistLoadVideoByUrl(MediaStyle.Video, model.url ?? '');
if (isCached) {
showToast('你已经添加过缓存了');
return;
}
if (!await _checkPermission()) return;
final result =
await VideoDownloadManager.instance.download(url: model.url ?? '');
if (result != null) return;
final viewModel = VideoModel()
..sourceURL = model.url
..title = 'AI换脸视频'
..coverThumb = model.newPic?.firstOrNull
..cover = model.newPic?.firstOrNull
..commentCount = 0
..id = '-1';
showToast('已加入缓存');
await VideoCacheStore.instance.saveVideoInfo(MediaStyle.Video, viewModel);
}
Future<bool> _checkPermission() async {
var status = await Permission.storage.status;
if (!status.isGranted) status = await Permission.storage.request();
return status.isGranted;
}
//视频换脸的结果位单独接播放器,链接由 VideoModel 拼(和 AiRecordModel.realVideoUrl 的 query 不一样)
void _playResultVideo() {
final videoModel = VideoModel()..sourceURL = model.url;
Get.to(SimpleVideoPlayerPage(
videoUrl: videoModel.realVideoUrl, title: 'AI生成视频'));
}
//三张图:模版 + 素材 = 结果
Widget _buildItemCell() {
return SizedBox(
height: 90,
child: Row(
children: [
if (hasTemplate) ...[
AIRecordThumb(
imageUrl: isImageFace ? model.modPic : model.modCover,
badge: const AISourceBadge(),
width: 90.w,
),
const AIRecordJoin(gap: 3.5),
],
AIRecordThumb(
imageUrl: _sourceCover,
badge: const AISourceBadge(isTemplate: false),
width: 90.w),
const AIRecordJoin(isEquals: true, gap: 3.5),
AIResultThumb(
imageUrl: _resultCover,
state: _state,
onTap: isVideoFace ? _playResultVideo : null,
width: 90.w,
),
],
),
);
}
}
+53
View File
@@ -0,0 +1,53 @@
import 'package:flutter/material.dart';
import 'package:hgdj/assets_tool/images.dart';
import 'package:hgdj/tools_base/widget/net_image_widget.dart';
import '../models/ai_change_face_video_model.dart';
/// 换脸模版选择项:封面 + 右上角选中标 + 标题
class AIModCell extends StatelessWidget {
final AiChangeFaceVideoMod mod;
final bool isSelected;
final VoidCallback? onTap;
const AIModCell(
{super.key, required this.mod, this.isSelected = false, this.onTap});
@override
Widget build(BuildContext context) {
return InkWell(
enableFeedback: false,
onTap: onTap,
child: Column(
children: [
AspectRatio(
aspectRatio: 1,
child: Stack(
children: [
NetworkImageLoader(imageUrl: mod.cover ?? ''),
Positioned(
right: 8,
top: 8,
child: Image.asset(
isSelected
? 'radio_sel.png'.commonImgPath
: 'ai_draw_unselected.png'.aiPath,
width: 20,
),
),
],
),
),
const Spacer(),
Text(
mod.title ?? '',
style: TextStyle(
color: Colors.white.withValues(alpha: .8),
fontSize: 14,
fontWeight: FontWeight.w500),
),
],
),
);
}
}
@@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../hj_utils/widget_util.dart';
import '../ai_novel/ai_novel_detail_page.dart';
import '../models/ai_record_model.dart';
import 'ai_record_parts.dart';
/// AI 小说记录:内容是提示词摘要,成功后点进详情看正文
class AINovelRecord extends StatelessWidget {
final AiRecordModel model;
final VoidCallback? onDeleteCallback;
const AINovelRecord(this.model, {super.key, this.onDeleteCallback});
/// 小说接口的 status → 三态,没列到的一律按生成中处理
AIRecordState get _state => switch (model.status) {
3 => AIRecordState.success,
-1 || 4 || 5 => AIRecordState.failed,
_ => AIRecordState.queuing,
};
@override
Widget build(BuildContext context) {
return AIRecordCard(
state: _state,
time: model.createdAt,
remark: model.remark,
onDelete: () => onDeleteCallback?.call(),
child: _buildItemCell(),
);
}
Widget _buildItemCell() {
return InkWell(
enableFeedback: false,
//只有成功才拦截点击,失败要放行给卡片弹失败原因
onTap: _state == AIRecordState.success ? () => Get.to(() => AiNovelDetailPage(model)) : null,
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 11),
child: Text(
'人物设定:${model.characterSetting}\n地点场景:${model.locationScene}\n故事情节:${model.description}\n细节说明:${model.details}',
style: textStyle(12, const Color(0xff999999), FontWeight.w400),
maxLines: 18,
overflow: TextOverflow.ellipsis,
),
),
);
}
}
@@ -0,0 +1,87 @@
import 'package:flutter/material.dart';
import 'package:hgdj/tools_base/toast.dart';
import 'package:hgdj/tools_base/widget/image_browser_page.dart';
import '../models/ai_record_model.dart';
import 'ai_record_parts.dart';
/// 文生图(ai绘画) 单条记录(提示词 → 结果图),列表/分页/删除由 AIRecordLogic 统一管理
class AIPaintRecord extends StatelessWidget {
final AiRecordModel model;
final int status; // 1 排队 2 成功 3 失败
final VoidCallback? onDeleteCallback;
const AIPaintRecord(this.model,
{super.key, required this.status, this.onDeleteCallback});
AIRecordState get _state => switch (status) {
2 => AIRecordState.success,
3 => AIRecordState.failed,
_ => AIRecordState.queuing,
};
/// 排队/失败没有结果图,退回模版封面占位。
/// 后端在这个接口里放模版封面的字段不固定(styleUrl 常为空),挨个兜一遍,别露占位图
String get _templateCover =>
[model.styleUrl, model.cover, model.modCover, model.modPic]
.firstWhere((e) => e?.isNotEmpty == true, orElse: () => '') ??
'';
//只有成功才是真结果图;未知 status 按排队处理,与 _state 的口径保持一致
String get _resultCover =>
status == 2 ? (model.newImgUrl ?? '') : _templateCover;
@override
Widget build(BuildContext context) {
return AIRecordCard(
state: _state,
time: model.createdAt,
remark: model.remark,
onDownload: _showNewPicture,
onStatusTap: _showNewPicture,
onDelete: () => onDeleteCallback?.call(),
child: _buildItemCell(),
);
}
void _showNewPicture() {
final url = model.newImgUrl ?? '';
if (url.isEmpty) {
showToast('图片地址为空');
return;
}
ImageBrowserPage.open([url], showSaveButton: true);
}
Widget _buildItemCell() {
return SizedBox(
height: 90,
child: Row(
children: [
Container(
width: 181,
height: 58,
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: .05),
borderRadius: BorderRadius.circular(6),
),
child: Text(
model.text ?? '',
style: const TextStyle(color: Color(0xff999999), fontSize: 12),
),
),
const AIRecordJoin(isEquals: true),
AIResultThumb(
imageUrl: _resultCover,
state: _state,
onTap: _resultCover.isEmpty
? null
: () =>
ImageBrowserPage.open([_resultCover], showSaveButton: true),
),
],
),
);
}
}
+344
View File
@@ -0,0 +1,344 @@
import 'package:flutter/material.dart';
import 'package:hgdj/assets_tool/images.dart';
import 'package:hgdj/hj_utils/date_time_util.dart';
import 'package:hgdj/hj_utils/screen.dart';
import 'package:hgdj/tools_base/widget/common_alert.dart';
import 'package:hgdj/tools_base/widget/net_image_widget.dart';
import '../../../hj_utils/widget_util.dart';
/// AI 生成记录卡片的公共小件
/// 记录状态。接口的 status 各业务口径不同,由各 item 自己映射进来
enum AIRecordState { queuing, success, failed }
/// 失败原因:接口没给就退回通用文案。底部文案和失败弹窗共用,改一处即可
String _failedText(String? remark) =>
remark?.isNotEmpty == true ? remark! : '生成失败';
/// 记录卡:白 5% 底 + 12 圆角,版式固定为
/// 创建时间/删除 → 内容 → 细线 → 状态胶囊/下载或失败原因。
/// 四类记录(脱衣换脸 / 小说 / 图生视频 / 文生图)只有中间的 [child] 不一样,
/// 头尾要调版式只改这里一处
class AIRecordCard extends StatelessWidget {
final AIRecordState state;
final Widget child;
final String? time;
final String? remark;
/// 成功时右下角的「一键下载」,不传就不显示(小说记录没有下载)
final VoidCallback? onDownload;
final VoidCallback? onDelete;
/// 成功态状态胶囊的点击,不传胶囊就不可点
final VoidCallback? onStatusTap;
const AIRecordCard({
super.key,
required this.state,
required this.child,
this.time,
this.remark,
this.onDownload,
this.onDelete,
this.onStatusTap,
});
@override
Widget build(BuildContext context) {
final card = Container(
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: .05),
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 18),
child: Column(
children: [
AIRecordHeader(time: time, onTap: onDelete),
12.sizeBoxH,
child,
12.sizeBoxH,
0.5.line,
12.sizeBoxH,
AIRecordFooter(
state: state,
remark: remark,
onDownload: onDownload,
onStatusTap: onStatusTap),
],
),
);
if (state != AIRecordState.failed) return card;
//失败原因底部只放得下两行,点卡片看全文。四类记录都走这里,别再各自实现
return GestureDetector(
behavior: HitTestBehavior.opaque, //卡片内的空白也要能点
onTap: () => CommonAlert.show(
title: '失败原因', content: _failedText(remark), showCancel: false),
child: card,
);
}
}
/// 卡片头部:左边创建时间,右边删除图标
class AIRecordHeader extends StatelessWidget {
final String? time;
final VoidCallback? onTap;
const AIRecordHeader({super.key, this.time, this.onTap});
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: Text(
'创建时间:${DateTimeUtil.utc2iso(time)}',
style: const TextStyle(fontSize: 12, color: Color(0xffDCDCDC)),
),
),
InkWell(
enableFeedback: false,
onTap: onTap,
child: Image.asset('ai_record_delete.png'.aiPath, height: 12),
),
],
);
}
}
/// 卡片底部:左边状态胶囊,右边成功给下载按钮、失败给原因
class AIRecordFooter extends StatelessWidget {
final AIRecordState state;
final String? remark;
final VoidCallback? onDownload;
final VoidCallback? onStatusTap;
const AIRecordFooter(
{super.key,
required this.state,
this.remark,
this.onDownload,
this.onStatusTap});
@override
Widget build(BuildContext context) {
return Container(
constraints: const BoxConstraints(minHeight: 26), //三态高度一致
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
AIStatusPill(state,
onTap: state == AIRecordState.success ? onStatusTap : null),
if (state == AIRecordState.success && onDownload != null)
AIDownloadBtn(onTap: onDownload)
else if (state == AIRecordState.failed)
Flexible(
child: Text(
_failedText(remark),
style: const TextStyle(color: Color(0xffF52C56), fontSize: 12),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
),
);
}
}
/// 状态胶囊:生成中 / 已完成(黄底)、生成失败(无底)
class AIStatusPill extends StatelessWidget {
final AIRecordState state;
final VoidCallback? onTap;
const AIStatusPill(this.state, {super.key, this.onTap});
@override
Widget build(BuildContext context) {
final isFailed = state == AIRecordState.failed;
final text = switch (state) {
AIRecordState.queuing => '生成中',
AIRecordState.success => '已完成',
AIRecordState.failed => '生成失败',
};
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: isFailed
? null
: BoxDecoration(
color: const Color(0x1AFFDB9E),
borderRadius: BorderRadius.circular(3)),
child: Text(
text,
style: textStyle(
13,
isFailed
? Colors.white.withValues(alpha: .55)
: const Color(0xffFFDB9E),
FontWeight.w400,
),
),
),
);
}
}
/// 「一键下载」按钮
class AIDownloadBtn extends StatelessWidget {
final VoidCallback? onTap;
const AIDownloadBtn({super.key, this.onTap});
@override
Widget build(BuildContext context) {
return InkWell(
enableFeedback: false,
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
decoration: BoxDecoration(
border: Border.all(
width: 0.5, color: Colors.white.withValues(alpha: .15)),
borderRadius: BorderRadius.circular(6),
),
child: Row(
children: [
Image.asset('icon_ai_download.png'.aiPath, width: 18),
2.5.sizeBoxW,
const Text('一键下载',
style: TextStyle(color: Colors.white, fontSize: 12)),
],
),
),
);
}
}
/// 记录里的缩略图,[badge] 盖在左上角。
/// [width] 三图并排的卡要传 90.w 按屏宽缩,窄屏才放得下
class AIRecordThumb extends StatelessWidget {
final String? imageUrl;
final Widget? badge;
final VoidCallback? onTap;
final double width;
const AIRecordThumb(
{super.key, this.imageUrl, this.badge, this.onTap, this.width = 90});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: SizedBox(
width: width,
height: 90,
child: Stack(
children: [
Container(
decoration: const BoxDecoration(
color: Color(0xff262626),
borderRadius: BorderRadius.all(Radius.circular(8)),
),
child: NetworkImageLoader(
imageUrl: imageUrl,
width: width,
height: 90,
borderRadius: 8),
),
if (badge != null) badge!,
],
),
),
);
}
}
/// 结果图:没出图先压一层黑蒙层,失败再盖个叉,成功才可点
class AIResultThumb extends StatelessWidget {
final String? imageUrl;
final AIRecordState state;
final VoidCallback? onTap;
final double width;
const AIResultThumb(
{super.key,
this.imageUrl,
required this.state,
this.onTap,
this.width = 90});
@override
Widget build(BuildContext context) {
return Stack(
children: [
AIRecordThumb(
imageUrl: imageUrl,
width: width,
onTap: state == AIRecordState.success ? onTap : null,
),
if (state != AIRecordState.success)
Container(
width: width,
height: 90,
decoration: const BoxDecoration(
color: Color(0x80000000),
borderRadius: BorderRadius.all(Radius.circular(8)),
),
),
if (state == AIRecordState.failed)
Positioned.fill(
child: Center(
child:
Image.asset('ai_record_failed.webp'.aiPath, width: 20))),
],
);
}
}
/// 缩略图左上角角标:模版(黄) / 素材(青),左上右下切角
class AISourceBadge extends StatelessWidget {
final bool isTemplate;
const AISourceBadge({super.key, this.isTemplate = true});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: isTemplate ? const Color(0xffFFD460) : const Color(0xff03FCEB),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(8),
bottomRight: Radius.circular(8),
),
),
child: Text(
isTemplate ? '模版' : '素材',
style: textStyle(12, const Color(0xff141414), FontWeight.w400),
),
);
}
}
/// 两张图之间的连接符:+ 或 =
class AIRecordJoin extends StatelessWidget {
final bool isEquals;
final double gap;
const AIRecordJoin({super.key, this.isEquals = false, this.gap = 3});
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: gap),
child: Image.asset(
(isEquals ? 'ai_record_equals.png' : 'ai_record_add.webp').aiPath,
width: 18,
height: 18,
),
);
}
}
+251
View File
@@ -0,0 +1,251 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgdj/assets_tool/app_colors.dart';
import 'package:hgdj/assets_tool/images.dart';
import 'package:hgdj/hj_page/video/simple_video_player_page.dart';
import 'package:hgdj/hj_utils/date_time_util.dart';
import 'package:hgdj/hj_utils/screen.dart';
import 'package:hgdj/tools_base/widget/header_widget.dart';
import 'package:hgdj/tools_base/widget/image_browser_page.dart';
import 'package:hgdj/tools_base/widget/net_image_widget.dart';
import '../../../hj_utils/widget_util.dart';
import '../ai_sub_type/ai_function_logic.dart';
import '../models/ai_square_model.dart';
/// AI 广场单条内容:作者信息 + 标题 + 按业务类型展示的图/视频
class AISquareCell extends StatelessWidget {
final AISquareItemModel model;
final VoidCallback onTap;
const AISquareCell({super.key, required this.model, required this.onTap});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_headerView(),
12.sizeBoxH,
_contentView(),
12.sizeBoxH,
_imgView(),
],
),
);
}
Widget _imgView() {
return switch (AiTypeCode.fromServerCode(model.type)) {
AiType.imageChangeFace ||
AiType.autoStrip =>
_twoImgsView(model.originalImage, model.generateImage),
AiType.videoChangeFace => _videoChangeFace(),
AiType.imageToVideo => _imgToVideo(),
AiType.aiPaint => _aiDraw(),
_ => const SizedBox.shrink(),
};
}
Widget _twoImgsView(String? left, String? right) {
return Row(
children: [
Expanded(
child: InkWell(
enableFeedback: false,
onTap: () =>
ImageBrowserPage.open([left ?? ''], showSaveButton: true),
child: NetworkImageLoader(
imageUrl: left,
imgBorderRadius:
const BorderRadius.horizontal(left: Radius.circular(9)),
),
),
),
Expanded(
child: InkWell(
enableFeedback: false,
onTap: () =>
ImageBrowserPage.open([right ?? ''], showSaveButton: true),
child: NetworkImageLoader(
imageUrl: right,
imgBorderRadius:
const BorderRadius.horizontal(right: Radius.circular(9)),
),
),
),
],
);
}
Widget _aiDraw() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${model.originContent ?? ''}',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
color: Color(0xff03FCEB)),
),
12.sizeBoxH,
Row(
children: [
Expanded(
child: InkWell(
enableFeedback: false,
onTap: () => ImageBrowserPage.open([model.generateImage ?? ''],
showSaveButton: true),
child: NetworkImageLoader(imageUrl: model.generateImage),
),
),
10.sizeBoxW,
const Expanded(child: SizedBox.shrink()),
],
)
],
);
}
Widget _imgToVideo() {
return Row(
children: [
Expanded(child: _imgItem(model.originalImage)),
10.sizeBoxW,
Expanded(child: _imgItem(model.generateImage)),
],
);
}
Widget _videoChangeFace() {
return Row(
children: [
Expanded(
child: _videoItem(
model.generateVideoCover, model.realGenerateVideoUrl)),
10.sizeBoxW,
const Expanded(child: SizedBox.shrink()),
],
);
}
Widget _contentView() {
return Text.rich(TextSpan(children: [
if ((model.sortCode ?? 0) > 0)
WidgetSpan(
child: Container(
margin: const EdgeInsets.only(right: 8),
width: 30,
height: 15,
alignment: Alignment.center,
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xffFF2264), Color(0x1AFF2264)],
),
),
child: Text(
'置顶',
style: textStyle(10, Colors.white, FontWeight.w500),
),
)),
TextSpan(
text: model.title,
style: textStyle(
14, Colors.white.withValues(alpha: .9), FontWeight.w500))
]));
}
Widget _headerView() {
return Row(
children: [
HeaderWidget(
headPath: model.portrait ?? '',
level: 0,
headWidth: 48,
headHeight: 48,
isCircle: false,
radius: 24,
),
12.sizeBoxW,
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
model.name ?? '',
style: textStyle(14, Colors.white, FontWeight.w500),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Row(
children: [
Text(
model.typeString,
style: textStyle(12, AppColors.actionRed, FontWeight.w400),
),
10.sizeBoxW,
Text(
DateTimeUtil.utcTurnYear(model.reviewAt),
style: textStyle(12, Colors.white.withValues(alpha: 0.55),
FontWeight.w400),
)
],
)
],
)),
12.sizeBoxW,
InkWell(
enableFeedback: false,
onTap: onTap,
child: Container(
width: 70,
height: 24,
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(3),
color: AppColors.actionRed),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset('make_the_same.png'.aiPath, width: 14, height: 14),
2.sizeBoxW,
Text(
'制作同款',
style: textStyle(10, Colors.white, FontWeight.w400),
)
],
),
),
)
],
);
}
Widget _imgItem(String? img) {
return InkWell(
enableFeedback: false,
onTap: () => ImageBrowserPage.open([img ?? ''], showSaveButton: true),
child: NetworkImageLoader(imageUrl: img, borderRadius: 9),
);
}
Widget _videoItem(String? img, String url) {
return InkWell(
enableFeedback: false,
onTap: () => Get.to(
() => SimpleVideoPlayerPage(videoUrl: url, title: model.title ?? '')),
child: Stack(
alignment: Alignment.center,
children: [
NetworkImageLoader(imageUrl: img, borderRadius: 9),
Image.asset('paly_btn.png'.aiPath, width: 36, height: 36),
],
),
);
}
}
@@ -0,0 +1,72 @@
import 'package:flutter/material.dart';
import 'package:hgdj/assets_tool/images.dart';
import 'package:hgdj/tools_base/toast.dart';
import 'package:hgdj/tools_base/widget/image_browser_page.dart';
import '../models/ai_record_model.dart';
import 'ai_record_parts.dart';
/// 图生视频 单条记录(原图 → 结果图),列表/分页/删除由 AIRecordLogic 统一管理
class AIVideoRecord extends StatelessWidget {
final AiRecordModel model;
final int status; // 1 排队 2 成功 3 失败
final VoidCallback? onDeleteCallback;
const AIVideoRecord(this.model,
{super.key, required this.status, this.onDeleteCallback});
AIRecordState get _state => switch (status) {
2 => AIRecordState.success,
3 => AIRecordState.failed,
_ => AIRecordState.queuing,
};
String get _resultCover =>
model.status == 3 ? (model.newImgUrl ?? '') : (model.imgUrl ?? '');
@override
Widget build(BuildContext context) {
return AIRecordCard(
state: _state,
time: model.createdAt,
remark: model.remark,
onDownload: _showNewPicture,
onStatusTap: _showNewPicture,
onDelete: () => onDeleteCallback?.call(),
child: _buildItemCell(),
);
}
void _showNewPicture() {
final url = model.newImgUrl ?? '';
if (url.isEmpty) {
showToast('图片地址为空');
return;
}
ImageBrowserPage.open([url], showSaveButton: true);
}
Widget _buildItemCell() {
return SizedBox(
height: 90,
child: Row(
children: [
AIRecordThumb(
imageUrl: model.imgUrl,
badge:
Image.asset('ai_record_org.png'.aiPath, width: 36, height: 18),
),
const AIRecordJoin(isEquals: true),
AIResultThumb(
imageUrl: _resultCover,
state: _state,
onTap: _resultCover.isEmpty
? null
: () =>
ImageBrowserPage.open([_resultCover], showSaveButton: true),
),
],
),
);
}
}
+132
View File
@@ -0,0 +1,132 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:hgdj/assets_tool/app_colors.dart';
import 'package:hgdj/assets_tool/images.dart';
import 'package:hgdj/hj_utils/screen.dart';
import 'package:hgdj/tools_base/toast.dart';
import 'package:image_pickers/image_pickers.dart';
import 'package:mobkit_dashed_border/mobkit_dashed_border.dart';
/// AI 功能的单图选择器:未选时是虚线占位框,选后是缩略图 + 右上角删除
class PicPicker extends StatefulWidget {
/// 与 logic 共享同一份本地图片路径,选/删直接写回
final List<String> picList;
final double? width;
final double? height;
const PicPicker({
super.key,
required this.picList,
this.width,
this.height,
});
@override
State<PicPicker> createState() => _PicPickerState();
}
class _PicPickerState extends State<PicPicker> {
List<String> get picList => widget.picList;
void _delPic() {
if (picList.isNotEmpty) picList.removeAt(0);
setState(() {});
}
Future<void> _addPic() async {
final paths = await _pickImg();
if (paths.isEmpty) {
showToast("请选择图片");
return;
}
picList
..clear()
..addAll(paths);
setState(() {});
}
//相册选图,只取 1 张;不做二次压缩,交给插件的 compressSize
Future<List<String>> _pickImg() async {
final medias = await ImagePickers.pickerPaths(
uiConfig: UIConfig(uiThemeColor: AppColors.primaryColor),
galleryMode: GalleryMode.image,
selectCount: 1,
showCamera: true,
);
final paths =
medias.map((e) => e.path ?? "").where((e) => e.isNotEmpty).toList();
if (paths.length < medias.length) showToast("添加图片失败");
return paths;
}
@override
Widget build(BuildContext context) {
return SizedBox(
width: widget.width,
height: widget.height,
child: picList.isEmpty ? _addBtn() : _picItem(),
);
}
// 已选图:点图预览,点右上角叉删除
Widget _picItem() {
return GestureDetector(
onTap: () => ImagePickers.previewImages(picList, 0),
child: Stack(
alignment: Alignment.center,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.file(
File(picList.first),
width: widget.width,
height: widget.height,
fit: BoxFit.cover,
),
),
Positioned(
top: 0,
right: 0,
child: InkWell(
enableFeedback: false,
onTap: _delPic,
child: Padding(
padding: const EdgeInsets.all(6),
child: Image.asset("close_grey.png".commonImgPath,
width: 14, height: 14),
),
),
),
],
),
);
}
// 未选图:虚线占位框
Widget _addBtn() {
return GestureDetector(
onTap: _addPic,
child: Container(
alignment: Alignment.center,
decoration: BoxDecoration(
color: const Color(0x0DFFFFFF),
border: DashedBorder.fromBorderSide(
dashLength: 2,
side: const BorderSide(color: Color(0xff656565)),
),
borderRadius: const BorderRadius.all(Radius.circular(3)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Image.asset('add_grey.png'.communityPath, width: 24),
10.sizeBoxH,
const Text("添加图片",
style: TextStyle(fontSize: 12, color: Color(0xff999999))),
],
),
),
);
}
}