import 'dart:io'; import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; import '../agent.dart'; import '../server/protocol.dart'; import '../setup/autostart.dart'; import '../setup/first_run.dart'; import '../setup/url_scheme.dart'; /// 首次引导向导:服务自检 → 打印机检测(含驱动引导)→ 自启/唤起配置。 /// 深色主题对话框,三步 Stepper 风格;允许无打印机完成(纯联调机)。 class SetupWizard extends StatefulWidget { const SetupWizard({super.key, required this.agent, required this.onFinished}); final Agent agent; final VoidCallback onFinished; /// 便捷入口:未完成过向导时弹出。 static Future showIfNeeded(BuildContext context, Agent agent) async { if (await FirstRun.isDone()) return; if (!context.mounted) return; await showDialog( context: context, barrierDismissible: false, builder: (_) => SetupWizard( agent: agent, onFinished: () {}, ), ); } @override State createState() => _SetupWizardState(); } class _SetupWizardState extends State { static const _driverUrl = 'https://www.hiti.com/support/download.aspx?MenuID=72&lang=CN'; int _step = 0; bool _autostart = true; bool _autostartBusy = false; bool _printerChecking = false; PrinterStatus? _printerStatus; Agent get _agent => widget.agent; @override void initState() { super.initState(); _checkPrinter(); } Future _checkPrinter() async { setState(() => _printerChecking = true); try { final status = await _agent.backend.getStatus(); if (mounted) setState(() => _printerStatus = status); } catch (_) { if (mounted) setState(() => _printerStatus = null); } finally { if (mounted) setState(() => _printerChecking = false); } } Future _finish() async { setState(() => _autostartBusy = true); try { if (_autostart && Autostart.isSupported) { await Autostart.enable(); } await UrlScheme.ensureRegistered(); await FirstRun.complete(); } finally { if (mounted) setState(() => _autostartBusy = false); } if (mounted) { Navigator.of(context).pop(); widget.onFinished(); } } @override Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; return Dialog( backgroundColor: scheme.surfaceContainerLow, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(14), side: BorderSide(color: scheme.outlineVariant), ), child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 520), child: Padding( padding: const EdgeInsets.all(24), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _header(scheme), const SizedBox(height: 18), if (_step == 0) _stepWelcome(scheme), if (_step == 1) _stepPrinter(scheme), if (_step == 2) _stepFinish(scheme), const SizedBox(height: 20), _footer(scheme), ], ), ), ), ); } Widget _header(ColorScheme scheme) => Row( children: [ Icon(Icons.extension_rounded, color: scheme.primary, size: 26), const SizedBox(width: 10), Text('初次使用引导', style: Theme.of(context).textTheme.titleLarge), const Spacer(), _stepBadge(scheme), ], ); Widget _stepBadge(ColorScheme scheme) => Row( children: [ for (var i = 0; i < 3; i++) ...[ Container( width: i == _step ? 18 : 8, height: 8, decoration: BoxDecoration( color: i <= _step ? scheme.primary : scheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(4), ), ), if (i < 2) const SizedBox(width: 6), ], ], ); // ---------------- Step 0:服务自检 ---------------- Widget _stepWelcome(ColorScheme scheme) => Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('资安信插件助手在本机为管理端网页提供打印等桌面能力,安装完成后保持后台运行即可,无需日常操作。', style: Theme.of(context).textTheme.bodyMedium), const SizedBox(height: 14), _checkRow( scheme, ok: _agent.port != null, okText: '本机服务运行中${_agent.port != null ? "(127.0.0.1:${_agent.port})" : ""}', badText: '本机服务未启动(可能是端口被占用,请重启应用)', ), if (Autostart.legacyCsharpAgentInstalled) ...[ const SizedBox(height: 8), Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: scheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(8), ), child: Text( '检测到旧版打印助手(C# 版)在本机自启。两者协议兼容可并存,' '如需完全切换到本应用,可在终端执行:\n' 'launchctl unload ~/Library/LaunchAgents/com.zax.cardprinteragent.plist', style: Theme.of(context).textTheme.bodySmall?.copyWith(height: 1.5), ), ), ], ], ); // ---------------- Step 1:打印机检测 ---------------- Widget _stepPrinter(ColorScheme scheme) { final s = _printerStatus; final ready = s != null && s.online; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _checkRow( scheme, ok: ready, loading: _printerChecking, okText: '打印机就绪${s != null && s.model.isNotEmpty ? "(${s.model})" : ""}', badText: s == null ? '未检测到打印队列' : (s.statusText.isEmpty ? '打印机未就绪' : s.statusText), ), if (!ready) ...[ const SizedBox(height: 12), Text( Platform.isMacOS ? '安装步骤:① 打开 HiTi 官网下载 macOS 驱动(HiTiCardTypeMacDrivers dmg),双击安装里面的 pkg;② USB 连接打印机并开机;③ 系统设置 → 打印机与扫描仪 确认出现 HiTi 打印机;④ 回到这里点「重新检测」。' : 'Windows 打印站:打开 HiTi 官网下载 CS-2xx Windows 驱动并安装,USB 连接打印机;本应用也支持免驱动的 SDK 直连模式,装完驱动或接好打印机后点「重新检测」。', style: Theme.of(context).textTheme.bodySmall?.copyWith(height: 1.6), ), const SizedBox(height: 8), Align( alignment: Alignment.centerLeft, child: TextButton.icon( onPressed: () => launchUrl(Uri.parse(_driverUrl)), icon: const Icon(Icons.open_in_new, size: 14), label: const Text('打开 HiTi 驱动下载页'), ), ), const SizedBox(height: 4), TextButton.icon( onPressed: _printerChecking ? null : _checkPrinter, icon: const Icon(Icons.refresh, size: 16), label: const Text('重新检测'), ), ] else ...[ const SizedBox(height: 12), Text('打印机已连接,可以正常接收管理端提交的卡面打印。', style: Theme.of(context).textTheme.bodySmall), ], ], ); } // ---------------- Step 2:完成配置 ---------------- Widget _stepFinish(ColorScheme scheme) => Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SwitchListTile( contentPadding: EdgeInsets.zero, value: _autostart, onChanged: Autostart.isSupported ? (v) => setState(() => _autostart = v) : null, title: const Text('开机自动启动'), subtitle: Text( Autostart.isSupported ? '登录系统后自动在后台运行(可在左侧菜单底部随时修改)' : '当前平台不支持', style: Theme.of(context).textTheme.bodySmall, ), ), const SizedBox(height: 6), _checkRow( scheme, ok: true, okText: '网页唤起支持(zaxprint:// 协议)已启用', badText: '', ), const SizedBox(height: 8), Text( '全部完成。关闭窗口将最小化到托盘,服务持续运行;退出请通过托盘菜单。', style: Theme.of(context).textTheme.bodySmall, ), ], ); // ---------------- 公共组件 ---------------- Widget _checkRow(ColorScheme scheme, {required bool ok, required String okText, required String badText, bool loading = false}) { final color = ok ? scheme.primary : const Color(0xFFD97706); return Row( children: [ if (loading) const SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2)) else Icon(ok ? Icons.check_circle : Icons.info_outline, size: 18, color: color), const SizedBox(width: 8), Expanded( child: Text( ok ? okText : badText, style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: color), ), ), ], ); } Widget _footer(ColorScheme scheme) => Row( children: [ if (_step > 0) TextButton( onPressed: () => setState(() => _step--), child: const Text('上一步'), ), const Spacer(), if (_step < 2) FilledButton( onPressed: () => setState(() => _step++), child: Text(_step == 0 ? '下一步' : '跳过,下一步'), ) else FilledButton( onPressed: _autostartBusy ? null : _finish, child: const Text('完成'), ), ], ); }