251 lines
7.4 KiB
Dart
251 lines
7.4 KiB
Dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
import '../../core/auth/auth_provider.dart';
|
|
|
|
class OutfitPlanInfo {
|
|
final int id;
|
|
final String title;
|
|
final String source; // ai / rule
|
|
final int score;
|
|
final int mainFlag;
|
|
final String dateRange;
|
|
final String location;
|
|
final int hairstyleId;
|
|
final String hairColor;
|
|
final String occasion;
|
|
|
|
const OutfitPlanInfo({
|
|
required this.id,
|
|
required this.title,
|
|
required this.source,
|
|
required this.score,
|
|
required this.mainFlag,
|
|
required this.dateRange,
|
|
required this.location,
|
|
required this.hairstyleId,
|
|
required this.hairColor,
|
|
this.occasion = '',
|
|
});
|
|
|
|
bool get isMain => mainFlag == 1;
|
|
bool get fromAi => source == 'ai';
|
|
}
|
|
|
|
class PlanItemInfo {
|
|
final int id;
|
|
final String slot; // 上衣/下装/鞋/配饰
|
|
final String source; // wardrobe / new
|
|
final int wardrobeItemId;
|
|
final String name;
|
|
final String desc;
|
|
|
|
const PlanItemInfo({
|
|
required this.id,
|
|
required this.slot,
|
|
required this.source,
|
|
required this.wardrobeItemId,
|
|
required this.name,
|
|
required this.desc,
|
|
});
|
|
|
|
bool get fromWardrobe => source == 'wardrobe';
|
|
}
|
|
|
|
class PlanEffectImageInfo {
|
|
final int id;
|
|
final String angle; // front / side / back
|
|
final String url;
|
|
final String status; // pending / rendering / done / failed
|
|
|
|
const PlanEffectImageInfo({
|
|
required this.id,
|
|
required this.angle,
|
|
required this.url,
|
|
required this.status,
|
|
});
|
|
|
|
bool get done => status == 'done' && url.isNotEmpty;
|
|
}
|
|
|
|
class PlanDetail {
|
|
final OutfitPlanInfo plan;
|
|
final List<PlanItemInfo> items;
|
|
final List<PlanEffectImageInfo> images;
|
|
final String hairstyleName;
|
|
|
|
const PlanDetail({
|
|
required this.plan,
|
|
required this.items,
|
|
required this.images,
|
|
required this.hairstyleName,
|
|
});
|
|
}
|
|
|
|
/// 方案列表
|
|
class OutfitPlanNotifier extends AsyncNotifier<List<OutfitPlanInfo>> {
|
|
@override
|
|
Future<List<OutfitPlanInfo>> build() async {
|
|
final api = ref.read(apiClientProvider);
|
|
final list = await api.get<List<dynamic>>('/outfit/plan/list');
|
|
return list
|
|
.map((e) => OutfitPlanInfo(
|
|
id: (e['id'] as num).toInt(),
|
|
title: e['title'] as String? ?? '',
|
|
source: e['source'] as String? ?? 'rule',
|
|
score: (e['score'] as num?)?.toInt() ?? 0,
|
|
mainFlag: (e['main_flag'] as num?)?.toInt() ?? 0,
|
|
dateRange: e['date_range'] as String? ?? '',
|
|
location: e['location'] as String? ?? '',
|
|
hairstyleId: (e['hairstyle_id'] as num?)?.toInt() ?? 0,
|
|
hairColor: e['hair_color'] as String? ?? '',
|
|
))
|
|
.toList();
|
|
}
|
|
|
|
Future<void> refresh() async {
|
|
state = const AsyncLoading();
|
|
state = await AsyncValue.guard(build);
|
|
}
|
|
|
|
Future<void> selectMain(int planId) async {
|
|
final api = ref.read(apiClientProvider);
|
|
await api.post('/outfit/plan/select-main', {'plan_id': planId});
|
|
await refresh();
|
|
}
|
|
|
|
Future<void> review(int planId, String action) async {
|
|
final api = ref.read(apiClientProvider);
|
|
await api.post('/outfit/plan/review', {'plan_id': planId, 'action': action});
|
|
}
|
|
}
|
|
|
|
final outfitPlanProvider =
|
|
AsyncNotifierProvider<OutfitPlanNotifier, List<OutfitPlanInfo>>(
|
|
OutfitPlanNotifier.new);
|
|
|
|
/// 生成任务状态(轮询后端任务状态机)
|
|
enum GeneratePhase { idle, running, done, failed }
|
|
|
|
class GenerateState {
|
|
final GeneratePhase phase;
|
|
final String statusText;
|
|
final String error;
|
|
|
|
const GenerateState({
|
|
this.phase = GeneratePhase.idle,
|
|
this.statusText = '',
|
|
this.error = '',
|
|
});
|
|
}
|
|
|
|
class GenerateNotifier extends Notifier<GenerateState> {
|
|
@override
|
|
GenerateState build() => const GenerateState();
|
|
|
|
Future<bool> generate({
|
|
required String startDate,
|
|
required String endDate,
|
|
required String location,
|
|
required String occasion,
|
|
}) async {
|
|
state =
|
|
const GenerateState(phase: GeneratePhase.running, statusText: '任务创建中...');
|
|
final api = ref.read(apiClientProvider);
|
|
final data = await api.post<Map<String, dynamic>>('/outfit/generate', {
|
|
'start_date': startDate,
|
|
'end_date': endDate,
|
|
'location': location,
|
|
'occasion': occasion,
|
|
});
|
|
final taskId = (data['task_id'] as num).toInt();
|
|
|
|
for (var i = 0; i < 90; i++) {
|
|
await Future.delayed(const Duration(seconds: 2));
|
|
final st = await api.get<Map<String, dynamic>>('/outfit/task/status',
|
|
query: {'task_id': taskId});
|
|
final status = st['status'] as String? ?? '';
|
|
final err = st['error'] as String? ?? '';
|
|
state = GenerateState(
|
|
phase: GeneratePhase.running, statusText: _statusText(status));
|
|
if (status == 'done') {
|
|
state = const GenerateState(phase: GeneratePhase.done, statusText: '完成');
|
|
break;
|
|
}
|
|
if (status == 'failed') {
|
|
state = GenerateState(
|
|
phase: GeneratePhase.failed,
|
|
error: err.isEmpty ? '生成失败,请稍后重试' : err);
|
|
return false;
|
|
}
|
|
}
|
|
await ref.read(outfitPlanProvider.notifier).refresh();
|
|
return state.phase == GeneratePhase.done;
|
|
}
|
|
|
|
String _statusText(String status) {
|
|
switch (status) {
|
|
case 'pending':
|
|
return '排队中...';
|
|
case 'planning':
|
|
return 'AI 规划穿搭中...';
|
|
case 'scoring':
|
|
return '方案评分中...';
|
|
case 'rendering':
|
|
return '生成效果图中...';
|
|
case 'failed':
|
|
return '生成失败';
|
|
default:
|
|
return '处理中...';
|
|
}
|
|
}
|
|
|
|
void reset() => state = const GenerateState();
|
|
}
|
|
|
|
final generateProvider =
|
|
NotifierProvider<GenerateNotifier, GenerateState>(GenerateNotifier.new);
|
|
|
|
/// 方案详情
|
|
final planDetailProvider = FutureProvider.family<PlanDetail, int>((ref, planId) async {
|
|
final api = ref.read(apiClientProvider);
|
|
final data = await api.get<Map<String, dynamic>>('/outfit/plan/detail',
|
|
query: {'plan_id': planId});
|
|
final planData = data['plan'] as Map<String, dynamic>? ?? {};
|
|
final items = (data['items'] as List<dynamic>? ?? [])
|
|
.map((e) => PlanItemInfo(
|
|
id: (e['id'] as num).toInt(),
|
|
slot: e['slot'] as String? ?? '',
|
|
source: e['source'] as String? ?? 'new',
|
|
wardrobeItemId: (e['wardrobe_item_id'] as num?)?.toInt() ?? 0,
|
|
name: e['name'] as String? ?? '',
|
|
desc: e['desc'] as String? ?? '',
|
|
))
|
|
.toList();
|
|
final images = (data['images'] as List<dynamic>? ?? [])
|
|
.map((e) => PlanEffectImageInfo(
|
|
id: (e['id'] as num).toInt(),
|
|
angle: e['angle'] as String? ?? '',
|
|
url: e['url'] as String? ?? '',
|
|
status: e['status'] as String? ?? 'pending',
|
|
))
|
|
.toList();
|
|
final hairstyle = data['hairstyle'] as Map<String, dynamic>?;
|
|
return PlanDetail(
|
|
plan: OutfitPlanInfo(
|
|
id: (planData['id'] as num).toInt(),
|
|
title: planData['title'] as String? ?? '',
|
|
source: planData['source'] as String? ?? 'rule',
|
|
score: (planData['score'] as num?)?.toInt() ?? 0,
|
|
mainFlag: (planData['main_flag'] as num?)?.toInt() ?? 0,
|
|
dateRange: planData['date_range'] as String? ?? '',
|
|
location: planData['location'] as String? ?? '',
|
|
hairstyleId: (planData['hairstyle_id'] as num?)?.toInt() ?? 0,
|
|
hairColor: planData['hair_color'] as String? ?? '',
|
|
occasion: planData['occasion'] as String? ?? '',
|
|
),
|
|
items: items,
|
|
images: images,
|
|
hairstyleName: hairstyle?['name'] as String? ?? '',
|
|
);
|
|
});
|