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 已注册
This commit is contained in:
commit
689a07ca44
46 files changed
+2986
No files matched your search
+203
@@ -0,0 +1,203 @@
|
||||
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))),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user