Files
desktop-plugin/lib/main.dart
T
dzc 689a07ca44 feat: 饭卡打印助手 Flutter 版(Phase 1/2:协议服务器 + macOS CUPS 真机打印)
- 协议与 C# 版 card-printer-agent 逐字段对齐(38200-38202 顺延、CORS *、串行任务队列、30 分钟保留)
- macOS CUPS 后端:lpoptions 队列发现 + lp 参数列表提交(LC_ALL=C 防本地化粘连坑)
- --mock 模拟模式 + bin/agent.dart headless 入口 + 测试卡生成工具
- 真机验收:HiTi CS-200e status 就绪 + 真实出卡;macOS 通用二进制构建通过
- App Sandbox 已关闭(服务需绑回环端口并调用 lp 子进程);zaxprint URL scheme 已注册
2026-09-21 12:03:27 +08:00

204 lines
6.1 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'agent.dart';
import 'server/protocol.dart';
/// 饭卡打印助手主窗口(Phase 1 最小版:服务状态 + 打印机状态 + 运行日志;
/// 托盘/首次引导/驱动安装在后续 Phase 增强)。
Future<void> main(List<String> args) async {
WidgetsFlutterBinding.ensureInitialized();
final agent = Agent(AgentOptions.parse(args));
String? startError;
try {
await agent.start();
} catch (e) {
startError = '$e';
}
runApp(DesktopPluginApp(agent: agent, startError: startError));
}
class DesktopPluginApp extends StatelessWidget {
const DesktopPluginApp({super.key, required this.agent, this.startError});
final Agent agent;
final String? startError;
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '饭卡打印助手',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF0F6FDE)),
useMaterial3: true,
),
home: AgentHomePage(agent: agent, startError: startError),
);
}
}
class AgentHomePage extends StatefulWidget {
const AgentHomePage({super.key, required this.agent, this.startError});
final Agent agent;
final String? startError;
@override
State<AgentHomePage> createState() => _AgentHomePageState();
}
class _AgentHomePageState extends State<AgentHomePage> {
PrinterStatus? _status;
Object? _statusError;
bool _loading = false;
StreamSubscription<String>? _logSub;
final _logs = <String>[];
@override
void initState() {
super.initState();
_logSub = widget.agent.log.stream.listen((line) {
if (mounted) setState(() => _logs.add(line));
});
_logs.addAll(widget.agent.log.lines);
_refreshStatus();
}
@override
void dispose() {
_logSub?.cancel();
super.dispose();
}
Future<void> _refreshStatus() async {
setState(() => _loading = true);
try {
final status = await widget.agent.backend.getStatus();
if (!mounted) return;
setState(() {
_status = status;
_statusError = null;
});
} catch (e) {
if (!mounted) return;
setState(() => _statusError = '$e');
} finally {
if (mounted) setState(() => _loading = false);
}
}
@override
Widget build(BuildContext context) {
final port = widget.agent.port;
return Scaffold(
appBar: AppBar(
title: const Text('饭卡打印助手'),
actions: [
IconButton(icon: const Icon(Icons.refresh), tooltip: '刷新状态', onPressed: _refreshStatus),
const SizedBox(width: 8),
],
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
_serviceCard(port),
const SizedBox(height: 12),
_printerCard(),
const SizedBox(height: 12),
_logCard(),
],
),
);
}
Widget _serviceCard(int? port) {
final err = widget.startError;
return Card(
child: Padding(
padding: const EdgeInsets.all(14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
err == null && port != null ? Icons.check_circle : Icons.error,
color: err == null && port != null ? Colors.green : Colors.red,
),
const SizedBox(width: 8),
Text('本机服务', style: Theme.of(context).textTheme.titleMedium),
],
),
const SizedBox(height: 8),
Text(err ?? (port != null ? 'http://127.0.0.1:$port (管理端开卡页自动发现)' : '未启动')),
const SizedBox(height: 4),
Text('后端:${widget.agent.options.mock ? "mock(模拟)" : "系统打印队列"}'),
],
),
),
);
}
Widget _printerCard() {
final status = _status;
final err = _statusError;
return Card(
child: Padding(
padding: const EdgeInsets.all(14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
if (_loading)
const SizedBox(width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2))
else
Icon(
status != null && status.online ? Icons.print : Icons.print_disabled,
color: status != null && status.online ? Colors.green : Colors.orange,
),
const SizedBox(width: 8),
Text('打印机', style: Theme.of(context).textTheme.titleMedium),
],
),
const SizedBox(height: 8),
if (err != null)
Text('状态读取失败:$err', style: const TextStyle(color: Colors.red))
else if (status != null) ...[
Text('${status.model.isEmpty ? "未知型号" : status.model} · ${status.statusText}'),
const SizedBox(height: 4),
Text(
'色带余量 ${status.ribbon.remain >= 0 ? "${status.ribbon.remain}%" : "未知"}'
' · 剩卡 ${status.cardLeft >= 0 ? "${status.cardLeft} 张" : "未知"}',
style: TextStyle(color: Theme.of(context).hintColor),
),
],
],
),
),
);
}
Widget _logCard() {
return Card(
child: Padding(
padding: const EdgeInsets.all(14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('运行日志', style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8),
if (_logs.isEmpty)
Text('(暂无)', style: TextStyle(color: Theme.of(context).hintColor))
else
...(_logs.length > 50 ? _logs.sublist(_logs.length - 50) : _logs)
.map((line) => Text(line, style: const TextStyle(fontFamily: 'monospace', fontSize: 12))),
],
),
),
);
}
}