Files
slogan/lib/features/outfit/outfit_provider.dart
T

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