part of 'studio_app.dart'; class StudioOperationsPage extends StatefulWidget { const StudioOperationsPage({ super.key, required this.tab, required this.runtime, required this.onAction, }); final StudioTab tab; final StudioRuntime runtime; final void Function(String title, String detail) onAction; @override State createState() => _StudioOperationsPageState(); } class _StudioOperationsPageState extends State { bool _loading = false; String _status = '等待执行'; String _detail = ''; Map? _lastData; Future _downloadConfigs() async { setState(() { _loading = true; _status = '正在下载配置包...'; _detail = ''; _lastData = null; }); try { final result = await widget.runtime.client.downloadModelConfigs( session: widget.runtime.session, ); final savedPath = await saveBytesToFile( bytes: result.bytes, suggestedName: result.filename, ); if (!mounted) { return; } setState(() { _status = savedPath == null ? '已下载配置包,但未保存' : '配置包已保存'; _detail = savedPath == null ? '文件名:${result.filename}\n大小:${result.bytes.length} bytes' : '文件:$savedPath\n文件名:${result.filename}\n大小:${result.bytes.length} bytes'; _lastData = { 'filename': result.filename, 'contentType': result.contentType, 'statusCode': result.statusCode, 'savedPath': savedPath, 'size': result.bytes.length, }; }); widget.onAction('下载配置', result.filename); } catch (error, stackTrace) { _printError('下载配置失败', error, stackTrace); if (!mounted) { return; } setState(() { _status = '下载失败'; _detail = _normalizeError(error); }); } finally { if (mounted) { setState(() { _loading = false; }); } } } Future _repairDepartment() async { setState(() { _loading = true; _status = '正在执行部门修复...'; _detail = ''; _lastData = null; }); try { final result = await widget.runtime.client.repairDepartment( session: widget.runtime.session, ); if (!mounted) { return; } setState(() { _status = result.success ? '修复完成' : '修复失败'; _detail = _formatActionResult(result); _lastData = result.raw; }); widget.onAction('部门修复', result.message.isNotEmpty ? result.message : '已执行'); } catch (error, stackTrace) { _printError('部门修复失败', error, stackTrace); if (!mounted) { return; } setState(() { _status = '修复失败'; _detail = _normalizeError(error); }); } finally { if (mounted) { setState(() { _loading = false; }); } } } void _printError(String title, Object error, StackTrace stackTrace) { debugPrint('========== $title =========='); debugPrint('error: $error'); debugPrint('stackTrace:\n$stackTrace'); debugPrint('========== $title end =========='); } String _normalizeError(Object error) { final text = error.toString(); const marker = 'Bad state: '; if (text.startsWith(marker)) { return text.substring(marker.length); } return text; } String _formatActionResult(StudioBackendActionResult result) { final buffer = StringBuffer() ..writeln('code: ${result.code.isEmpty ? '空' : result.code}') ..writeln('message: ${result.message.isEmpty ? '无' : result.message}') ..writeln('data: ${const JsonEncoder.withIndent(' ').convert(result.data)}') ..writeln('raw: ${const JsonEncoder.withIndent(' ').convert(result.raw)}'); return buffer.toString(); } @override Widget build(BuildContext context) { final isRepair = widget.tab.pageType == StudioPageType.repair; final primaryAction = isRepair ? _repairDepartment : _downloadConfigs; final primaryLabel = isRepair ? '执行修复' : '下载配置'; final primaryIcon = isRepair ? Icons.build_rounded : Icons.download_rounded; final secondaryHint = isRepair ? '部门修复会直接调用后端接口,执行后请刷新相关页面确认数据。' : '下载配置会调用后端压缩包接口,并让你选择本地保存位置。'; return SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Card( child: Padding( padding: const EdgeInsets.all(18), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( widget.tab.title, style: Theme.of(context).textTheme.headlineSmall ?.copyWith(fontWeight: FontWeight.w700), ), const SizedBox(height: 6), Text( widget.tab.summary, style: Theme.of(context).textTheme.bodyMedium ?.copyWith(color: _mutedColor(context)), ), const SizedBox(height: 16), LinearProgressIndicator( value: _loading ? null : 1.0, minHeight: 10, borderRadius: BorderRadius.circular(999), ), ], ), ), ), const SizedBox(height: 16), Card( child: Padding( padding: const EdgeInsets.all(18), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '操作说明', style: Theme.of(context).textTheme.titleLarge ?.copyWith(fontWeight: FontWeight.w700), ), const SizedBox(height: 8), Text(secondaryHint), const SizedBox(height: 14), Wrap( spacing: 10, runSpacing: 10, children: [ FilledButton.icon( onPressed: _loading ? null : primaryAction, icon: _loading ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ) : Icon(primaryIcon), label: Text(primaryLabel), ), FilledButton.tonalIcon( onPressed: _loading ? null : () => widget.onAction('记录动作', widget.tab.title), icon: const Icon(Icons.history_rounded), label: const Text('记录动作'), ), ], ), ], ), ), ), const SizedBox(height: 16), Card( child: Padding( padding: const EdgeInsets.all(18), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '执行结果', style: Theme.of(context).textTheme.titleLarge ?.copyWith(fontWeight: FontWeight.w700), ), const SizedBox(height: 10), SelectableText( _status, style: Theme.of(context).textTheme.bodyLarge ?.copyWith(fontWeight: FontWeight.w700), ), if (_detail.isNotEmpty) ...[ const SizedBox(height: 10), SelectableText( _detail, style: Theme.of(context).textTheme.bodyMedium, ), ], if (_lastData != null) ...[ const SizedBox(height: 12), SelectableText( const JsonEncoder.withIndent(' ').convert(_lastData), style: const TextStyle(fontFamily: 'monospace', height: 1.45), ), ], ], ), ), ), ], ), ); } }