Compare commits

...

8 Commits

Author SHA1 Message Date
lifangliang
34890faee6 优化面板日志 2025-07-11 18:02:49 +08:00
lifangliang
b0b6c6dfad 新增日志面板,ws连接状态 2025-07-11 17:58:17 +08:00
lifangliang
e525e06d7a 新增服务器连接配置 2025-07-11 17:47:22 +08:00
lifangliang
1486e1f6ec 优化统计信息无法显示的问题 2025-07-11 17:05:35 +08:00
lifangliang
cba45ba926 国际化优化 2025-07-11 16:17:26 +08:00
lifangliang
40d7c2f886 国际化优化 2025-07-10 17:53:55 +08:00
lifangliang
3c4e72cd58 新增国际化 2025-07-10 17:43:32 +08:00
lifangliang
110ee99978 新增打印机图像页面 2025-07-01 10:01:07 +08:00
33 changed files with 5346 additions and 62 deletions

View File

@ -0,0 +1,58 @@
# 循环依赖解决方案
## 当前问题
在应用程序上下文中存在循环依赖:
```
┌─────┐
| printController (field private com.goeing.printserver.main.service.PrintQueueService com.goeing.printserver.main.PrintController.printQueueService)
↑ ↓
| printQueueService (field private com.goeing.printserver.main.PrintController com.goeing.printserver.main.service.PrintQueueService.printController)
└─────┘
```
## 临时解决方案
已在 `application.properties` 中添加以下配置,临时允许循环依赖:
```properties
spring.main.allow-circular-references=true
```
## 已实施的重构步骤
1. 创建了 `PrintService` 接口,定义打印相关操作
2. 修改 `PrintController` 实现 `PrintService` 接口
3. 修改 `PrintQueueService` 依赖 `PrintService` 而不是直接依赖 `PrintController`
## 后续重构建议
为彻底解决循环依赖问题,建议进一步重构:
### 方案一:使用事件驱动架构
1. 使用 Spring 的事件机制ApplicationEventPublisher替代直接方法调用
2. `PrintQueueService` 发布打印事件
3. `PrintController` 订阅并处理这些事件
### 方案二:引入服务层抽象
1. 创建更完整的服务层抽象,明确职责分离
2. 将 `PrintController` 中的业务逻辑移至专门的服务类
3. 让 `PrintController``PrintQueueService` 都依赖这个新服务类
### 方案三:重新设计组件职责
1. 重新评估 `PrintController``PrintQueueService` 的职责
2. 可能的职责划分:
- `PrintController`:仅处理 HTTP 请求和响应
- `PrintQueueService`:管理打印队列和执行打印操作
- 新增 `PrintExecutionService`:实际执行打印操作的逻辑
## 最佳实践
- 遵循单一职责原则,每个类只负责一个功能领域
- 使用依赖注入,但避免双向依赖
- 考虑使用事件驱动架构处理组件间通信
- 使用接口进行解耦,降低组件间直接依赖

View File

@ -0,0 +1,76 @@
# macOS 系统上的 HeadlessException 解决方案
## 问题描述
在 macOS 系统上运行打印服务器时,可能会遇到 `java.awt.HeadlessException` 错误,错误信息类似:
```
Exception in thread "AWT-EventQueue-0" java.awt.HeadlessException
at java.desktop/java.awt.GraphicsEnvironment.checkHeadless(GraphicsEnvironment.java:166)
at java.desktop/java.awt.Window.<init>(Window.java:553)
at java.desktop/java.awt.Frame.<init>(Frame.java:428)
at java.desktop/javax.swing.JFrame.<init>(JFrame.java:224)
at com.goeing.printserver.main.gui.PrintQueueGUI.initializeGUI(PrintQueueGUI.java:58)
```
这个错误表明应用程序在无头模式Headless Mode下运行但尝试创建图形界面组件。在 macOS 系统上特别是在某些环境下如远程会话、无显示器连接或特定的系统配置Java 应用程序可能会自动进入无头模式。
## 解决方案
我们已经对应用程序进行了优化,以更好地处理无头模式。现在有以下几种方式可以解决这个问题:
### 1. 使用命令行参数启用无头模式
如果您知道系统不支持图形界面,可以在启动应用程序时明确指定无头模式:
```bash
java -Djava.awt.headless=true -jar goeingPrintServer.jar
```
### 2. 通过配置文件设置
`application.properties` 文件中,我们添加了一个配置项:
```properties
# 在macOS系统上如果遇到HeadlessException可以设置为true强制使用无头模式
app.force.headless=false
```
将此值设置为 `true` 可以强制应用程序以无头模式运行。
### 3. 自动检测和适应
应用程序现在会自动检测系统是否支持图形界面,并在不支持时自动切换到无头模式。在无头模式下:
- 图形界面组件不会被初始化
- 系统托盘图标不会显示
- 通知功能将被禁用
- 打印功能仍然正常工作
## 无头模式下的功能
在无头模式下,应用程序仍然可以通过以下方式使用:
1. **REST API**:所有打印功能都可以通过 REST API 访问
2. **WebSocket**:打印请求可以通过 WebSocket 连接发送
3. **命令行**:可以通过命令行工具与应用程序交互
## 日志输出
当应用程序检测到无头模式时,会在日志中输出相关信息:
```
当前环境不支持图形界面,将以无头模式运行
系统运行在无头模式下,通知功能将被禁用
```
## 技术说明
我们通过以下方式改进了应用程序对无头模式的处理:
1. 在应用启动时检测系统环境
2. 在检测到无头模式时设置系统属性 `app.headless.mode=true`
3. 所有图形界面组件在初始化前检查此属性
4. 添加了异常处理,防止图形界面初始化失败导致整个应用崩溃
这些改进确保了应用程序在各种环境下都能稳定运行,无论是否支持图形界面。

View File

@ -2,12 +2,47 @@ package com.goeing.printserver;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.core.env.Environment;
import org.springframework.context.ConfigurableApplicationContext;
import java.awt.GraphicsEnvironment;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@SpringBootApplication
public class GoeingPrintServerApplication {
private static final Logger log = LoggerFactory.getLogger(GoeingPrintServerApplication.class);
public static void main(String[] args) {
SpringApplication.run(GoeingPrintServerApplication.class, args);
// 检查是否在macOS系统上运行
String osName = System.getProperty("os.name").toLowerCase();
boolean isMacOS = osName.contains("mac");
// 检查是否已经设置了java.awt.headless系统属性
String headlessProperty = System.getProperty("java.awt.headless");
// 如果是macOS并且没有明确设置headless属性可能需要特殊处理
if (isMacOS && headlessProperty == null) {
log.info("在macOS系统上运行检查是否需要启用无头模式");
// 检查是否支持图形界面
if (GraphicsEnvironment.isHeadless()) {
log.warn("检测到系统不支持图形界面,自动启用无头模式");
System.setProperty("java.awt.headless", "true");
System.setProperty("app.headless.mode", "true");
}
}
ConfigurableApplicationContext context = SpringApplication.run(GoeingPrintServerApplication.class, args);
// 从配置中读取是否强制使用无头模式
Environment env = context.getEnvironment();
boolean forceHeadless = Boolean.parseBoolean(env.getProperty("app.force.headless", "false"));
if (forceHeadless) {
log.info("根据配置强制启用无头模式");
System.setProperty("java.awt.headless", "true");
System.setProperty("app.headless.mode", "true");
}
}
}

View File

@ -0,0 +1,40 @@
package com.goeing.printserver.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.i18n.SessionLocaleResolver;
import java.util.Locale;
/**
* 国际化配置类
*/
@Configuration
@ComponentScan(basePackages = {"com.goeing.printserver.main.utils"})
public class MessageConfig {
/**
* 配置消息源
*/
@Bean
public ResourceBundleMessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasenames("messages");
messageSource.setDefaultEncoding("UTF-8");
return messageSource;
}
/**
* 配置区域解析器
*/
@Bean
public LocaleResolver localeResolver() {
SessionLocaleResolver resolver = new SessionLocaleResolver();
resolver.setDefaultLocale(Locale.getDefault());
return resolver;
}
}

View File

@ -3,11 +3,16 @@ package com.goeing.printserver.main;
import cn.hutool.core.util.IdUtil;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONUtil;
import com.goeing.printserver.main.config.PrintServerConfig;
import com.goeing.printserver.main.domain.request.PrintRequest;
import com.goeing.printserver.main.service.PrintQueueService;
import com.goeing.printserver.main.service.PrintService;
import com.goeing.printserver.main.utils.PdfPrinter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.print.PrintService;
// 使用完全限定名称避免与自定义PrintService接口冲突
import java.awt.print.PrinterJob;
import java.io.File;
import java.util.Arrays;
@ -19,7 +24,15 @@ import java.util.stream.Collectors;
@RestController
@RequestMapping("/api")
public class PrintController {
@Slf4j
public class PrintController implements PrintService {
@Autowired
private PrintQueueService printQueueService;
@Autowired
private PrintServerConfig config;
private final String rootPath = "/Users/fl0919/work_space/goeingPrintServer/pdf";
/**
@ -29,8 +42,8 @@ public class PrintController {
*/
@GetMapping("printerList")
public List<String> printerList() {
PrintService[] printServices = PrinterJob.lookupPrintServices();
Set<String> collect = Arrays.stream(printServices).map(PrintService::getName).collect(Collectors.toSet());
javax.print.PrintService[] printServices = PrinterJob.lookupPrintServices();
Set<String> collect = Arrays.stream(printServices).map(service -> service.getName()).collect(Collectors.toSet());
return collect.stream().sorted().collect(Collectors.toList());
}
@ -51,10 +64,38 @@ public class PrintController {
}
/**
* 获取当前打印队列状态
*
* @return 包含队列信息的Map
*/
@GetMapping("queue/status")
public Map<String, Object> getQueueStatus() {
Map<String, Object> status = new HashMap<>();
status.put("queueSize", printQueueService.getQueueSize());
status.put("timestamp", System.currentTimeMillis());
status.put("currentTask", printQueueService.getCurrentTaskInfo());
return status;
}
/**
* 获取打印队列中的所有任务
*
* @return 任务列表
*/
@GetMapping("queue/tasks")
public Map<String, Object> getQueueTasks() {
Map<String, Object> result = new HashMap<>();
result.put("currentTask", printQueueService.getCurrentTaskInfo());
result.put("queuedTasks", printQueueService.getQueuedTasksInfo());
result.put("timestamp", System.currentTimeMillis());
return result;
}
@PostMapping("print")
public String print(@RequestBody PrintRequest request) {
// 记录请求信息
System.out.println("Received print request: " + JSONUtil.toJsonPrettyStr(request));
log.info("收到打印请求: {}", JSONUtil.toJsonPrettyStr(request));
// 参数验证
if (request == null) {
@ -65,8 +106,14 @@ public class PrintController {
throw new IllegalArgumentException("File URL cannot be null or empty");
}
// 如果打印机名称为空使用默认打印机
if (request.getPrinterName() == null || request.getPrinterName().trim().isEmpty()) {
throw new IllegalArgumentException("Printer name cannot be null or empty");
String defaultPrinter = config.getDefaultPrinter();
if (defaultPrinter == null || defaultPrinter.trim().isEmpty()) {
throw new IllegalArgumentException("Printer name cannot be null or empty, and no default printer is configured");
}
request.setPrinterName(defaultPrinter);
log.info("使用默认打印机: {}", defaultPrinter);
}
// 验证打印机是否存在
@ -90,7 +137,7 @@ public class PrintController {
try {
// 下载文件
System.out.println("Downloading file from: " + fileUrl);
log.info("正在从以下地址下载文件: {}", fileUrl);
HttpUtil.downloadFile(fileUrl, filePath);
if (!pdfFile.exists() || pdfFile.length() == 0) {
@ -98,12 +145,12 @@ public class PrintController {
}
// 打印文件
System.out.println("Printing file to printer: " + request.getPrinterName());
log.info("正在将文件发送到打印机: {}", request.getPrinterName());
PdfPrinter.print(filePath, request.getPrinterName(), request.getPrintOption());
return "success";
} catch (Exception e) {
System.err.println("Error during print process: " + e.getMessage());
log.error("打印过程中发生错误: {}", e.getMessage(), e);
e.printStackTrace();
throw new RuntimeException("Print failed: " + e.getMessage(), e);
} finally {
@ -111,9 +158,9 @@ public class PrintController {
if (pdfFile.exists()) {
try {
pdfFile.delete();
System.out.println("Temporary file deleted: " + filePath);
log.debug("临时文件已删除: {}", filePath);
} catch (Exception e) {
System.err.println("Failed to delete temporary file: " + filePath);
log.warn("删除临时文件失败: {}", filePath);
}
}
}

View File

@ -0,0 +1,137 @@
package com.goeing.printserver.main.config;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import jakarta.annotation.PostConstruct;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Properties;
/**
* 打印服务器配置类
* 负责管理打印服务器的所有配置信息包括默认打印机最大队列大小通知设置等
*/
@Component
@Slf4j
@Data
public class PrintServerConfig {
private static final String CONFIG_FILE = "printserver.properties";
// 默认配置值
private static final String DEFAULT_PRINTER = "默认打印机";
private static final int DEFAULT_MAX_QUEUE_SIZE = 10;
private static final boolean DEFAULT_ENABLE_NOTIFICATIONS = true;
private static final boolean DEFAULT_START_MINIMIZED = false;
private static final boolean DEFAULT_AUTO_START = false;
private static final String DEFAULT_WEBSOCKET_URL = "ws://127.0.0.1:8080/print-websocket";
private static final String DEFAULT_PRINTER_ID = "123456";
private static final String DEFAULT_API_KEY = "519883ab-3677-ce4b-59ba-7263870d0a26";
// 配置属性
private String defaultPrinter = DEFAULT_PRINTER;
private int maxQueueSize = DEFAULT_MAX_QUEUE_SIZE;
private boolean enableNotifications = DEFAULT_ENABLE_NOTIFICATIONS;
private boolean startMinimized = DEFAULT_START_MINIMIZED;
private boolean autoStart = DEFAULT_AUTO_START;
private String websocketUrl = DEFAULT_WEBSOCKET_URL;
private String printerId = DEFAULT_PRINTER_ID;
private String apiKey = DEFAULT_API_KEY;
private Properties properties = new Properties();
private File configFile;
@PostConstruct
public void init() {
// 确定配置文件路径
String userHome = System.getProperty("user.home");
configFile = new File(userHome + File.separator + ".goeing" + File.separator + CONFIG_FILE);
// 确保目录存在
if (!configFile.getParentFile().exists()) {
configFile.getParentFile().mkdirs();
}
// 加载配置
loadConfig();
}
/**
* 加载配置
*/
public void loadConfig() {
if (configFile.exists()) {
try (FileInputStream fis = new FileInputStream(configFile)) {
properties.load(fis);
// 读取配置值
defaultPrinter = properties.getProperty("defaultPrinter", DEFAULT_PRINTER);
maxQueueSize = Integer.parseInt(properties.getProperty("maxQueueSize", String.valueOf(DEFAULT_MAX_QUEUE_SIZE)));
enableNotifications = Boolean.parseBoolean(properties.getProperty("enableNotifications", String.valueOf(DEFAULT_ENABLE_NOTIFICATIONS)));
startMinimized = Boolean.parseBoolean(properties.getProperty("startMinimized", String.valueOf(DEFAULT_START_MINIMIZED)));
autoStart = Boolean.parseBoolean(properties.getProperty("autoStart", String.valueOf(DEFAULT_AUTO_START)));
websocketUrl = properties.getProperty("websocketUrl", DEFAULT_WEBSOCKET_URL);
printerId = properties.getProperty("printerId", DEFAULT_PRINTER_ID);
apiKey = properties.getProperty("apiKey", DEFAULT_API_KEY);
log.info("配置已加载: {}", configFile.getAbsolutePath());
} catch (IOException e) {
log.error("加载配置文件失败", e);
// 使用默认值
resetToDefaults();
} catch (NumberFormatException e) {
log.error("解析配置值失败", e);
// 使用默认值
resetToDefaults();
}
} else {
log.info("配置文件不存在,使用默认配置");
// 使用默认值并保存
resetToDefaults();
saveConfig();
}
}
/**
* 保存配置
*/
public void saveConfig() {
try {
// 更新属性
properties.setProperty("defaultPrinter", defaultPrinter);
properties.setProperty("maxQueueSize", String.valueOf(maxQueueSize));
properties.setProperty("enableNotifications", String.valueOf(enableNotifications));
properties.setProperty("startMinimized", String.valueOf(startMinimized));
properties.setProperty("autoStart", String.valueOf(autoStart));
properties.setProperty("websocketUrl", websocketUrl);
properties.setProperty("printerId", printerId);
properties.setProperty("apiKey", apiKey);
// 保存到文件
try (FileOutputStream fos = new FileOutputStream(configFile)) {
properties.store(fos, "Goeing Print Server Configuration");
log.info("配置已保存: {}", configFile.getAbsolutePath());
}
} catch (IOException e) {
log.error("保存配置文件失败", e);
}
}
/**
* 重置为默认配置
*/
public void resetToDefaults() {
defaultPrinter = DEFAULT_PRINTER;
maxQueueSize = DEFAULT_MAX_QUEUE_SIZE;
enableNotifications = DEFAULT_ENABLE_NOTIFICATIONS;
startMinimized = DEFAULT_START_MINIMIZED;
autoStart = DEFAULT_AUTO_START;
websocketUrl = DEFAULT_WEBSOCKET_URL;
printerId = DEFAULT_PRINTER_ID;
apiKey = DEFAULT_API_KEY;
}
}

View File

@ -0,0 +1,55 @@
package com.goeing.printserver.main.domain;
import com.goeing.printserver.main.domain.bo.PrintOption;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
/**
* 打印任务实体类用于表示一个打印任务
*/
@Data
public class PrintTask {
// 文件URL
private String fileUrl;
// 打印机名称
private String printer;
// 任务状态queued, processing, completed, failed
private String status;
// 排队时间
private LocalDateTime queuedTime;
// 开始处理时间
private LocalDateTime startTime;
// 结束时间
private LocalDateTime endTime;
// 打印选项
private PrintOption printOptions;
/**
* 将任务转换为Map
*
* @return 包含任务信息的Map
*/
public Map<String, Object> toMap() {
Map<String, Object> map = new HashMap<>();
map.put("fileUrl", fileUrl);
map.put("printerName", printer);
map.put("status", status);
map.put("queuedTime", queuedTime);
if (startTime != null) {
map.put("startTime", startTime);
}
if (endTime != null) {
map.put("endTime", endTime);
}
return map;
}
}

View File

@ -0,0 +1,179 @@
package com.goeing.printserver.main.gui;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.util.Locale;
import com.goeing.printserver.main.utils.LocaleChangeListener;
import com.goeing.printserver.main.utils.LocaleManager;
import com.goeing.printserver.main.utils.MessageUtils;
/**
* 关于对话框显示应用程序信息
*/
public class AboutDialog extends JDialog implements LocaleChangeListener {
private JLabel titleLabel;
private JLabel versionLabel;
private JLabel buildDateLabel;
private JLabel jdkVersionLabel;
private JLabel osVersionLabel;
private JLabel copyrightLabel;
private JButton closeButton;
/**
* 创建关于对话框
*
* @param parent 父窗口
*/
public AboutDialog(Window parent) {
super(parent, MessageUtils.getMessage("about.title"), ModalityType.APPLICATION_MODAL);
initializeUI();
// 注册语言变更监听器
LocaleManager.getInstance().addLocaleChangeListener(this);
}
/**
* 初始化用户界面
*/
private void initializeUI() {
setSize(400, 300);
setLocationRelativeTo(getOwner());
setResizable(false);
setLayout(new BorderLayout());
// 创建图标面板
JPanel iconPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));
iconPanel.setBorder(BorderFactory.createEmptyBorder(20, 0, 10, 0));
// 创建应用图标
ImageIcon appIcon = createAppIcon();
JLabel iconLabel = new JLabel(appIcon);
iconPanel.add(iconLabel);
add(iconPanel, BorderLayout.NORTH);
// 创建信息面板
JPanel infoPanel = new JPanel();
infoPanel.setLayout(new BoxLayout(infoPanel, BoxLayout.Y_AXIS));
infoPanel.setBorder(BorderFactory.createEmptyBorder(10, 20, 10, 20));
// 添加应用信息
titleLabel = addInfoLabel(infoPanel, MessageUtils.getMessage("about.app.name"), Font.BOLD, 16);
versionLabel = addInfoLabel(infoPanel, MessageUtils.getMessage("about.version", new Object[]{"1.0.0"}), Font.PLAIN, 12);
buildDateLabel = addInfoLabel(infoPanel, MessageUtils.getMessage("about.build.date", new Object[]{java.time.LocalDate.now().toString()}), Font.PLAIN, 12);
jdkVersionLabel = addInfoLabel(infoPanel, MessageUtils.getMessage("about.jdk.version", new Object[]{System.getProperty("java.version")}), Font.PLAIN, 12);
osVersionLabel = addInfoLabel(infoPanel, MessageUtils.getMessage("about.os.version", new Object[]{System.getProperty("os.name") + " " + System.getProperty("os.version")}), Font.PLAIN, 12);
// 添加空白间隔
infoPanel.add(Box.createVerticalStrut(10));
// 添加版权信息
copyrightLabel = addInfoLabel(infoPanel, MessageUtils.getMessage("about.copyright", new Object[]{java.time.Year.now().getValue()}), Font.ITALIC, 12);
add(infoPanel, BorderLayout.CENTER);
// 创建按钮面板
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));
closeButton = new JButton(MessageUtils.getMessage("button.close"));
closeButton.addActionListener(e -> {
// 移除语言变更监听器
LocaleManager.getInstance().removeLocaleChangeListener(this);
dispose();
});
buttonPanel.add(closeButton);
add(buttonPanel, BorderLayout.SOUTH);
}
/**
* 添加信息标签
*
* @param panel 面板
* @param text 文本
* @param fontStyle 字体样式
* @param fontSize 字体大小
*/
private JLabel addInfoLabel(JPanel panel, String text, int fontStyle, int fontSize) {
JLabel label = new JLabel(text);
label.setFont(new Font(label.getFont().getName(), fontStyle, fontSize));
label.setAlignmentX(0.5f); // CENTER_ALIGNMENT = 0.5f
panel.add(label);
panel.add(Box.createVerticalStrut(5));
return label;
}
/**
* 创建应用图标
*
* @return 图标
*/
private ImageIcon createAppIcon() {
// 创建一个简单的打印机图标
int iconSize = 64;
BufferedImage image = new BufferedImage(iconSize, iconSize, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = image.createGraphics();
// 启用抗锯齿
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// 绘制打印机主体
g2d.setColor(new Color(50, 50, 50));
g2d.fillRect(10, 25, 44, 25);
// 绘制打印机纸盒
g2d.setColor(new Color(200, 200, 200));
g2d.fillRect(15, 50, 34, 5);
// 绘制打印机出纸口
g2d.setColor(new Color(240, 240, 240));
g2d.fillRect(15, 15, 34, 10);
// 绘制打印纸
g2d.setColor(Color.WHITE);
g2d.fillRect(20, 5, 24, 20);
// 绘制打印机按钮
g2d.setColor(new Color(0, 150, 200));
g2d.fillOval(45, 30, 5, 5);
g2d.dispose();
return new ImageIcon(image);
}
/**
* 显示关于对话框
*
* @param parent 父窗口
*/
public static void showDialog(Window parent) {
AboutDialog dialog = new AboutDialog(parent);
dialog.setVisible(true);
}
/**
* 当语言变更时更新UI元素
* @param newLocale 新的语言区域
*/
@Override
public void onLocaleChanged(Locale newLocale) {
// 更新窗口标题
setTitle(MessageUtils.getMessage("about.title"));
// 更新标签文本
titleLabel.setText(MessageUtils.getMessage("about.app.name"));
versionLabel.setText(MessageUtils.getMessage("about.version", new Object[]{"1.0.0"}));
buildDateLabel.setText(MessageUtils.getMessage("about.build.date", new Object[]{java.time.LocalDate.now().toString()}));
jdkVersionLabel.setText(MessageUtils.getMessage("about.jdk.version", new Object[]{System.getProperty("java.version")}));
osVersionLabel.setText(MessageUtils.getMessage("about.os.version", new Object[]{System.getProperty("os.name") + " " + System.getProperty("os.version")}));
copyrightLabel.setText(MessageUtils.getMessage("about.copyright", new Object[]{java.time.Year.now().getValue()}));
// 更新按钮文本
closeButton.setText(MessageUtils.getMessage("button.close"));
}
}

View File

@ -0,0 +1,86 @@
package com.goeing.printserver.main.gui;
import com.goeing.printserver.main.utils.MessageUtils;
import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import java.awt.*;
/**
* GUI启动器负责在Spring Boot应用程序启动后初始化图形界面
*/
@Component
@Slf4j
public class GUILauncher {
private final PrintQueueGUI printQueueGUI;
private final PrintServerTray printServerTray;
private final PrintSettingsPanel settingsPanel;
@Autowired
public GUILauncher(PrintQueueGUI printQueueGUI, PrintServerTray printServerTray, PrintSettingsPanel settingsPanel) {
this.printQueueGUI = printQueueGUI;
this.printServerTray = printServerTray;
this.settingsPanel = settingsPanel;
}
/**
* 在应用程序准备就绪后启动GUI
*/
@EventListener(ApplicationReadyEvent.class)
public void launchGUI() {
log.info("应用程序已准备就绪,正在启动图形界面...");
// 检查是否支持图形界面
boolean isHeadless = GraphicsEnvironment.isHeadless();
if (isHeadless) {
log.warn("当前环境不支持图形界面,将以无头模式运行");
// 在无头模式下设置一个系统属性其他组件可以检查这个属性
System.setProperty("app.headless.mode", "true");
return;
}
try {
// 初始化GUI和系统托盘
printServerTray.initialize();
// 根据设置决定是否显示主窗口
if (!settingsPanel.isStartMinimized()) {
printQueueGUI.show();
} else {
log.info("根据用户设置,应用程序启动时最小化到系统托盘");
}
// 显示欢迎通知
printServerTray.displayMessage(
MessageUtils.getMessage("tray.notification.title"),
MessageUtils.getMessage("tray.notification.message"),
TrayIcon.MessageType.INFO
);
// 更新托盘提示
printServerTray.updateTooltip(MessageUtils.getMessage("app.name") + " - " + MessageUtils.getMessage("app.status.running"));
log.info("图形界面已启动");
} catch (Exception e) {
log.error("初始化图形界面时发生错误: {}", e.getMessage());
log.warn("将以无头模式运行");
System.setProperty("app.headless.mode", "true");
}
}
/**
* 在应用程序关闭时释放资源
*/
@PreDestroy
public void shutdown() {
log.info("正在关闭图形界面...");
printQueueGUI.shutdown();
printServerTray.shutdown();
log.info("图形界面已关闭");
}
}

View File

@ -0,0 +1,167 @@
package com.goeing.printserver.main.gui;
import com.goeing.printserver.main.domain.request.PrintRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.awt.*;
/**
* 打印通知服务用于在打印任务状态变化时发送系统通知
*/
@Service
@Slf4j
public class PrintNotificationService {
private PrintServerTray printServerTray;
private final PrintSettingsPanel settingsPanel;
private boolean headless = false;
@Autowired
public PrintNotificationService(PrintSettingsPanel settingsPanel) {
this.settingsPanel = settingsPanel;
// 检查是否在无头模式下运行
this.headless = GraphicsEnvironment.isHeadless() || Boolean.getBoolean("app.headless.mode");
if (headless) {
log.info("系统运行在无头模式下,通知功能将被禁用");
}
}
@Autowired
public void setPrintServerTray(PrintServerTray printServerTray) {
this.printServerTray = printServerTray;
}
/**
* 通知打印任务已添加到队列
*
* @param printRequest 打印请求
* @param queueSize 当前队列大小
*/
public void notifyTaskQueued(PrintRequest printRequest, int queueSize) {
if (headless || !settingsPanel.isEnableNotifications()) {
return; // 无头模式或通知被禁用时不发送通知
}
String fileName = extractFileName(printRequest.getFileUrl());
String message = String.format(
"文件 '%s' 已添加到打印队列\n打印机: %s\n队列位置: %d",
fileName,
printRequest.getPrinterName(),
queueSize
);
printServerTray.displayMessage("新打印任务", message, TrayIcon.MessageType.INFO);
updateTrayTooltip(queueSize);
}
/**
* 通知打印任务开始处理
*
* @param printRequest 打印请求
* @param queueSize 当前队列大小
*/
public void notifyTaskStarted(PrintRequest printRequest, int queueSize) {
if (headless || !settingsPanel.isEnableNotifications()) {
return; // 无头模式或通知被禁用时不发送通知
}
String fileName = extractFileName(printRequest.getFileUrl());
String message = String.format(
"开始打印文件 '%s'\n打印机: %s",
fileName,
printRequest.getPrinterName()
);
printServerTray.displayMessage("打印开始", message, TrayIcon.MessageType.INFO);
updateTrayTooltip(queueSize);
}
/**
* 通知打印任务完成
*
* @param printRequest 打印请求
* @param queueSize 当前队列大小
*/
public void notifyTaskCompleted(PrintRequest printRequest, int queueSize) {
if (headless || !settingsPanel.isEnableNotifications()) {
return; // 无头模式或通知被禁用时不发送通知
}
String fileName = extractFileName(printRequest.getFileUrl());
String message = String.format(
"文件 '%s' 打印完成\n打印机: %s",
fileName,
printRequest.getPrinterName()
);
printServerTray.displayMessage("打印完成", message, TrayIcon.MessageType.INFO);
updateTrayTooltip(queueSize);
}
/**
* 通知打印任务失败
*
* @param printRequest 打印请求
* @param errorMessage 错误消息
* @param queueSize 当前队列大小
*/
public void notifyTaskFailed(PrintRequest printRequest, String errorMessage, int queueSize) {
if (headless || !settingsPanel.isEnableNotifications()) {
return; // 无头模式或通知被禁用时不发送通知
}
String fileName = extractFileName(printRequest.getFileUrl());
String message = String.format(
"文件 '%s' 打印失败\n打印机: %s\n错误: %s",
fileName,
printRequest.getPrinterName(),
errorMessage
);
printServerTray.displayMessage("打印失败", message, TrayIcon.MessageType.ERROR);
updateTrayTooltip(queueSize);
}
/**
* 从URL中提取文件名
*
* @param fileUrl 文件URL
* @return 文件名
*/
private String extractFileName(String fileUrl) {
if (fileUrl == null || fileUrl.isEmpty()) {
return "未知文件";
}
// 尝试从URL中提取文件名
int lastSlashIndex = fileUrl.lastIndexOf('/');
if (lastSlashIndex >= 0 && lastSlashIndex < fileUrl.length() - 1) {
String fileName = fileUrl.substring(lastSlashIndex + 1);
// 移除查询参数
int queryIndex = fileName.indexOf('?');
if (queryIndex > 0) {
fileName = fileName.substring(0, queryIndex);
}
return fileName;
}
return fileUrl; // 如果无法提取则返回完整URL
}
/**
* 更新托盘提示
*
* @param queueSize 当前队列大小
*/
private void updateTrayTooltip(int queueSize) {
String tooltip;
if (queueSize > 0) {
tooltip = String.format("打印服务器 - 运行中 (队列中有 %d 个任务)", queueSize);
} else {
tooltip = "打印服务器 - 运行中 (队列为空)";
}
printServerTray.updateTooltip(tooltip);
}
}

View File

@ -0,0 +1,503 @@
package com.goeing.printserver.main.gui;
import com.goeing.printserver.main.service.PrintQueueService;
import com.goeing.printserver.main.utils.LocaleChangeListener;
import com.goeing.printserver.main.utils.LocaleManager;
import com.goeing.printserver.main.utils.MessageUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import jakarta.annotation.PreDestroy;
import javax.swing.*;
import javax.swing.table.DefaultTableModel;
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* 打印队列图形界面
*/
@Component
public class PrintQueueGUI implements LocaleChangeListener {
private final PrintQueueService printQueueService;
private PrinterStatusPanel printerStatusPanel;
private final PrintStatisticsPanel statisticsPanel;
private final PrintTaskSearchPanel searchPanel;
private final PrintSettingsPanel settingsPanel;
private final WebSocketStatusPanel webSocketStatusPanel;
private final SystemLogPanel systemLogPanel;
private JFrame frame;
private JTable currentTaskTable;
private JTable queuedTasksTable;
private DefaultTableModel currentTaskModel;
private DefaultTableModel queuedTasksModel;
private JLabel statusLabel;
private final ScheduledExecutorService refreshExecutor = Executors.newSingleThreadScheduledExecutor();
private final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Autowired
public PrintQueueGUI(PrintQueueService printQueueService,
PrintStatisticsPanel statisticsPanel, PrintTaskSearchPanel searchPanel,
PrintSettingsPanel settingsPanel, WebSocketStatusPanel webSocketStatusPanel,
SystemLogPanel systemLogPanel) {
this.printQueueService = printQueueService;
this.statisticsPanel = statisticsPanel;
this.searchPanel = searchPanel;
this.settingsPanel = settingsPanel;
this.webSocketStatusPanel = webSocketStatusPanel;
this.systemLogPanel = systemLogPanel;
// 注册语言变更监听器
LocaleManager.getInstance().addLocaleChangeListener(this);
// 只有在非无头模式下才初始化GUI
if (!GraphicsEnvironment.isHeadless() && !Boolean.getBoolean("app.headless.mode")) {
SwingUtilities.invokeLater(this::initializeGUI);
}
}
@Autowired
public void setPrinterStatusPanel(PrinterStatusPanel printerStatusPanel) {
this.printerStatusPanel = printerStatusPanel;
}
/**
* 初始化图形界面
*/
private void initializeGUI() {
// 创建主窗口
frame = new JFrame(MessageUtils.getMessage("main.title"));
frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
frame.setSize(800, 600);
frame.setLayout(new BorderLayout());
// 添加窗口关闭监听器
frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
// 只隐藏窗口不关闭应用程序
frame.setVisible(false);
}
});
// 创建菜单栏
JMenuBar menuBar = createMenuBar();
frame.setJMenuBar(menuBar);
// 创建顶部面板
JPanel topPanel = new JPanel(new BorderLayout());
statusLabel = new JLabel(MessageUtils.getMessage("queue.status.idle"));
statusLabel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
topPanel.add(statusLabel, BorderLayout.WEST);
// 创建刷新按钮
JButton refreshButton = new JButton(MessageUtils.getMessage("button.refresh"));
refreshButton.addActionListener(e -> refreshData());
topPanel.add(refreshButton, BorderLayout.EAST);
frame.add(topPanel, BorderLayout.NORTH);
// 创建选项卡面板
JTabbedPane tabbedPane = new JTabbedPane();
// 创建当前任务面板
JPanel currentTaskPanel = createCurrentTaskPanel();
tabbedPane.addTab(MessageUtils.getMessage("tab.current.task"), currentTaskPanel);
// 创建队列任务面板
JPanel queuedTasksPanel = createQueuedTasksPanel();
tabbedPane.addTab(MessageUtils.getMessage("tab.queued.tasks"), queuedTasksPanel);
// 添加打印机状态面板
tabbedPane.addTab(MessageUtils.getMessage("tab.printer.status"), printerStatusPanel);
// 添加统计面板
tabbedPane.addTab(MessageUtils.getMessage("tab.statistics"), statisticsPanel);
// 添加任务搜索面板
tabbedPane.addTab(MessageUtils.getMessage("tab.task.search"), searchPanel);
// 添加设置面板
tabbedPane.addTab(MessageUtils.getMessage("tab.settings"), settingsPanel);
// 添加WebSocket状态面板
tabbedPane.addTab(MessageUtils.getMessage("tab.websocket.status"), webSocketStatusPanel);
// 添加系统日志面板
tabbedPane.addTab(MessageUtils.getMessage("tab.system.log"), systemLogPanel);
frame.add(tabbedPane, BorderLayout.CENTER);
// 启动定时刷新
startRefreshTimer();
// 显示窗口
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
/**
* 创建当前任务面板
*/
private JPanel createCurrentTaskPanel() {
JPanel panel = new JPanel(new BorderLayout());
panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
// 创建表格模型
String[] columnNames = {
MessageUtils.getMessage("table.header.file.url"),
MessageUtils.getMessage("table.header.printer"),
MessageUtils.getMessage("table.header.status"),
MessageUtils.getMessage("table.header.queued.time"),
MessageUtils.getMessage("table.header.start.time"),
MessageUtils.getMessage("table.header.end.time")
};
currentTaskModel = new DefaultTableModel(columnNames, 0) {
@Override
public boolean isCellEditable(int row, int column) {
return false; // 禁止编辑单元格
}
};
// 创建表格
currentTaskTable = new JTable(currentTaskModel);
currentTaskTable.getTableHeader().setReorderingAllowed(false);
currentTaskTable.setFillsViewportHeight(true);
// 添加双击事件监听器
currentTaskTable.addMouseListener(new java.awt.event.MouseAdapter() {
@Override
public void mouseClicked(java.awt.event.MouseEvent e) {
if (e.getClickCount() == 2) {
showTaskDetails(currentTaskTable);
}
}
});
// 添加滚动面板
JScrollPane scrollPane = new JScrollPane(currentTaskTable);
panel.add(scrollPane, BorderLayout.CENTER);
return panel;
}
/**
* 创建队列任务面板
*/
private JPanel createQueuedTasksPanel() {
JPanel panel = new JPanel(new BorderLayout());
panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
// 创建表格模型
String[] columnNames = {
MessageUtils.getMessage("table.header.file.url"),
MessageUtils.getMessage("table.header.printer"),
MessageUtils.getMessage("table.header.status"),
MessageUtils.getMessage("table.header.queued.time")
};
queuedTasksModel = new DefaultTableModel(columnNames, 0) {
@Override
public boolean isCellEditable(int row, int column) {
return false; // 禁止编辑单元格
}
};
// 创建表格
queuedTasksTable = new JTable(queuedTasksModel);
queuedTasksTable.getTableHeader().setReorderingAllowed(false);
queuedTasksTable.setFillsViewportHeight(true);
// 添加双击事件监听器
queuedTasksTable.addMouseListener(new java.awt.event.MouseAdapter() {
@Override
public void mouseClicked(java.awt.event.MouseEvent e) {
if (e.getClickCount() == 2) {
showTaskDetails(queuedTasksTable);
}
}
});
// 添加滚动面板
JScrollPane scrollPane = new JScrollPane(queuedTasksTable);
panel.add(scrollPane, BorderLayout.CENTER);
return panel;
}
/**
* 启动定时刷新
*/
private void startRefreshTimer() {
refreshExecutor.scheduleAtFixedRate(this::refreshData, 0, 2, TimeUnit.SECONDS);
}
/**
* 刷新数据
*/
private void refreshData() {
SwingUtilities.invokeLater(() -> {
try {
// 更新队列状态
int queueSize = printQueueService.getQueueSize();
Map<String, Object> currentTask = printQueueService.getCurrentTaskInfo();
if (currentTask != null) {
statusLabel.setText(MessageUtils.getMessage("queue.status.processing", new Object[]{queueSize}));
} else if (queueSize > 0) {
statusLabel.setText(MessageUtils.getMessage("queue.status.waiting", new Object[]{queueSize}));
} else {
statusLabel.setText(MessageUtils.getMessage("queue.status.idle"));
}
// 更新当前任务表格
updateCurrentTaskTable(currentTask);
// 更新队列任务表格
List<Map<String, Object>> queuedTasks = printQueueService.getQueuedTasksInfo();
updateQueuedTasksTable(queuedTasks);
} catch (Exception e) {
e.printStackTrace();
statusLabel.setText(MessageUtils.getMessage("error.refresh.data", new Object[]{e.getMessage()}));
}
});
}
/**
* 更新当前任务表格
*/
private void updateCurrentTaskTable(Map<String, Object> currentTask) {
// 清空表格
currentTaskModel.setRowCount(0);
if (currentTask != null) {
Object[] rowData = new Object[6];
rowData[0] = currentTask.get("fileUrl");
rowData[1] = currentTask.get("printerName");
rowData[2] = currentTask.get("status");
rowData[3] = formatDateTime(currentTask.get("queuedTime"));
rowData[4] = formatDateTime(currentTask.get("startTime"));
rowData[5] = formatDateTime(currentTask.get("endTime"));
currentTaskModel.addRow(rowData);
}
}
/**
* 更新队列任务表格
*/
private void updateQueuedTasksTable(List<Map<String, Object>> queuedTasks) {
// 清空表格
queuedTasksModel.setRowCount(0);
if (queuedTasks != null && !queuedTasks.isEmpty()) {
for (Map<String, Object> task : queuedTasks) {
Object[] rowData = new Object[4];
rowData[0] = task.get("fileUrl");
rowData[1] = task.get("printerName");
rowData[2] = task.get("status");
rowData[3] = formatDateTime(task.get("queuedTime"));
queuedTasksModel.addRow(rowData);
}
}
}
/**
* 格式化日期时间
*/
private String formatDateTime(Object dateTimeObj) {
if (dateTimeObj == null) {
return "";
}
if (dateTimeObj instanceof LocalDateTime) {
return ((LocalDateTime) dateTimeObj).format(dateFormatter);
}
return dateTimeObj.toString();
}
/**
* 显示窗口
*/
public void show() {
if (frame != null) {
frame.setVisible(true);
frame.toFront();
}
}
/**
* 创建菜单栏
*/
private JMenuBar createMenuBar() {
JMenuBar menuBar = new JMenuBar();
// 文件菜单
JMenu fileMenu = new JMenu(MessageUtils.getMessage("menu.file"));
JMenuItem refreshItem = new JMenuItem(MessageUtils.getMessage("menu.file.refresh"));
JMenuItem exitItem = new JMenuItem(MessageUtils.getMessage("menu.file.exit"));
refreshItem.addActionListener(e -> refreshData());
exitItem.addActionListener(e -> System.exit(0));
fileMenu.add(refreshItem);
fileMenu.addSeparator();
fileMenu.add(exitItem);
// 视图菜单
JMenu viewMenu = new JMenu(MessageUtils.getMessage("menu.view"));
JMenuItem alwaysOnTopItem = new JCheckBoxMenuItem(MessageUtils.getMessage("menu.view.always.on.top"));
alwaysOnTopItem.addActionListener(e -> {
boolean selected = ((JCheckBoxMenuItem) e.getSource()).isSelected();
frame.setAlwaysOnTop(selected);
});
viewMenu.add(alwaysOnTopItem);
// 语言菜单
JMenu languageMenu = new JMenu(MessageUtils.getMessage("menu.language"));
// 添加语言选项
ButtonGroup languageGroup = new ButtonGroup();
for (Locale locale : LocaleManager.getInstance().getSupportedLocales()) {
JRadioButtonMenuItem languageItem = new JRadioButtonMenuItem(locale.getDisplayName());
languageItem.setSelected(locale.equals(LocaleManager.getInstance().getCurrentLocale()));
languageItem.addActionListener(e -> LocaleManager.getInstance().setCurrentLocale(locale));
languageGroup.add(languageItem);
languageMenu.add(languageItem);
}
// 帮助菜单
JMenu helpMenu = new JMenu(MessageUtils.getMessage("menu.help"));
JMenuItem aboutItem = new JMenuItem(MessageUtils.getMessage("menu.help.about"));
aboutItem.addActionListener(e -> AboutDialog.showDialog(frame));
helpMenu.add(aboutItem);
// 添加菜单到菜单栏
menuBar.add(fileMenu);
menuBar.add(viewMenu);
menuBar.add(languageMenu);
menuBar.add(helpMenu);
return menuBar;
}
/**
* 显示任务详情
*
* @param table 表格
*/
private void showTaskDetails(JTable table) {
int selectedRow = table.getSelectedRow();
if (selectedRow >= 0) {
Map<String, Object> taskInfo = null;
if (table == currentTaskTable) {
// 当前任务表格
taskInfo = printQueueService.getCurrentTaskInfo();
} else if (table == queuedTasksTable) {
// 队列任务表格
List<Map<String, Object>> queuedTasks = printQueueService.getQueuedTasksInfo();
if (selectedRow < queuedTasks.size()) {
taskInfo = queuedTasks.get(selectedRow);
}
}
if (taskInfo != null) {
PrintTaskDetailDialog dialog = new PrintTaskDetailDialog(frame, taskInfo);
dialog.setVisible(true);
}
}
}
/**
* 关闭窗口和资源
*/
@PreDestroy
public void shutdown() {
refreshExecutor.shutdownNow();
printerStatusPanel.shutdown();
statisticsPanel.shutdown();
searchPanel.shutdown();
settingsPanel.shutdown();
LocaleManager.getInstance().removeLocaleChangeListener(this);
if (frame != null) {
frame.dispose();
}
}
/**
* 当语言变更时更新界面
*/
@Override
public void onLocaleChanged(Locale newLocale) {
if (frame != null) {
SwingUtilities.invokeLater(() -> {
// 更新窗口标题
frame.setTitle(MessageUtils.getMessage("main.title"));
// 更新状态标签
refreshData();
// 更新菜单栏
frame.setJMenuBar(createMenuBar());
// 更新选项卡标题
JTabbedPane tabbedPane = (JTabbedPane) frame.getContentPane().getComponent(1);
tabbedPane.setTitleAt(0, MessageUtils.getMessage("tab.current.task"));
tabbedPane.setTitleAt(1, MessageUtils.getMessage("tab.queued.tasks"));
tabbedPane.setTitleAt(2, MessageUtils.getMessage("tab.printer.status"));
tabbedPane.setTitleAt(3, MessageUtils.getMessage("tab.statistics"));
tabbedPane.setTitleAt(4, MessageUtils.getMessage("tab.task.search"));
tabbedPane.setTitleAt(5, MessageUtils.getMessage("tab.settings"));
// 更新表格列名
updateTableHeaders();
});
}
}
/**
* 更新表格列名
*/
private void updateTableHeaders() {
// 更新当前任务表格列名
String[] currentTaskColumnNames = {
MessageUtils.getMessage("table.header.file.url"),
MessageUtils.getMessage("table.header.printer"),
MessageUtils.getMessage("table.header.status"),
MessageUtils.getMessage("table.header.queued.time"),
MessageUtils.getMessage("table.header.start.time"),
MessageUtils.getMessage("table.header.end.time")
};
for (int i = 0; i < currentTaskColumnNames.length; i++) {
currentTaskTable.getColumnModel().getColumn(i).setHeaderValue(currentTaskColumnNames[i]);
}
// 更新队列任务表格列名
String[] queuedTasksColumnNames = {
MessageUtils.getMessage("table.header.file.url"),
MessageUtils.getMessage("table.header.printer"),
MessageUtils.getMessage("table.header.status"),
MessageUtils.getMessage("table.header.queued.time")
};
for (int i = 0; i < queuedTasksColumnNames.length; i++) {
queuedTasksTable.getColumnModel().getColumn(i).setHeaderValue(queuedTasksColumnNames[i]);
}
// 刷新表格头
currentTaskTable.getTableHeader().repaint();
queuedTasksTable.getTableHeader().repaint();
}
}

View File

@ -0,0 +1,189 @@
package com.goeing.printserver.main.gui;
import com.goeing.printserver.main.utils.LocaleChangeListener;
import com.goeing.printserver.main.utils.LocaleManager;
import com.goeing.printserver.main.utils.MessageUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.util.Locale;
/**
* 打印服务器系统托盘
*/
@Component
@Slf4j
public class PrintServerTray implements LocaleChangeListener {
private PrintQueueGUI printQueueGUI;
private TrayIcon trayIcon;
private SystemTray systemTray;
private boolean traySupported;
@Autowired
public PrintServerTray() {
// 移除构造函数中的PrintQueueGUI依赖改为在initialize方法中注入
// 注册语言变更监听器
LocaleManager.getInstance().addLocaleChangeListener(this);
}
@Autowired
public void setPrintQueueGUI(PrintQueueGUI printQueueGUI) {
this.printQueueGUI = printQueueGUI;
}
/**
* 初始化系统托盘
*/
public void initialize() {
// 在无头模式下不初始化系统托盘
if (GraphicsEnvironment.isHeadless() || Boolean.getBoolean("app.headless.mode")) {
log.info("在无头模式下运行,跳过系统托盘初始化");
traySupported = false;
return;
}
initializeTray();
}
/**
* 初始化系统托盘
*/
private void initializeTray() {
// 检查系统是否支持系统托盘
if (!SystemTray.isSupported()) {
log.warn("系统不支持系统托盘功能");
traySupported = false;
return;
}
traySupported = true;
systemTray = SystemTray.getSystemTray();
// 创建托盘图标
Image trayImage = createTrayImage();
trayIcon = new TrayIcon(trayImage, MessageUtils.getMessage("app.name"));
trayIcon.setImageAutoSize(true);
// 创建弹出菜单
PopupMenu popupMenu = createPopupMenu();
trayIcon.setPopupMenu(popupMenu);
// 添加鼠标点击事件
trayIcon.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (e.getButton() == MouseEvent.BUTTON1) {
// 左键点击显示主窗口
printQueueGUI.show();
}
}
});
// 添加托盘图标
try {
systemTray.add(trayIcon);
log.info("系统托盘图标已添加");
} catch (AWTException e) {
log.error("添加系统托盘图标失败", e);
traySupported = false;
}
}
/**
* 创建托盘图标图像
*/
private Image createTrayImage() {
// 创建一个简单的打印机图标
BufferedImage image = new BufferedImage(16, 16, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = image.createGraphics();
// 设置抗锯齿
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// 绘制打印机图标
g2d.setColor(Color.BLACK);
g2d.fillRect(2, 10, 12, 5); // 打印机底座
g2d.fillRect(3, 3, 10, 7); // 打印机主体
g2d.setColor(Color.WHITE);
g2d.fillRect(4, 4, 8, 5); // 打印机内部
g2d.setColor(Color.BLACK);
g2d.fillRect(5, 6, 6, 1); // 打印纸
g2d.dispose();
return image;
}
/**
* 创建弹出菜单
*/
private PopupMenu createPopupMenu() {
PopupMenu popupMenu = new PopupMenu();
// 添加菜单项
MenuItem openItem = new MenuItem(MessageUtils.getMessage("tray.open"));
MenuItem exitItem = new MenuItem(MessageUtils.getMessage("tray.exit"));
// 设置事件监听器
openItem.addActionListener(e -> printQueueGUI.show());
exitItem.addActionListener(e -> System.exit(0));
// 添加菜单项到弹出菜单
popupMenu.add(openItem);
popupMenu.addSeparator();
popupMenu.add(exitItem);
return popupMenu;
}
/**
* 显示通知消息
*/
public void displayMessage(String caption, String text, TrayIcon.MessageType messageType) {
if (traySupported && trayIcon != null) {
trayIcon.displayMessage(caption, text, messageType);
}
}
/**
* 更新托盘图标提示文本
*/
public void updateTooltip(String tooltip) {
if (traySupported && trayIcon != null) {
trayIcon.setToolTip(tooltip);
}
}
/**
* 关闭系统托盘
*/
public void shutdown() {
if (traySupported && systemTray != null && trayIcon != null) {
systemTray.remove(trayIcon);
log.info("系统托盘图标已移除");
}
// 移除语言变更监听器
LocaleManager.getInstance().removeLocaleChangeListener(this);
}
/**
* 当语言变更时更新系统托盘
*/
@Override
public void onLocaleChanged(Locale newLocale) {
if (traySupported && trayIcon != null) {
// 更新托盘图标提示文本
trayIcon.setToolTip(MessageUtils.getMessage("app.name"));
// 更新弹出菜单
PopupMenu popupMenu = createPopupMenu();
trayIcon.setPopupMenu(popupMenu);
}
}
}

View File

@ -0,0 +1,439 @@
package com.goeing.printserver.main.gui;
import com.goeing.printserver.main.PrintController;
import com.goeing.printserver.main.config.PrintServerConfig;
import com.goeing.printserver.main.sse.PrinterClient;
import com.goeing.printserver.main.utils.LocaleChangeListener;
import com.goeing.printserver.main.utils.LocaleManager;
import com.goeing.printserver.main.utils.MessageUtils;
import com.goeing.printserver.main.service.PrintQueueService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Locale;
import com.goeing.printserver.main.gui.PrinterStatusPanel.PrinterListUpdatedEvent;
import javax.swing.*;
import java.awt.*;
/**
* 打印设置面板
*/
@Component
@Slf4j
public class PrintSettingsPanel extends JPanel implements LocaleChangeListener {
private final PrintQueueService printQueueService;
private final PrintServerConfig config;
private final PrintController printController;
private final PrinterClient printerClient;
// UI组件
private JComboBox<String> defaultPrinterComboBox;
private JSpinner maxQueueSizeSpinner;
private JCheckBox enableNotificationsCheckBox;
private JCheckBox startMinimizedCheckBox;
private JCheckBox autoStartCheckBox;
private JTextField websocketUrlField;
private JTextField printerIdField;
private JTextField apiKeyField;
// 标签和按钮
private JLabel titleLabel;
private JLabel defaultPrinterLabel;
private JLabel maxQueueSizeLabel;
private JLabel websocketUrlLabel;
private JLabel printerIdLabel;
private JLabel apiKeyLabel;
private JButton saveButton;
private JButton resetButton;
@Autowired
public PrintSettingsPanel(PrintQueueService printQueueService, PrintServerConfig config, PrintController printController, PrinterClient printerClient) {
this.printQueueService = printQueueService;
this.config = config;
this.printController = printController;
this.printerClient = printerClient;
initializeUI();
loadSettings();
// 注册语言变更监听器
LocaleManager.getInstance().addLocaleChangeListener(this);
}
/**
* 初始化用户界面
*/
private void initializeUI() {
setLayout(new BorderLayout());
setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
// 创建标题
String message = MessageUtils.getMessage("settings.title");
titleLabel = new JLabel(message);
titleLabel.setFont(new Font(titleLabel.getFont().getName(), Font.BOLD, 16));
titleLabel.setBorder(BorderFactory.createEmptyBorder(0, 0, 10, 0));
add(titleLabel, BorderLayout.NORTH);
// 创建设置面板
JPanel settingsPanel = new JPanel(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(5, 5, 5, 5);
gbc.anchor = GridBagConstraints.WEST;
gbc.fill = GridBagConstraints.HORIZONTAL;
// 默认打印机设置
gbc.gridx = 0;
gbc.gridy = 0;
gbc.gridwidth = 1;
defaultPrinterLabel = new JLabel(MessageUtils.getMessage("settings.defaultPrinter"));
settingsPanel.add(defaultPrinterLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
defaultPrinterComboBox = new JComboBox<>();
updatePrinterList(); // 初始化打印机列表
settingsPanel.add(defaultPrinterComboBox, gbc);
// 最大队列大小设置
gbc.gridx = 0;
gbc.gridy = 1;
gbc.weightx = 0.0;
maxQueueSizeLabel = new JLabel(MessageUtils.getMessage("settings.maxQueueSize"));
settingsPanel.add(maxQueueSizeLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
SpinnerNumberModel spinnerModel = new SpinnerNumberModel(10, 1, 100, 1);
maxQueueSizeSpinner = new JSpinner(spinnerModel);
settingsPanel.add(maxQueueSizeSpinner, gbc);
// 启用通知设置
gbc.gridx = 0;
gbc.gridy = 2;
gbc.gridwidth = 2;
enableNotificationsCheckBox = new JCheckBox(MessageUtils.getMessage("settings.notifications"));
settingsPanel.add(enableNotificationsCheckBox, gbc);
// 启动时最小化设置
gbc.gridx = 0;
gbc.gridy = 3;
startMinimizedCheckBox = new JCheckBox(MessageUtils.getMessage("settings.startMinimized"));
settingsPanel.add(startMinimizedCheckBox, gbc);
// 开机自启动设置
gbc.gridx = 0;
gbc.gridy = 4;
autoStartCheckBox = new JCheckBox(MessageUtils.getMessage("settings.autoStart"));
settingsPanel.add(autoStartCheckBox, gbc);
// WebSocket URL设置
gbc.gridx = 0;
gbc.gridy = 5;
gbc.gridwidth = 1;
gbc.weightx = 0.0;
websocketUrlLabel = new JLabel(MessageUtils.getMessage("settings.websocketUrl"));
settingsPanel.add(websocketUrlLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
websocketUrlField = new JTextField();
settingsPanel.add(websocketUrlField, gbc);
// 打印机ID设置
gbc.gridx = 0;
gbc.gridy = 6;
gbc.weightx = 0.0;
printerIdLabel = new JLabel(MessageUtils.getMessage("settings.printerId"));
settingsPanel.add(printerIdLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
printerIdField = new JTextField();
settingsPanel.add(printerIdField, gbc);
// API Key设置
gbc.gridx = 0;
gbc.gridy = 7;
gbc.weightx = 0.0;
apiKeyLabel = new JLabel(MessageUtils.getMessage("settings.apiKey"));
settingsPanel.add(apiKeyLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
apiKeyField = new JTextField();
settingsPanel.add(apiKeyField, gbc);
// 添加一个弹性空间
gbc.gridx = 0;
gbc.gridy = 8;
gbc.gridwidth = 2;
gbc.weighty = 1.0;
settingsPanel.add(Box.createVerticalGlue(), gbc);
// 添加设置面板到滚动面板
JScrollPane scrollPane = new JScrollPane(settingsPanel);
scrollPane.setBorder(null);
add(scrollPane, BorderLayout.CENTER);
// 创建按钮面板
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
saveButton = new JButton(MessageUtils.getMessage("settings.save"));
saveButton.addActionListener(e -> saveSettings());
buttonPanel.add(saveButton);
resetButton = new JButton(MessageUtils.getMessage("settings.reset"));
resetButton.addActionListener(e -> resetSettings());
buttonPanel.add(resetButton);
add(buttonPanel, BorderLayout.SOUTH);
}
/**
* 加载设置
*/
private void loadSettings() {
// 从配置对象加载设置
config.loadConfig();
// 更新UI组件
defaultPrinterComboBox.setSelectedItem(config.getDefaultPrinter());
maxQueueSizeSpinner.setValue(config.getMaxQueueSize());
enableNotificationsCheckBox.setSelected(config.isEnableNotifications());
startMinimizedCheckBox.setSelected(config.isStartMinimized());
autoStartCheckBox.setSelected(config.isAutoStart());
websocketUrlField.setText(config.getWebsocketUrl());
printerIdField.setText(config.getPrinterId());
apiKeyField.setText(config.getApiKey());
}
/**
* 保存设置
*/
private void saveSettings() {
try {
// 从UI组件获取设置值
String defaultPrinter = defaultPrinterComboBox.getSelectedItem().toString();
int maxQueueSize = (Integer) maxQueueSizeSpinner.getValue();
boolean enableNotifications = enableNotificationsCheckBox.isSelected();
boolean startMinimized = startMinimizedCheckBox.isSelected();
boolean autoStart = autoStartCheckBox.isSelected();
String websocketUrl = websocketUrlField.getText().trim();
String printerId = printerIdField.getText().trim();
String apiKey = apiKeyField.getText().trim();
// 检查WebSocket相关配置是否发生变化
boolean websocketConfigChanged = !websocketUrl.equals(config.getWebsocketUrl()) ||
!printerId.equals(config.getPrinterId()) ||
!apiKey.equals(config.getApiKey());
// 更新配置对象
config.setDefaultPrinter(defaultPrinter);
config.setMaxQueueSize(maxQueueSize);
config.setEnableNotifications(enableNotifications);
config.setStartMinimized(startMinimized);
config.setAutoStart(autoStart);
config.setWebsocketUrl(websocketUrl);
config.setPrinterId(printerId);
config.setApiKey(apiKey);
// 保存配置
config.saveConfig();
// 应用设置到服务
applySettings();
// 如果WebSocket配置发生变化重新连接WebSocket
if (websocketConfigChanged) {
printerClient.reconnect();
String message = MessageUtils.getMessage("settings.saved") + "\n\n" +
MessageUtils.getMessage("settings.websocket.reconnected");
JOptionPane.showMessageDialog(this, message, MessageUtils.getMessage("dialog.success"), JOptionPane.INFORMATION_MESSAGE);
} else {
JOptionPane.showMessageDialog(this, MessageUtils.getMessage("settings.saved"), MessageUtils.getMessage("dialog.success"), JOptionPane.INFORMATION_MESSAGE);
}
} catch (Exception e) {
log.error(MessageUtils.getMessage("log.settings.save.error"), e);
JOptionPane.showMessageDialog(this, MessageUtils.getMessage("settings.save.error", new Object[]{e.getMessage()}), MessageUtils.getMessage("dialog.error"), JOptionPane.ERROR_MESSAGE);
}
}
/**
* 重置设置为默认值
*/
private void resetSettings() {
int option = JOptionPane.showConfirmDialog(this, MessageUtils.getMessage("settings.reset.confirm"), MessageUtils.getMessage("dialog.confirm"), JOptionPane.YES_NO_OPTION);
if (option == JOptionPane.YES_OPTION) {
try {
// 重置配置对象到默认值
config.resetToDefaults();
// 保存默认配置
config.saveConfig();
// 更新UI
loadSettings();
// 应用默认设置
applySettings();
JOptionPane.showMessageDialog(this, MessageUtils.getMessage("settings.reset.success"), MessageUtils.getMessage("dialog.success"), JOptionPane.INFORMATION_MESSAGE);
} catch (Exception e) {
log.error(MessageUtils.getMessage("log.settings.reset.error"), e);
JOptionPane.showMessageDialog(this, MessageUtils.getMessage("settings.reset.error", new Object[]{e.getMessage()}), MessageUtils.getMessage("dialog.error"), JOptionPane.ERROR_MESSAGE);
}
}
}
/**
* 应用设置到服务
*/
private void applySettings() {
// 在这里应用设置到相关服务
// 例如可以设置打印队列服务的最大队列大小等
printQueueService.setMaxQueueSize(config.getMaxQueueSize());
// 通知设置变更
log.info(MessageUtils.getMessage("log.settings.applied"),
config.getDefaultPrinter(),
config.getMaxQueueSize(),
config.isEnableNotifications(),
config.isStartMinimized(),
config.isAutoStart());
}
/**
* 获取是否启动时最小化设置
*
* @return 是否启动时最小化
*/
public boolean isStartMinimized() {
return config.isStartMinimized();
}
/**
* 获取是否启用通知设置
*
* @return 是否启用通知
*/
public boolean isEnableNotifications() {
return config.isEnableNotifications();
}
/**
* 获取默认打印机设置
*
* @return 默认打印机名称
*/
public String getDefaultPrinter() {
return config.getDefaultPrinter();
}
/**
* 获取最大队列大小设置
*
* @return 最大队列大小
*/
public int getMaxQueueSize() {
return config.getMaxQueueSize();
}
/**
* 更新打印机列表
*/
public void updatePrinterList() {
try {
// 保存当前选中的打印机
String selectedPrinter = defaultPrinterComboBox.getSelectedItem() != null ?
defaultPrinterComboBox.getSelectedItem().toString() : null;
// 清空打印机列表
defaultPrinterComboBox.removeAllItems();
List<String> printers = printController.printerList();
updatePrinterComboBox(printers, selectedPrinter);
} catch (Exception e) {
log.error(MessageUtils.getMessage("log.printer.list.update.error"), e);
}
}
/**
* 使用提供的打印机列表更新下拉框
*
* @param printers 打印机列表
* @param selectedPrinter 当前选中的打印机
*/
private void updatePrinterComboBox(List<String> printers, String selectedPrinter) {
if (printers != null && !printers.isEmpty()) {
for (String printer : printers) {
defaultPrinterComboBox.addItem(printer);
}
// 尝试恢复之前选中的打印机
if (selectedPrinter != null && printers.contains(selectedPrinter)) {
defaultPrinterComboBox.setSelectedItem(selectedPrinter);
} else {
// 如果之前选中的打印机不存在则使用配置中的默认打印机
String defaultPrinter = config.getDefaultPrinter();
if (defaultPrinter != null && printers.contains(defaultPrinter)) {
defaultPrinterComboBox.setSelectedItem(defaultPrinter);
}
}
// 使用debug级别记录日志减少info日志输出
log.debug(MessageUtils.getMessage("log.printer.list.updated"), printers.size());
}
}
/**
* 监听打印机列表更新事件
*
* @param event 打印机列表更新事件
*/
@EventListener
public void onPrinterListUpdated(PrinterListUpdatedEvent event) {
SwingUtilities.invokeLater(() -> {
// 保存当前选中的打印机
String selectedPrinter = defaultPrinterComboBox.getSelectedItem() != null ?
defaultPrinterComboBox.getSelectedItem().toString() : null;
// 清空并重新填充打印机列表
defaultPrinterComboBox.removeAllItems();
updatePrinterComboBox(event.getPrinters(), selectedPrinter);
// 使用debug级别记录日志减少info日志输出
log.debug(MessageUtils.getMessage("log.printer.list.event.received"));
});
}
/**
* 当语言变更时更新UI元素
*/
@Override
public void onLocaleChanged(Locale newLocale) {
// 更新标题和标签
titleLabel.setText(MessageUtils.getMessage("settings.title"));
defaultPrinterLabel.setText(MessageUtils.getMessage("settings.defaultPrinter"));
maxQueueSizeLabel.setText(MessageUtils.getMessage("settings.maxQueueSize"));
websocketUrlLabel.setText(MessageUtils.getMessage("settings.websocketUrl"));
printerIdLabel.setText(MessageUtils.getMessage("settings.printerId"));
apiKeyLabel.setText(MessageUtils.getMessage("settings.apiKey"));
// 更新复选框
enableNotificationsCheckBox.setText(MessageUtils.getMessage("settings.notifications"));
startMinimizedCheckBox.setText(MessageUtils.getMessage("settings.startMinimized"));
autoStartCheckBox.setText(MessageUtils.getMessage("settings.autoStart"));
// 更新按钮
saveButton.setText(MessageUtils.getMessage("settings.save"));
resetButton.setText(MessageUtils.getMessage("settings.reset"));
}
/**
* 关闭资源
*/
public void shutdown() {
// 移除语言变更监听器
LocaleManager.getInstance().removeLocaleChangeListener(this);
}
}

View File

@ -0,0 +1,251 @@
package com.goeing.printserver.main.gui;
import com.goeing.printserver.main.service.PrintQueueService;
import com.goeing.printserver.main.utils.LocaleChangeListener;
import com.goeing.printserver.main.utils.LocaleManager;
import com.goeing.printserver.main.utils.MessageUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.swing.*;
import java.awt.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 打印统计面板
*/
@Component
@Slf4j
public class PrintStatisticsPanel extends JPanel implements LocaleChangeListener {
private final PrintQueueService printQueueService;
private final ScheduledExecutorService refreshExecutor = Executors.newSingleThreadScheduledExecutor();
private final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
// 统计数据
private final AtomicInteger totalTasksCount = new AtomicInteger(0);
private final AtomicInteger completedTasksCount = new AtomicInteger(0);
private final AtomicInteger failedTasksCount = new AtomicInteger(0);
private LocalDateTime startTime = LocalDateTime.now();
// UI组件
private JLabel totalTasksLabel;
private JLabel completedTasksLabel;
private JLabel failedTasksLabel;
private JLabel queueSizeLabel;
private JLabel upTimeLabel;
private JLabel currentTimeLabel;
@Autowired
public PrintStatisticsPanel(PrintQueueService printQueueService) {
this.printQueueService = printQueueService;
initializeUI();
startRefreshTimer();
// 注册语言变更监听器
LocaleManager.getInstance().addLocaleChangeListener(this);
}
/**
* 初始化用户界面
*/
private void initializeUI() {
setLayout(new BorderLayout());
setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
// 创建顶部面板
JPanel topPanel = new JPanel(new BorderLayout());
JLabel titleLabel = new JLabel(MessageUtils.getMessage("stats.title"));
titleLabel.setFont(new Font(titleLabel.getFont().getName(), Font.BOLD, 14));
topPanel.add(titleLabel, BorderLayout.WEST);
// 创建刷新按钮
JButton refreshButton = new JButton(MessageUtils.getMessage("button.refresh"));
refreshButton.addActionListener(e -> refreshStatistics());
topPanel.add(refreshButton, BorderLayout.EAST);
add(topPanel, BorderLayout.NORTH);
// 创建统计信息面板
JPanel statsPanel = new JPanel();
statsPanel.setLayout(new BoxLayout(statsPanel, BoxLayout.Y_AXIS));
statsPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
// 添加统计信息
totalTasksLabel = createStatLabel(MessageUtils.getMessage("stats.totalTasks", new Object[]{0}));
completedTasksLabel = createStatLabel(MessageUtils.getMessage("stats.completedTasks", new Object[]{0}));
failedTasksLabel = createStatLabel(MessageUtils.getMessage("stats.failedTasks", new Object[]{0}));
queueSizeLabel = createStatLabel(MessageUtils.getMessage("stats.queueSize", new Object[]{0}));
upTimeLabel = createStatLabel(MessageUtils.getMessage("stats.uptime", new Object[]{0, 0}));
currentTimeLabel = createStatLabel(MessageUtils.getMessage("stats.currentTime", new Object[]{LocalDateTime.now().format(dateFormatter)}));
statsPanel.add(totalTasksLabel);
statsPanel.add(Box.createVerticalStrut(10));
statsPanel.add(completedTasksLabel);
statsPanel.add(Box.createVerticalStrut(10));
statsPanel.add(failedTasksLabel);
statsPanel.add(Box.createVerticalStrut(10));
statsPanel.add(queueSizeLabel);
statsPanel.add(Box.createVerticalStrut(10));
statsPanel.add(upTimeLabel);
statsPanel.add(Box.createVerticalStrut(10));
statsPanel.add(currentTimeLabel);
// 添加一个弹性空间使内容居上
statsPanel.add(Box.createVerticalGlue());
// 添加重置按钮
JButton resetButton = new JButton(MessageUtils.getMessage("stats.reset"));
resetButton.addActionListener(e -> resetStatistics());
resetButton.setAlignmentX(0.0f); // LEFT_ALIGNMENT = 0.0f
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
buttonPanel.add(resetButton);
buttonPanel.setAlignmentX(0.0f); // LEFT_ALIGNMENT = 0.0f
statsPanel.add(buttonPanel);
add(new JScrollPane(statsPanel), BorderLayout.CENTER);
}
/**
* 创建统计标签
*
* @param text 标签文本
* @return 标签组件
*/
private JLabel createStatLabel(String text) {
JLabel label = new JLabel(text);
label.setFont(new Font(label.getFont().getName(), Font.PLAIN, 14));
label.setAlignmentX(0.0f); // LEFT_ALIGNMENT = 0.0f
return label;
}
/**
* 启动定时刷新
*/
private void startRefreshTimer() {
refreshExecutor.scheduleAtFixedRate(this::refreshStatistics, 0, 1, TimeUnit.SECONDS);
}
/**
* 刷新统计信息
*/
private void refreshStatistics() {
SwingUtilities.invokeLater(() -> {
try {
// 更新队列大小
int queueSize = printQueueService.getQueueSize();
queueSizeLabel.setText(MessageUtils.getMessage("stats.queueSize", new Object[]{queueSize}));
// 更新运行时间
LocalDateTime now = LocalDateTime.now();
long hours = java.time.Duration.between(startTime, now).toHours();
long minutes = java.time.Duration.between(startTime, now).toMinutes() % 60;
upTimeLabel.setText(MessageUtils.getMessage("stats.uptime", new Object[]{hours, minutes}));
// 更新当前时间
currentTimeLabel.setText(MessageUtils.getMessage("stats.currentTime", new Object[]{now.format(dateFormatter)}));
} catch (Exception e) {
log.error(MessageUtils.getMessage("log.error.refresh.statistics"), e);
}
});
}
/**
* 重置统计信息
*/
private void resetStatistics() {
totalTasksCount.set(0);
completedTasksCount.set(0);
failedTasksCount.set(0);
startTime = LocalDateTime.now();
totalTasksLabel.setText(MessageUtils.getMessage("stats.totalTasks", new Object[]{0}));
completedTasksLabel.setText(MessageUtils.getMessage("stats.completedTasks", new Object[]{0}));
failedTasksLabel.setText(MessageUtils.getMessage("stats.failedTasks", new Object[]{0}));
upTimeLabel.setText(MessageUtils.getMessage("stats.uptime", new Object[]{0, 0}));
}
/**
* 增加总任务数
*/
public void incrementTotalTasks() {
int total = totalTasksCount.incrementAndGet();
SwingUtilities.invokeLater(() -> totalTasksLabel.setText(MessageUtils.getMessage("stats.totalTasks", new Object[]{total})));
}
/**
* 增加已完成任务数
*/
public void incrementCompletedTasks() {
int completed = completedTasksCount.incrementAndGet();
SwingUtilities.invokeLater(() -> completedTasksLabel.setText(MessageUtils.getMessage("stats.completedTasks", new Object[]{completed})));
}
/**
* 增加失败任务数
*/
public void incrementFailedTasks() {
int failed = failedTasksCount.incrementAndGet();
SwingUtilities.invokeLater(() -> failedTasksLabel.setText(MessageUtils.getMessage("stats.failedTasks", new Object[]{failed})));
}
/**
* 关闭资源
*/
public void shutdown() {
refreshExecutor.shutdownNow();
LocaleManager.getInstance().removeLocaleChangeListener(this);
}
/**
* 当语言变更时更新界面
*/
@Override
public void onLocaleChanged(Locale newLocale) {
SwingUtilities.invokeLater(() -> {
// 更新标题标签
java.awt.Component[] components = ((JPanel)getComponent(0)).getComponents();
for (java.awt.Component component : components) {
if (component instanceof JLabel) {
((JLabel) component).setText(MessageUtils.getMessage("stats.title"));
break;
}
}
// 更新刷新按钮
for (java.awt.Component component : components) {
if (component instanceof JButton) {
((JButton) component).setText(MessageUtils.getMessage("button.refresh"));
break;
}
}
// 更新重置按钮
JPanel statsPanel = (JPanel) ((JScrollPane) getComponent(1)).getViewport().getView();
JPanel buttonPanel = (JPanel) statsPanel.getComponent(statsPanel.getComponentCount() - 1);
JButton resetButton = (JButton) buttonPanel.getComponent(0);
resetButton.setText(MessageUtils.getMessage("stats.reset"));
// 刷新统计信息
refreshStatistics();
// 更新其他标签
int total = totalTasksCount.get();
int completed = completedTasksCount.get();
int failed = failedTasksCount.get();
totalTasksLabel.setText(MessageUtils.getMessage("stats.totalTasks", new Object[]{total}));
completedTasksLabel.setText(MessageUtils.getMessage("stats.completedTasks", new Object[]{completed}));
failedTasksLabel.setText(MessageUtils.getMessage("stats.failedTasks", new Object[]{failed}));
});
}
}

View File

@ -0,0 +1,253 @@
package com.goeing.printserver.main.gui;
import com.goeing.printserver.main.domain.PrintTask;
import com.goeing.printserver.main.utils.LocaleChangeListener;
import com.goeing.printserver.main.utils.LocaleManager;
import com.goeing.printserver.main.utils.MessageUtils;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
import java.awt.Dialog.ModalityType;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
import java.util.Map;
/**
* 打印任务详情对话框
*/
public class PrintTaskDetailDialog extends JDialog implements LocaleChangeListener {
private final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
/**
* 创建打印任务详情对话框
*
* @param parent 父窗口
* @param task 任务信息
*/
public PrintTaskDetailDialog(Frame parent, Map<String, Object> task) {
super(parent, MessageUtils.getMessage("dialog.task.detail.title"), true);
initializeUI(task);
LocaleManager.getInstance().addLocaleChangeListener(this);
}
/**
* 创建打印任务详情对话框
*
* @param parent 父窗口
* @param task 打印任务对象
*/
public PrintTaskDetailDialog(Frame parent, PrintTask task) {
super(parent, MessageUtils.getMessage("dialog.task.detail.title"), true);
initializeUI(task.toMap());
LocaleManager.getInstance().addLocaleChangeListener(this);
}
/**
* 创建打印任务详情对话框接受Window类型参数
*
* @param parent 父窗口
* @param task 打印任务对象
*/
public PrintTaskDetailDialog(Window parent, PrintTask task) {
super(parent, MessageUtils.getMessage("dialog.task.detail.title"), ModalityType.APPLICATION_MODAL);
initializeUI(task.toMap());
LocaleManager.getInstance().addLocaleChangeListener(this);
}
/**
* 创建打印任务详情对话框接受Window类型参数和Map类型任务信息
*
* @param parent 父窗口
* @param task 任务信息
*/
public PrintTaskDetailDialog(Window parent, Map<String, Object> task) {
super(parent, MessageUtils.getMessage("dialog.task.detail.title"), ModalityType.APPLICATION_MODAL);
initializeUI(task);
LocaleManager.getInstance().addLocaleChangeListener(this);
}
/**
* 初始化用户界面
*
* @param task 任务信息
*/
private void initializeUI(Map<String, Object> task) {
setSize(500, 400);
setLocationRelativeTo(getParent());
setLayout(new BorderLayout());
// 创建内容面板
JPanel contentPanel = new JPanel();
contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.Y_AXIS));
contentPanel.setBorder(new EmptyBorder(10, 10, 10, 10));
// 添加任务信息
addTaskInfo(contentPanel, task);
// 添加滚动面板
JScrollPane scrollPane = new JScrollPane(contentPanel);
scrollPane.setBorder(null);
add(scrollPane, BorderLayout.CENTER);
// 添加关闭按钮
JButton closeButton = new JButton(MessageUtils.getMessage("common.close"));
closeButton.addActionListener(e -> {
LocaleManager.getInstance().removeLocaleChangeListener(this);
dispose();
});
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
buttonPanel.add(closeButton);
add(buttonPanel, BorderLayout.SOUTH);
}
/**
* 添加任务信息
*
* @param panel 面板
* @param task 任务信息
*/
private void addTaskInfo(JPanel panel, Map<String, Object> task) {
// 添加标题
JLabel titleLabel = new JLabel(MessageUtils.getMessage("dialog.task.detail.title"));
titleLabel.setFont(new Font(titleLabel.getFont().getName(), Font.BOLD, 16));
titleLabel.setAlignmentX(0.0f); // LEFT_ALIGNMENT = 0.0f
panel.add(titleLabel);
panel.add(Box.createVerticalStrut(10));
// 添加基本信息
addInfoField(panel, MessageUtils.getMessage("dialog.task.detail.file.url"), getStringValue(task.get("fileUrl")));
addInfoField(panel, MessageUtils.getMessage("dialog.task.detail.printer"), getStringValue(task.get("printerName")));
addInfoField(panel, MessageUtils.getMessage("dialog.task.detail.status"), getStringValue(task.get("status")));
addInfoField(panel, MessageUtils.getMessage("dialog.task.detail.queued.time"), formatDateTime(task.get("queuedTime")));
// 添加开始和结束时间如果有
if (task.containsKey("startTime") && task.get("startTime") != null) {
addInfoField(panel, MessageUtils.getMessage("dialog.task.detail.start.time"), formatDateTime(task.get("startTime")));
}
if (task.containsKey("endTime") && task.get("endTime") != null) {
addInfoField(panel, MessageUtils.getMessage("dialog.task.detail.end.time"), formatDateTime(task.get("endTime")));
}
// 添加打印选项如果有
if (task.containsKey("printOption") && task.get("printOption") != null) {
panel.add(Box.createVerticalStrut(10));
JLabel optionsLabel = new JLabel(MessageUtils.getMessage("dialog.task.detail.print.options"));
optionsLabel.setFont(new Font(optionsLabel.getFont().getName(), Font.BOLD, 14));
optionsLabel.setAlignmentX(0.0f); // LEFT_ALIGNMENT = 0.0f
panel.add(optionsLabel);
panel.add(Box.createVerticalStrut(5));
Map<String, Object> printOption = (Map<String, Object>) task.get("printOption");
for (Map.Entry<String, Object> entry : printOption.entrySet()) {
addInfoField(panel, entry.getKey() + ":", getStringValue(entry.getValue()));
}
}
}
/**
* 添加信息字段
*
* @param panel 面板
* @param label 标签
* @param value
*/
private void addInfoField(JPanel panel, String label, String value) {
JPanel fieldPanel = new JPanel(new BorderLayout());
fieldPanel.setAlignmentX(0.0f); // LEFT_ALIGNMENT = 0.0f
fieldPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 25));
JLabel labelComponent = new JLabel(label);
labelComponent.setPreferredSize(new Dimension(100, 20));
fieldPanel.add(labelComponent, BorderLayout.WEST);
JTextField valueField = new JTextField(value);
valueField.setEditable(false);
valueField.setBorder(null);
valueField.setBackground(null);
fieldPanel.add(valueField, BorderLayout.CENTER);
panel.add(fieldPanel);
panel.add(Box.createVerticalStrut(5));
}
/**
* 获取字符串值
*
* @param value
* @return 字符串值
*/
private String getStringValue(Object value) {
return value != null ? value.toString() : "";
}
/**
* 格式化日期时间
*
* @param dateTimeObj 日期时间对象
* @return 格式化后的日期时间字符串
*/
private String formatDateTime(Object dateTimeObj) {
if (dateTimeObj == null) {
return "";
}
if (dateTimeObj instanceof LocalDateTime) {
return ((LocalDateTime) dateTimeObj).format(dateFormatter);
}
return dateTimeObj.toString();
}
/**
* 当语言变更时更新界面
*/
@Override
public void onLocaleChanged(Locale newLocale) {
SwingUtilities.invokeLater(() -> {
// 更新对话框标题
setTitle(MessageUtils.getMessage("dialog.task.detail.title"));
// 更新标题标签
Container contentPane = getContentPane();
JScrollPane scrollPane = (JScrollPane) contentPane.getComponent(0);
JPanel contentPanel = (JPanel) scrollPane.getViewport().getView();
JLabel titleLabel = (JLabel) contentPanel.getComponent(0);
titleLabel.setText(MessageUtils.getMessage("dialog.task.detail.title"));
// 更新字段标签
for (int i = 2; i < contentPanel.getComponentCount(); i++) {
Component comp = contentPanel.getComponent(i);
if (comp instanceof JPanel) {
JPanel fieldPanel = (JPanel) comp;
JLabel label = (JLabel) fieldPanel.getComponent(0);
String labelText = label.getText();
if (labelText.contains("文件URL") || labelText.contains("File URL")) {
label.setText(MessageUtils.getMessage("dialog.task.detail.file.url"));
} else if (labelText.contains("打印机") || labelText.contains("Printer")) {
label.setText(MessageUtils.getMessage("dialog.task.detail.printer"));
} else if (labelText.contains("状态") || labelText.contains("Status")) {
label.setText(MessageUtils.getMessage("dialog.task.detail.status"));
} else if (labelText.contains("队列时间") || labelText.contains("Queued Time")) {
label.setText(MessageUtils.getMessage("dialog.task.detail.queued.time"));
} else if (labelText.contains("开始时间") || labelText.contains("Start Time")) {
label.setText(MessageUtils.getMessage("dialog.task.detail.start.time"));
} else if (labelText.contains("结束时间") || labelText.contains("End Time")) {
label.setText(MessageUtils.getMessage("dialog.task.detail.end.time"));
} else if (labelText.contains("打印选项") || labelText.contains("Print Options")) {
label.setText(MessageUtils.getMessage("dialog.task.detail.print.options"));
}
}
}
// 更新关闭按钮
JPanel buttonPanel = (JPanel) contentPane.getComponent(1);
JButton closeButton = (JButton) buttonPanel.getComponent(0);
closeButton.setText(MessageUtils.getMessage("common.close"));
});
}
}

View File

@ -0,0 +1,404 @@
package com.goeing.printserver.main.gui;
import com.goeing.printserver.main.domain.PrintTask;
import com.goeing.printserver.main.service.PrintQueueService;
import com.goeing.printserver.main.service.PrintHistoryService;
import com.goeing.printserver.main.utils.LocaleChangeListener;
import com.goeing.printserver.main.utils.LocaleManager;
import com.goeing.printserver.main.utils.MessageUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.swing.*;
import javax.swing.table.DefaultTableModel;
import java.awt.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Locale;
/**
* 打印任务搜索面板
*/
@Component
@Slf4j
public class PrintTaskSearchPanel extends JPanel implements LocaleChangeListener {
private final PrintQueueService printQueueService;
private final PrintHistoryService historyService;
private final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private String allText;
// UI组件
private JComboBox<String> printerComboBox;
private JComboBox<String> statusComboBox;
private JTextField fileUrlField;
private JCheckBox includeHistoryCheckBox;
private JTable resultsTable;
private DefaultTableModel tableModel;
@Autowired
public PrintTaskSearchPanel(PrintQueueService printQueueService, PrintHistoryService historyService) {
this.printQueueService = printQueueService;
this.historyService = historyService;
this.allText = MessageUtils.getMessage("common.all");
initializeUI();
// 注册语言变更监听器
LocaleManager.getInstance().addLocaleChangeListener(this);
}
/**
* 初始化用户界面
*/
private void initializeUI() {
setLayout(new BorderLayout());
setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
// 创建搜索条件面板
JPanel searchPanel = new JPanel(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(5, 5, 5, 5);
gbc.fill = GridBagConstraints.HORIZONTAL;
// 打印机选择
gbc.gridx = 0;
gbc.gridy = 0;
searchPanel.add(new JLabel(MessageUtils.getMessage("search.printer")), gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
printerComboBox = new JComboBox<>(new String[]{MessageUtils.getMessage("common.all"), "打印机1", "打印机2", "打印机3"});
searchPanel.add(printerComboBox, gbc);
// 状态选择
gbc.gridx = 0;
gbc.gridy = 1;
gbc.weightx = 0.0;
searchPanel.add(new JLabel(MessageUtils.getMessage("search.status")), gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
statusComboBox = new JComboBox<>(new String[]{MessageUtils.getMessage("common.all"), "queued", "processing", "completed", "failed"});
searchPanel.add(statusComboBox, gbc);
// 文件URL
gbc.gridx = 0;
gbc.gridy = 2;
gbc.weightx = 0.0;
searchPanel.add(new JLabel(MessageUtils.getMessage("search.fileUrl")), gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
fileUrlField = new JTextField();
searchPanel.add(fileUrlField, gbc);
// 包含历史记录选项
gbc.gridx = 0;
gbc.gridy = 3;
gbc.gridwidth = 2;
includeHistoryCheckBox = new JCheckBox(MessageUtils.getMessage("search.includeHistory"));
includeHistoryCheckBox.setSelected(true);
searchPanel.add(includeHistoryCheckBox, gbc);
// 搜索按钮
gbc.gridx = 0;
gbc.gridy = 4;
gbc.gridwidth = 2;
gbc.weightx = 1.0;
gbc.anchor = GridBagConstraints.CENTER;
JButton searchButton = new JButton(MessageUtils.getMessage("common.search"));
searchButton.addActionListener(e -> performSearch());
searchPanel.add(searchButton, gbc);
add(searchPanel, BorderLayout.NORTH);
// 创建结果表格
String[] columnNames = {
MessageUtils.getMessage("table.header.file.url"),
MessageUtils.getMessage("table.header.printer"),
MessageUtils.getMessage("table.header.status"),
MessageUtils.getMessage("table.header.queued.time"),
MessageUtils.getMessage("table.header.start.time"),
MessageUtils.getMessage("table.header.end.time")
};
tableModel = new DefaultTableModel(columnNames, 0) {
@Override
public boolean isCellEditable(int row, int column) {
return false;
}
};
resultsTable = new JTable(tableModel);
resultsTable.getTableHeader().setReorderingAllowed(false);
resultsTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
// 添加双击事件
resultsTable.addMouseListener(new java.awt.event.MouseAdapter() {
@Override
public void mouseClicked(java.awt.event.MouseEvent evt) {
if (evt.getClickCount() == 2) {
showTaskDetails();
}
}
});
JScrollPane scrollPane = new JScrollPane(resultsTable);
add(scrollPane, BorderLayout.CENTER);
// 创建按钮面板
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
JButton detailsButton = new JButton(MessageUtils.getMessage("search.viewDetails"));
detailsButton.addActionListener(e -> showTaskDetails());
buttonPanel.add(detailsButton);
JButton clearButton = new JButton(MessageUtils.getMessage("search.clearResults"));
clearButton.addActionListener(e -> clearResults());
buttonPanel.add(clearButton);
add(buttonPanel, BorderLayout.SOUTH);
}
/**
* 执行搜索
*/
private void performSearch() {
// 清空当前结果
clearResults();
// 获取搜索条件
String printer = printerComboBox.getSelectedItem().toString();
String status = statusComboBox.getSelectedItem().toString();
String fileUrl = fileUrlField.getText().trim();
// 获取当前任务和队列任务
List<PrintTask> currentTasks = printQueueService.getCurrentTask() != null ?
List.of(printQueueService.getCurrentTask()) : List.of();
List<PrintTask> queuedTasks = printQueueService.getQueuedTasks();
// 添加当前任务和队列任务到结果中
addTasksToResults(currentTasks, printer, status, fileUrl);
addTasksToResults(queuedTasks, printer, status, fileUrl);
// 如果选择包含历史记录则添加历史任务到结果中
if (includeHistoryCheckBox.isSelected()) {
List<PrintTask> historyTasks = historyService.getAllHistory();
addTasksToResults(historyTasks, printer, status, fileUrl);
}
// 更新状态
updateStatus();
}
/**
* 将符合条件的任务添加到结果表格中
*
* @param tasks 任务列表
* @param printer 打印机筛选条件
* @param status 状态筛选条件
* @param fileUrl 文件URL筛选条件
*/
private void addTasksToResults(List<PrintTask> tasks, String printer, String status, String fileUrl) {
for (PrintTask task : tasks) {
// 应用筛选条件
if (!allText.equals(printer) && !printer.equals(task.getPrinter())) {
continue;
}
if (!allText.equals(status) && !status.equals(task.getStatus())) {
continue;
}
if (!fileUrl.isEmpty() && !task.getFileUrl().contains(fileUrl)) {
continue;
}
// 添加到表格
Object[] row = {
task.getFileUrl(),
task.getPrinter(),
task.getStatus(),
formatDateTime(task.getQueuedTime()),
formatDateTime(task.getStartTime()),
formatDateTime(task.getEndTime())
};
tableModel.addRow(row);
}
}
/**
* 格式化日期时间
*
* @param dateTime 日期时间
* @return 格式化后的字符串
*/
private String formatDateTime(LocalDateTime dateTime) {
return dateTime != null ? dateTime.format(dateFormatter) : "--";
}
/**
* 更新状态信息
*/
private void updateStatus() {
int resultCount = tableModel.getRowCount();
if (resultCount == 0) {
JOptionPane.showMessageDialog(this, MessageUtils.getMessage("search.noResults"), MessageUtils.getMessage("dialog.title.search.results"), JOptionPane.INFORMATION_MESSAGE);
}
}
/**
* 清空搜索结果
*/
private void clearResults() {
while (tableModel.getRowCount() > 0) {
tableModel.removeRow(0);
}
}
/**
* 显示任务详情
*/
private void showTaskDetails() {
int selectedRow = resultsTable.getSelectedRow();
if (selectedRow >= 0) {
String fileUrl = (String) tableModel.getValueAt(selectedRow, 0);
String printer = (String) tableModel.getValueAt(selectedRow, 1);
String status = (String) tableModel.getValueAt(selectedRow, 2);
String queuedTime = (String) tableModel.getValueAt(selectedRow, 3);
String startTime = (String) tableModel.getValueAt(selectedRow, 4);
String endTime = (String) tableModel.getValueAt(selectedRow, 5);
// 查找对应的任务对象
PrintTask task = findTaskByFileUrl(fileUrl);
if (task != null) {
// 显示详情对话框
PrintTaskDetailDialog dialog = new PrintTaskDetailDialog(SwingUtilities.getWindowAncestor(this), task);
dialog.setVisible(true);
} else {
// 使用表格中的数据创建一个简化的任务对象
PrintTask simpleTask = new PrintTask();
simpleTask.setFileUrl(fileUrl);
simpleTask.setPrinter(printer);
simpleTask.setStatus(status);
// 显示详情对话框
PrintTaskDetailDialog dialog = new PrintTaskDetailDialog(SwingUtilities.getWindowAncestor(this), simpleTask);
dialog.setVisible(true);
}
} else {
JOptionPane.showMessageDialog(this, MessageUtils.getMessage("search.selectTask"), MessageUtils.getMessage("dialog.title.prompt"), JOptionPane.INFORMATION_MESSAGE);
}
}
/**
* 根据文件URL查找任务
*
* @param fileUrl 文件URL
* @return 任务对象如果未找到则返回null
*/
private PrintTask findTaskByFileUrl(String fileUrl) {
return findTaskByFileUrl(fileUrl, true);
}
/**
* 根据文件URL查找任务可选是否包含历史记录
*
* @param fileUrl 文件URL
* @param includeHistory 是否包含历史记录
* @return 任务对象如果未找到则返回null
*/
private PrintTask findTaskByFileUrl(String fileUrl, boolean includeHistory) {
// 检查当前任务
PrintTask currentTask = printQueueService.getCurrentTask();
if (currentTask != null && currentTask.getFileUrl().equals(fileUrl)) {
return currentTask;
}
// 检查队列中的任务
for (PrintTask task : printQueueService.getQueuedTasks()) {
if (task.getFileUrl().equals(fileUrl)) {
return task;
}
}
// 检查历史记录中的任务
if (includeHistory && includeHistoryCheckBox.isSelected()) {
List<PrintTask> historyTasks = historyService.getHistoryByFileUrl(fileUrl);
if (!historyTasks.isEmpty()) {
return historyTasks.get(0);
}
}
return null;
}
/**
* 当语言变更时更新界面
*/
@Override
public void onLocaleChanged(Locale newLocale) {
SwingUtilities.invokeLater(() -> {
// 更新 allText 变量
allText = MessageUtils.getMessage("common.all");
// 更新标签文本
java.awt.Component[] searchPanelComponents = ((JPanel)getComponent(0)).getComponents();
for (java.awt.Component component : searchPanelComponents) {
if (component instanceof JLabel) {
JLabel label = (JLabel) component;
if (label.getText().contains("打印机") || label.getText().contains("Printer")) {
label.setText(MessageUtils.getMessage("search.printer"));
} else if (label.getText().contains("状态") || label.getText().contains("Status")) {
label.setText(MessageUtils.getMessage("search.status"));
} else if (label.getText().contains("URL")) {
label.setText(MessageUtils.getMessage("search.fileUrl"));
}
} else if (component instanceof JCheckBox) {
((JCheckBox) component).setText(MessageUtils.getMessage("search.includeHistory"));
} else if (component instanceof JButton) {
((JButton) component).setText(MessageUtils.getMessage("common.search"));
} else if (component instanceof JComboBox) {
JComboBox<String> comboBox = (JComboBox<String>) component;
if (comboBox.getItemAt(0).toString().equals("全部") ||
comboBox.getItemAt(0).toString().equals("All")) {
comboBox.removeItemAt(0);
comboBox.insertItemAt(MessageUtils.getMessage("common.all"), 0);
comboBox.setSelectedIndex(0);
}
}
}
// 更新按钮文本
JPanel buttonPanel = (JPanel) getComponent(2);
java.awt.Component[] buttonComponents = buttonPanel.getComponents();
for (java.awt.Component component : buttonComponents) {
if (component instanceof JButton) {
JButton button = (JButton) component;
if (button.getText().contains("详情") || button.getText().contains("Details")) {
button.setText(MessageUtils.getMessage("search.viewDetails"));
} else if (button.getText().contains("清空") || button.getText().contains("Clear")) {
button.setText(MessageUtils.getMessage("search.clearResults"));
}
}
}
// 更新表格列名
String[] columnNames = {
MessageUtils.getMessage("table.header.file.url"),
MessageUtils.getMessage("table.header.printer"),
MessageUtils.getMessage("table.header.status"),
MessageUtils.getMessage("table.header.queued.time"),
MessageUtils.getMessage("table.header.start.time"),
MessageUtils.getMessage("table.header.end.time")
};
for (int i = 0; i < columnNames.length; i++) {
resultsTable.getColumnModel().getColumn(i).setHeaderValue(columnNames[i]);
}
resultsTable.getTableHeader().repaint();
});
}
/**
* 关闭资源
*/
public void shutdown() {
LocaleManager.getInstance().removeLocaleChangeListener(this);
}
}

View File

@ -0,0 +1,222 @@
package com.goeing.printserver.main.gui;
import com.goeing.printserver.main.PrintController;
import com.goeing.printserver.main.config.PrintServerConfig;
import com.goeing.printserver.main.utils.LocaleChangeListener;
import com.goeing.printserver.main.utils.LocaleManager;
import com.goeing.printserver.main.utils.MessageUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.swing.*;
import javax.swing.table.DefaultTableModel;
import java.awt.*;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* 打印机状态面板
*/
@Component
@Slf4j
public class PrinterStatusPanel extends JPanel implements LocaleChangeListener {
private PrintController printController;
private final PrintServerConfig config;
private JTable printerTable;
private DefaultTableModel printerModel;
private final ScheduledExecutorService refreshExecutor = Executors.newSingleThreadScheduledExecutor();
// 使用ApplicationEventPublisher来发布事件避免循环依赖
@Autowired
private org.springframework.context.ApplicationEventPublisher eventPublisher;
@Autowired
public PrinterStatusPanel(PrintServerConfig config) {
this.config = config;
// 注册语言变更监听器
LocaleManager.getInstance().addLocaleChangeListener(this);
}
@Autowired
public void setPrintController(PrintController printController) {
this.printController = printController;
// 在设置完依赖后初始化UI和定时器
initializeUI();
startRefreshTimer();
}
/**
* 初始化用户界面
*/
private void initializeUI() {
setLayout(new BorderLayout());
setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
// 创建顶部面板
JPanel topPanel = new JPanel(new BorderLayout());
JLabel titleLabel = new JLabel(MessageUtils.getMessage("printer.status.title"));
titleLabel.setFont(new Font(titleLabel.getFont().getName(), Font.BOLD, 14));
topPanel.add(titleLabel, BorderLayout.WEST);
// 创建刷新按钮
JButton refreshButton = new JButton(MessageUtils.getMessage("button.refresh"));
refreshButton.addActionListener(e -> refreshPrinterList());
topPanel.add(refreshButton, BorderLayout.EAST);
add(topPanel, BorderLayout.NORTH);
// 创建表格模型
String[] columnNames = {
MessageUtils.getMessage("table.header.printer.name"),
MessageUtils.getMessage("table.header.status"),
MessageUtils.getMessage("table.header.default")
};
printerModel = new DefaultTableModel(columnNames, 0) {
@Override
public boolean isCellEditable(int row, int column) {
return false; // 禁止编辑单元格
}
@Override
public Class<?> getColumnClass(int columnIndex) {
return columnIndex == 2 ? Boolean.class : String.class;
}
};
// 创建表格
printerTable = new JTable(printerModel);
printerTable.getTableHeader().setReorderingAllowed(false);
printerTable.setFillsViewportHeight(true);
// 添加滚动面板
JScrollPane scrollPane = new JScrollPane(printerTable);
add(scrollPane, BorderLayout.CENTER);
}
/**
* 启动定时刷新
*/
private void startRefreshTimer() {
// 增加刷新间隔到60秒减少刷新频率
refreshExecutor.scheduleAtFixedRate(this::refreshPrinterList, 0, 60, TimeUnit.SECONDS);
}
/**
* 刷新打印机列表
*/
private void refreshPrinterList() {
SwingUtilities.invokeLater(() -> {
try {
List<String> printers = printController.printerList();
updatePrinterTable(printers);
} catch (Exception e) {
log.error("刷新打印机列表时发生错误", e);
}
});
}
/**
* 更新打印机表格
*
* @param printers 打印机列表
*/
private void updatePrinterTable(List<String> printers) {
// 清空表格
printerModel.setRowCount(0);
if (printers != null && !printers.isEmpty()) {
String defaultPrinter = config.getDefaultPrinter();
for (String printer : printers) {
Object[] rowData = new Object[3];
rowData[0] = printer;
rowData[1] = MessageUtils.getMessage("printer.status.available"); // 默认状态为可用
rowData[2] = printer.equals(defaultPrinter); // 是否为默认打印机
printerModel.addRow(rowData);
}
// 更新PrintSettingsPanel中的打印机下拉列表
updatePrinterComboBoxes(printers);
}
}
/**
* 更新所有打印机下拉列表
*
* @param printers 打印机列表
*/
private void updatePrinterComboBoxes(List<String> printers) {
// 发布打印机列表更新事件
eventPublisher.publishEvent(new PrinterListUpdatedEvent(printers));
// 使用debug级别记录日志减少info日志输出
log.debug(MessageUtils.getMessage("log.printer.list.updated"), printers.size());
}
/**
* 打印机列表更新事件
*/
public static class PrinterListUpdatedEvent {
private final List<String> printers;
public PrinterListUpdatedEvent(List<String> printers) {
this.printers = printers;
}
public List<String> getPrinters() {
return printers;
}
}
/**
* 关闭资源
*/
public void shutdown() {
refreshExecutor.shutdownNow();
LocaleManager.getInstance().removeLocaleChangeListener(this);
}
/**
* 当语言变更时更新界面
*/
@Override
public void onLocaleChanged(Locale newLocale) {
SwingUtilities.invokeLater(() -> {
// 更新标题标签
java.awt.Component[] components = ((JPanel)getComponent(0)).getComponents();
for (java.awt.Component component : components) {
if (component instanceof JLabel) {
((JLabel) component).setText(MessageUtils.getMessage("printer.status.title"));
break;
}
}
// 更新刷新按钮
for (java.awt.Component component : components) {
if (component instanceof JButton) {
((JButton) component).setText(MessageUtils.getMessage("button.refresh"));
break;
}
}
// 更新表格列名
String[] columnNames = {
MessageUtils.getMessage("table.header.printer.name"),
MessageUtils.getMessage("table.header.status"),
MessageUtils.getMessage("table.header.default")
};
for (int i = 0; i < columnNames.length; i++) {
printerTable.getColumnModel().getColumn(i).setHeaderValue(columnNames[i]);
}
printerTable.getTableHeader().repaint();
// 刷新打印机列表以更新状态文本
refreshPrinterList();
});
}
}

View File

@ -0,0 +1,82 @@
package com.goeing.printserver.main.gui;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.AppenderBase;
import lombok.extern.slf4j.Slf4j;
/**
* 自定义日志Appender将日志输出到Swing界面
*/
public class SwingLogAppender extends AppenderBase<ILoggingEvent> {
private static SystemLogPanel logPanel;
/**
* 设置日志面板
*/
public static void setLogPanel(SystemLogPanel panel) {
logPanel = panel;
}
@Override
protected void append(ILoggingEvent event) {
if (logPanel != null && isStarted()) {
try {
// 转换日志级别
SystemLogPanel.LogLevel level = convertLogLevel(event.getLevel());
// 格式化日志消息包含时间戳
String timestamp = java.time.LocalDateTime.ofInstant(
java.time.Instant.ofEpochMilli(event.getTimeStamp()),
java.time.ZoneId.systemDefault()
).format(java.time.format.DateTimeFormatter.ofPattern("HH:mm:ss.SSS"));
String loggerName = event.getLoggerName();
// 简化logger名称只显示最后两个包名
String[] parts = loggerName.split("\\.");
if (parts.length > 2) {
loggerName = parts[parts.length - 2] + "." + parts[parts.length - 1];
}
String message = String.format("%s [%s] %s",
timestamp,
loggerName,
event.getFormattedMessage());
// 如果有异常信息添加到消息中
if (event.getThrowableProxy() != null) {
message += "\n 异常: " + event.getThrowableProxy().getMessage();
// 添加异常堆栈的前几行
if (event.getThrowableProxy().getStackTraceElementProxyArray() != null &&
event.getThrowableProxy().getStackTraceElementProxyArray().length > 0) {
message += "\n 位置: " + event.getThrowableProxy().getStackTraceElementProxyArray()[0].toString();
}
}
// 添加到日志面板
logPanel.addLogEntry(level, message);
} catch (Exception e) {
// 避免日志记录本身出错导致的循环问题
System.err.println("SwingLogAppender error: " + e.getMessage());
}
}
}
/**
* 转换日志级别
*/
private SystemLogPanel.LogLevel convertLogLevel(ch.qos.logback.classic.Level level) {
switch (level.toInt()) {
case ch.qos.logback.classic.Level.ERROR_INT:
return SystemLogPanel.LogLevel.ERROR;
case ch.qos.logback.classic.Level.WARN_INT:
return SystemLogPanel.LogLevel.WARN;
case ch.qos.logback.classic.Level.INFO_INT:
return SystemLogPanel.LogLevel.INFO;
case ch.qos.logback.classic.Level.DEBUG_INT:
case ch.qos.logback.classic.Level.TRACE_INT:
default:
return SystemLogPanel.LogLevel.DEBUG;
}
}
}

View File

@ -0,0 +1,331 @@
package com.goeing.printserver.main.gui;
import com.goeing.printserver.main.utils.LocaleChangeListener;
import com.goeing.printserver.main.utils.LocaleManager;
import com.goeing.printserver.main.utils.MessageUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.swing.*;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
/**
* 系统日志面板
*/
@Component
@Slf4j
public class SystemLogPanel extends JPanel implements LocaleChangeListener {
private static final int MAX_LOG_LINES = 1000; // 最大日志行数
private final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private final BlockingQueue<LogEntry> logQueue = new LinkedBlockingQueue<>();
// UI组件
private JLabel titleLabel;
private JTextPane logTextPane;
private JScrollPane scrollPane;
private JButton clearButton;
private JButton saveButton;
private JComboBox<LogLevel> logLevelComboBox;
private JCheckBox autoScrollCheckBox;
// 样式
private SimpleAttributeSet infoStyle;
private SimpleAttributeSet warnStyle;
private SimpleAttributeSet errorStyle;
private SimpleAttributeSet debugStyle;
// 当前日志级别过滤
private LogLevel currentLogLevel = LogLevel.INFO;
public SystemLogPanel() {
initializeStyles();
initializeUI();
startLogProcessor();
initializeLogPanel();
// 注册语言变更监听器
LocaleManager.getInstance().addLocaleChangeListener(this);
}
/**
* 初始化文本样式
*/
private void initializeStyles() {
infoStyle = new SimpleAttributeSet();
StyleConstants.setForeground(infoStyle, Color.BLACK);
warnStyle = new SimpleAttributeSet();
StyleConstants.setForeground(warnStyle, Color.ORANGE);
StyleConstants.setBold(warnStyle, true);
errorStyle = new SimpleAttributeSet();
StyleConstants.setForeground(errorStyle, Color.RED);
StyleConstants.setBold(errorStyle, true);
debugStyle = new SimpleAttributeSet();
StyleConstants.setForeground(debugStyle, Color.GRAY);
StyleConstants.setItalic(debugStyle, true);
}
/**
* 初始化用户界面
*/
private void initializeUI() {
setLayout(new BorderLayout());
setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
// 创建顶部面板
JPanel topPanel = new JPanel(new BorderLayout());
// 标题
titleLabel = new JLabel(MessageUtils.getMessage("log.panel.title"));
titleLabel.setFont(new Font(titleLabel.getFont().getName(), Font.BOLD, 16));
titleLabel.setBorder(BorderFactory.createEmptyBorder(0, 0, 10, 0));
topPanel.add(titleLabel, BorderLayout.WEST);
// 控制面板
JPanel controlPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
// 日志级别过滤
JLabel levelLabel = new JLabel(MessageUtils.getMessage("log.level.filter"));
controlPanel.add(levelLabel);
logLevelComboBox = new JComboBox<>(LogLevel.values());
logLevelComboBox.setSelectedItem(currentLogLevel);
logLevelComboBox.addActionListener(e -> {
currentLogLevel = (LogLevel) logLevelComboBox.getSelectedItem();
refreshLogDisplay();
});
controlPanel.add(logLevelComboBox);
// 自动滚动
autoScrollCheckBox = new JCheckBox(MessageUtils.getMessage("log.auto.scroll"), true);
controlPanel.add(autoScrollCheckBox);
// 清空按钮
clearButton = new JButton(MessageUtils.getMessage("log.button.clear"));
clearButton.addActionListener(e -> clearLogs());
controlPanel.add(clearButton);
// 保存按钮
saveButton = new JButton(MessageUtils.getMessage("log.button.save"));
saveButton.addActionListener(e -> saveLogs());
controlPanel.add(saveButton);
topPanel.add(controlPanel, BorderLayout.EAST);
add(topPanel, BorderLayout.NORTH);
// 创建日志显示区域
logTextPane = new JTextPane();
logTextPane.setEditable(false);
logTextPane.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
logTextPane.setBackground(Color.WHITE);
scrollPane = new JScrollPane(logTextPane);
scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
add(scrollPane, BorderLayout.CENTER);
}
/**
* 启动日志处理器
*/
private void startLogProcessor() {
Thread logProcessor = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
LogEntry entry = logQueue.take();
SwingUtilities.invokeLater(() -> appendLogEntry(entry));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
});
logProcessor.setDaemon(true);
logProcessor.setName("LogProcessor");
logProcessor.start();
}
/**
* 初始化日志面板
*/
private void initializeLogPanel() {
// 注册到SwingLogAppender
SwingLogAppender.setLogPanel(this);
// 添加初始化日志
addLogEntry(LogLevel.INFO, "日志面板已初始化");
}
/**
* 添加日志条目公开方法供SwingLogAppender调用
*/
public void addLogEntry(LogLevel level, String message) {
LogEntry entry = new LogEntry(level, message, LocalDateTime.now());
logQueue.offer(entry);
}
/**
* 添加日志条目到显示区域
*/
private void appendLogEntry(LogEntry entry) {
if (entry.level.ordinal() < currentLogLevel.ordinal()) {
return; // 过滤掉低级别的日志
}
Document doc = logTextPane.getDocument();
try {
// 检查是否超过最大行数
if (doc.getLength() > 0) {
String text = doc.getText(0, doc.getLength());
int lineCount = text.split("\n").length;
if (lineCount > MAX_LOG_LINES) {
// 删除前面的行
int firstLineEnd = text.indexOf('\n');
if (firstLineEnd > 0) {
doc.remove(0, firstLineEnd + 1);
}
}
}
// 格式化日志消息SwingLogAppender已经包含时间戳这里只添加级别
String formattedMessage = String.format("[%s] %s\n",
entry.level.name(),
entry.message);
// 选择样式
SimpleAttributeSet style = getStyleForLevel(entry.level);
// 添加到文档
doc.insertString(doc.getLength(), formattedMessage, style);
// 自动滚动到底部
if (autoScrollCheckBox.isSelected()) {
logTextPane.setCaretPosition(doc.getLength());
}
} catch (BadLocationException e) {
log.error("添加日志条目失败", e);
}
}
/**
* 获取日志级别对应的样式
*/
private SimpleAttributeSet getStyleForLevel(LogLevel level) {
switch (level) {
case ERROR:
return errorStyle;
case WARN:
return warnStyle;
case DEBUG:
return debugStyle;
case INFO:
default:
return infoStyle;
}
}
/**
* 刷新日志显示
*/
private void refreshLogDisplay() {
// 这里可以重新过滤和显示日志
// 为简化实现暂时不做复杂的过滤重显示
}
/**
* 清空日志
*/
private void clearLogs() {
int option = JOptionPane.showConfirmDialog(this,
MessageUtils.getMessage("log.clear.confirm"),
MessageUtils.getMessage("dialog.confirm"),
JOptionPane.YES_NO_OPTION);
if (option == JOptionPane.YES_OPTION) {
logTextPane.setText("");
log.info("日志已清空");
}
}
/**
* 保存日志到文件
*/
private void saveLogs() {
JFileChooser fileChooser = new JFileChooser();
fileChooser.setDialogTitle(MessageUtils.getMessage("log.save.dialog.title"));
fileChooser.setSelectedFile(new java.io.File("printserver_logs_" +
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss")) + ".txt"));
int result = fileChooser.showSaveDialog(this);
if (result == JFileChooser.APPROVE_OPTION) {
try {
java.io.File file = fileChooser.getSelectedFile();
java.nio.file.Files.write(file.toPath(), logTextPane.getText().getBytes());
JOptionPane.showMessageDialog(this,
MessageUtils.getMessage("log.save.success", new Object[]{file.getAbsolutePath()}),
MessageUtils.getMessage("dialog.success"),
JOptionPane.INFORMATION_MESSAGE);
} catch (Exception e) {
log.error("保存日志文件失败", e);
JOptionPane.showMessageDialog(this,
MessageUtils.getMessage("log.save.error", new Object[]{e.getMessage()}),
MessageUtils.getMessage("dialog.error"),
JOptionPane.ERROR_MESSAGE);
}
}
}
@Override
public void onLocaleChanged(Locale newLocale) {
SwingUtilities.invokeLater(() -> {
titleLabel.setText(MessageUtils.getMessage("log.panel.title"));
clearButton.setText(MessageUtils.getMessage("log.button.clear"));
saveButton.setText(MessageUtils.getMessage("log.button.save"));
autoScrollCheckBox.setText(MessageUtils.getMessage("log.auto.scroll"));
});
}
/**
* 日志级别枚举
*/
public enum LogLevel {
DEBUG, INFO, WARN, ERROR
}
/**
* 日志条目类
*/
private static class LogEntry {
final LogLevel level;
final String message;
final LocalDateTime timestamp;
LogEntry(LogLevel level, String message, LocalDateTime timestamp) {
this.level = level;
this.message = message;
this.timestamp = timestamp;
}
}
/**
* 清理资源
*/
public void shutdown() {
LocaleManager.getInstance().removeLocaleChangeListener(this);
}
}

View File

@ -0,0 +1,258 @@
package com.goeing.printserver.main.gui;
import com.goeing.printserver.main.sse.PrinterClient;
import com.goeing.printserver.main.utils.LocaleChangeListener;
import com.goeing.printserver.main.utils.LocaleManager;
import com.goeing.printserver.main.utils.MessageUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.swing.*;
import java.awt.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* WebSocket连接状态面板
*/
@Component
@Slf4j
public class WebSocketStatusPanel extends JPanel implements LocaleChangeListener {
private final PrinterClient printerClient;
private final ScheduledExecutorService refreshExecutor = Executors.newSingleThreadScheduledExecutor();
private final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
// UI组件
private JLabel titleLabel;
private JLabel connectionStatusLabel;
private JLabel connectionUrlLabel;
private JLabel lastConnectTimeLabel;
private JLabel reconnectCountLabel;
private JButton reconnectButton;
private JButton disconnectButton;
// 状态数据
private boolean isConnected = false;
private String connectionUrl = "";
private LocalDateTime lastConnectTime;
private int reconnectCount = 0;
@Autowired
public WebSocketStatusPanel(PrinterClient printerClient) {
this.printerClient = printerClient;
initializeUI();
startStatusRefresh();
// 注册语言变更监听器
LocaleManager.getInstance().addLocaleChangeListener(this);
}
/**
* 初始化用户界面
*/
private void initializeUI() {
setLayout(new BorderLayout());
setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
// 创建标题
titleLabel = new JLabel(MessageUtils.getMessage("websocket.status.title"));
titleLabel.setFont(new Font(titleLabel.getFont().getName(), Font.BOLD, 16));
titleLabel.setBorder(BorderFactory.createEmptyBorder(0, 0, 10, 0));
add(titleLabel, BorderLayout.NORTH);
// 创建状态信息面板
JPanel statusPanel = new JPanel(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(5, 5, 5, 5);
gbc.anchor = GridBagConstraints.WEST;
gbc.fill = GridBagConstraints.HORIZONTAL;
// 连接状态
gbc.gridx = 0;
gbc.gridy = 0;
gbc.weightx = 0.0;
JLabel statusTitleLabel = new JLabel(MessageUtils.getMessage("websocket.status.connection"));
statusPanel.add(statusTitleLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
connectionStatusLabel = new JLabel(MessageUtils.getMessage("websocket.status.disconnected"));
connectionStatusLabel.setForeground(Color.RED);
statusPanel.add(connectionStatusLabel, gbc);
// 连接URL
gbc.gridx = 0;
gbc.gridy = 1;
gbc.weightx = 0.0;
JLabel urlTitleLabel = new JLabel(MessageUtils.getMessage("websocket.status.url"));
statusPanel.add(urlTitleLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
connectionUrlLabel = new JLabel("-");
statusPanel.add(connectionUrlLabel, gbc);
// 最后连接时间
gbc.gridx = 0;
gbc.gridy = 2;
gbc.weightx = 0.0;
JLabel timeTitleLabel = new JLabel(MessageUtils.getMessage("websocket.status.last.connect"));
statusPanel.add(timeTitleLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
lastConnectTimeLabel = new JLabel("-");
statusPanel.add(lastConnectTimeLabel, gbc);
// 重连次数
gbc.gridx = 0;
gbc.gridy = 3;
gbc.weightx = 0.0;
JLabel reconnectTitleLabel = new JLabel(MessageUtils.getMessage("websocket.status.reconnect.count"));
statusPanel.add(reconnectTitleLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
reconnectCountLabel = new JLabel("0");
statusPanel.add(reconnectCountLabel, gbc);
// 添加弹性空间
gbc.gridx = 0;
gbc.gridy = 4;
gbc.gridwidth = 2;
gbc.weighty = 1.0;
statusPanel.add(Box.createVerticalGlue(), gbc);
add(statusPanel, BorderLayout.CENTER);
// 创建按钮面板
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
reconnectButton = new JButton(MessageUtils.getMessage("websocket.button.reconnect"));
reconnectButton.addActionListener(e -> reconnectWebSocket());
buttonPanel.add(reconnectButton);
disconnectButton = new JButton(MessageUtils.getMessage("websocket.button.disconnect"));
disconnectButton.addActionListener(e -> disconnectWebSocket());
buttonPanel.add(disconnectButton);
add(buttonPanel, BorderLayout.SOUTH);
}
/**
* 启动状态刷新
*/
private void startStatusRefresh() {
refreshExecutor.scheduleAtFixedRate(this::updateStatus, 0, 1, TimeUnit.SECONDS);
}
/**
* 更新状态信息
*/
private void updateStatus() {
SwingUtilities.invokeLater(() -> {
try {
// 这里需要从PrinterClient获取实际状态
// 由于PrinterClient没有公开状态方法我们使用反射或添加公开方法
updateConnectionStatus();
} catch (Exception e) {
log.error("更新WebSocket状态失败", e);
}
});
}
/**
* 更新连接状态显示
*/
private void updateConnectionStatus() {
// 获取实际的连接状态
boolean actuallyConnected = printerClient.isConnected();
String currentUrl = printerClient.getCurrentConnectionUrl();
if (actuallyConnected) {
connectionStatusLabel.setText(MessageUtils.getMessage("websocket.status.connected"));
connectionStatusLabel.setForeground(Color.GREEN);
reconnectButton.setEnabled(false);
disconnectButton.setEnabled(true);
// 如果之前是断开状态现在连接了更新连接时间
if (!isConnected) {
lastConnectTime = LocalDateTime.now();
}
} else {
connectionStatusLabel.setText(MessageUtils.getMessage("websocket.status.disconnected"));
connectionStatusLabel.setForeground(Color.RED);
reconnectButton.setEnabled(true);
disconnectButton.setEnabled(false);
}
isConnected = actuallyConnected;
connectionUrl = currentUrl != null ? currentUrl : "";
connectionUrlLabel.setText(connectionUrl.isEmpty() ? "-" : connectionUrl);
lastConnectTimeLabel.setText(lastConnectTime != null ? lastConnectTime.format(dateFormatter) : "-");
reconnectCountLabel.setText(String.valueOf(reconnectCount));
}
/**
* 重新连接WebSocket
*/
private void reconnectWebSocket() {
try {
printerClient.reconnect();
reconnectCount++;
lastConnectTime = LocalDateTime.now();
isConnected = true; // 假设连接成功
log.info("手动重新连接WebSocket");
} catch (Exception e) {
log.error("重新连接WebSocket失败", e);
JOptionPane.showMessageDialog(this,
MessageUtils.getMessage("websocket.error.reconnect", new Object[]{e.getMessage()}),
MessageUtils.getMessage("dialog.error"),
JOptionPane.ERROR_MESSAGE);
}
}
/**
* 断开WebSocket连接
*/
private void disconnectWebSocket() {
try {
printerClient.disconnect();
isConnected = false;
log.info("手动断开WebSocket连接");
} catch (Exception e) {
log.error("断开WebSocket连接失败", e);
JOptionPane.showMessageDialog(this,
MessageUtils.getMessage("websocket.error.disconnect", new Object[]{e.getMessage()}),
MessageUtils.getMessage("dialog.error"),
JOptionPane.ERROR_MESSAGE);
}
}
@Override
public void onLocaleChanged(Locale newLocale) {
SwingUtilities.invokeLater(() -> {
titleLabel.setText(MessageUtils.getMessage("websocket.status.title"));
reconnectButton.setText(MessageUtils.getMessage("websocket.button.reconnect"));
disconnectButton.setText(MessageUtils.getMessage("websocket.button.disconnect"));
updateConnectionStatus();
});
}
/**
* 清理资源
*/
public void shutdown() {
if (refreshExecutor != null && !refreshExecutor.isShutdown()) {
refreshExecutor.shutdownNow();
}
LocaleManager.getInstance().removeLocaleChangeListener(this);
}
}

View File

@ -0,0 +1,168 @@
package com.goeing.printserver.main.service;
import com.goeing.printserver.main.domain.PrintTask;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import jakarta.annotation.PreDestroy;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;
/**
* 打印历史记录服务用于保存和查询打印任务的历史记录
*/
@Service
@Slf4j
public class PrintHistoryService {
// 使用线程安全的列表存储历史记录
private final CopyOnWriteArrayList<PrintTask> historyTasks = new CopyOnWriteArrayList<>();
// 最大历史记录数量
private static final int MAX_HISTORY_SIZE = 1000;
/**
* 添加任务到历史记录
*
* @param task 打印任务
*/
public void addTaskToHistory(PrintTask task) {
// 创建任务的副本避免引用原始对象
PrintTask historyCopy = createTaskCopy(task);
// 添加到历史记录
historyTasks.add(historyCopy);
log.debug("添加任务到历史记录: {}", historyCopy.getFileUrl());
// 如果历史记录超过最大数量移除最旧的记录
if (historyTasks.size() > MAX_HISTORY_SIZE) {
historyTasks.remove(0);
log.debug("历史记录超过最大数量,移除最旧的记录");
}
}
/**
* 创建任务的副本
*
* @param original 原始任务
* @return 任务副本
*/
private PrintTask createTaskCopy(PrintTask original) {
PrintTask copy = new PrintTask();
copy.setFileUrl(original.getFileUrl());
copy.setPrinter(original.getPrinter());
copy.setStatus(original.getStatus());
copy.setQueuedTime(original.getQueuedTime());
copy.setStartTime(original.getStartTime());
copy.setEndTime(original.getEndTime());
copy.setPrintOptions(original.getPrintOptions());
return copy;
}
/**
* 获取所有历史记录
*
* @return 历史记录列表
*/
public List<PrintTask> getAllHistory() {
return Collections.unmodifiableList(new ArrayList<>(historyTasks));
}
/**
* 根据打印机名称查询历史记录
*
* @param printer 打印机名称
* @return 历史记录列表
*/
public List<PrintTask> getHistoryByPrinter(String printer) {
return historyTasks.stream()
.filter(task -> task.getPrinter().equals(printer))
.collect(Collectors.toList());
}
/**
* 根据状态查询历史记录
*
* @param status 状态
* @return 历史记录列表
*/
public List<PrintTask> getHistoryByStatus(String status) {
return historyTasks.stream()
.filter(task -> task.getStatus().equals(status))
.collect(Collectors.toList());
}
/**
* 根据时间范围查询历史记录
*
* @param startTime 开始时间
* @param endTime 结束时间
* @return 历史记录列表
*/
public List<PrintTask> getHistoryByTimeRange(LocalDateTime startTime, LocalDateTime endTime) {
return historyTasks.stream()
.filter(task -> {
LocalDateTime taskTime = task.getQueuedTime();
return taskTime != null && !taskTime.isBefore(startTime) && !taskTime.isAfter(endTime);
})
.collect(Collectors.toList());
}
/**
* 根据文件URL查询历史记录
*
* @param fileUrl 文件URL
* @return 历史记录列表
*/
public List<PrintTask> getHistoryByFileUrl(String fileUrl) {
return historyTasks.stream()
.filter(task -> task.getFileUrl().contains(fileUrl))
.collect(Collectors.toList());
}
/**
* 清空历史记录
*/
public void clearHistory() {
historyTasks.clear();
log.info("历史记录已清空");
}
/**
* 获取历史记录数量
*
* @return 历史记录数量
*/
public int getHistoryCount() {
return historyTasks.size();
}
/**
* 获取最近的历史记录
*
* @param count 数量
* @return 历史记录列表
*/
public List<PrintTask> getRecentHistory(int count) {
int size = historyTasks.size();
if (size <= count) {
return Collections.unmodifiableList(new ArrayList<>(historyTasks));
} else {
return Collections.unmodifiableList(new ArrayList<>(historyTasks.subList(size - count, size)));
}
}
/**
* 在应用程序关闭时执行清理操作
*/
@PreDestroy
public void shutdown() {
log.info("正在关闭打印历史记录服务...");
// 可以在这里添加持久化历史记录的逻辑
log.info("打印历史记录服务已关闭");
}
}

View File

@ -0,0 +1,394 @@
package com.goeing.printserver.main.service;
import com.goeing.printserver.main.domain.dto.WebSocketMessageDTO;
import com.goeing.printserver.main.domain.request.PrintRequest;
import com.goeing.printserver.main.gui.PrintNotificationService;
import com.goeing.printserver.main.gui.PrintStatisticsPanel;
import cn.hutool.json.JSONUtil;
import jakarta.websocket.Session;
import lombok.extern.slf4j.Slf4j;
import jakarta.annotation.PreDestroy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.time.LocalDateTime;
/**
* 打印队列服务用于管理打印任务的队列和执行
*/
@Service
@Slf4j
public class PrintQueueService {
@Autowired
private PrintService printService;
@Autowired
private PrintNotificationService notificationService;
@Autowired
private PrintStatisticsPanel statisticsPanel;
@Autowired
private PrintHistoryService historyService;
// 默认最大队列大小
private static final int DEFAULT_MAX_QUEUE_SIZE = 10;
// 当前最大队列大小
private int maxQueueSize = DEFAULT_MAX_QUEUE_SIZE;
// 打印任务队列
private final BlockingQueue<PrintTask> printQueue = new LinkedBlockingQueue<>();
// 记录当前正在处理的任务
private PrintTask currentTask;
// 打印任务线程池使用单线程确保任务按顺序执行
private final ThreadPoolExecutor printExecutor = new ThreadPoolExecutor(
1, // 核心线程数
1, // 最大线程数
0L, // 空闲线程存活时间
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>() // 工作队列
);
/**
* 打印任务内部类封装打印请求和WebSocket会话
*/
private static class PrintTask {
private final PrintRequest printRequest;
private final WebSocketMessageDTO messageDTO;
private final Session session;
private final LocalDateTime queuedTime;
private LocalDateTime startTime;
private LocalDateTime endTime;
private String status; // queued, processing, completed, failed
public PrintTask(PrintRequest printRequest, WebSocketMessageDTO messageDTO, Session session) {
this.printRequest = printRequest;
this.messageDTO = messageDTO;
this.session = session;
this.queuedTime = LocalDateTime.now();
this.status = "queued";
}
public PrintRequest getPrintRequest() {
return printRequest;
}
public WebSocketMessageDTO getMessageDTO() {
return messageDTO;
}
public Session getSession() {
return session;
}
public LocalDateTime getQueuedTime() {
return queuedTime;
}
public LocalDateTime getStartTime() {
return startTime;
}
public void setStartTime(LocalDateTime startTime) {
this.startTime = startTime;
}
public LocalDateTime getEndTime() {
return endTime;
}
public void setEndTime(LocalDateTime endTime) {
this.endTime = endTime;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public Map<String, Object> toMap() {
Map<String, Object> map = new HashMap<>();
map.put("fileUrl", printRequest.getFileUrl());
map.put("printerName", printRequest.getPrinterName());
map.put("status", status);
map.put("queuedTime", queuedTime);
if (startTime != null) {
map.put("startTime", startTime);
}
if (endTime != null) {
map.put("endTime", endTime);
}
return map;
}
}
/**
* 构造函数启动打印任务处理线程
*/
public PrintQueueService() {
// 启动打印任务处理线程
startPrintTaskProcessor();
}
/**
* 添加打印任务到队列
*
* @param printRequest 打印请求
* @param messageDTO WebSocket消息DTO
* @param session WebSocket会话
* @return 是否成功添加到队列如果队列已满则返回false
*/
public boolean addPrintTask(PrintRequest printRequest, WebSocketMessageDTO messageDTO, Session session) {
// 检查队列是否已满
if (printQueue.size() >= maxQueueSize) {
log.warn("打印队列已满,无法添加新任务: {}, 当前队列长度: {}, 最大队列大小: {}",
printRequest.getFileUrl(), printQueue.size(), maxQueueSize);
return false;
}
PrintTask task = new PrintTask(printRequest, messageDTO, session);
printQueue.offer(task);
int queueSize = printQueue.size();
log.info("打印任务已添加到队列: {}, 当前队列长度: {}, 最大队列大小: {}",
printRequest.getFileUrl(), queueSize, maxQueueSize);
// 发送任务已加入队列的通知
notificationService.notifyTaskQueued(printRequest, queueSize);
statisticsPanel.incrementTotalTasks();
return true;
}
/**
* 启动打印任务处理线程
*/
private void startPrintTaskProcessor() {
printExecutor.execute(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
// 从队列中获取打印任务如果队列为空则阻塞等待
PrintTask task = printQueue.take();
processPrintTask(task);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("打印任务处理线程被中断", e);
} catch (Exception e) {
log.error("处理打印任务时发生错误", e);
}
}
});
log.info("打印任务处理线程已启动");
}
/**
* 处理打印任务
*
* @param task 打印任务
*/
private void processPrintTask(PrintTask task) {
PrintRequest printRequest = task.getPrintRequest();
WebSocketMessageDTO messageDTO = task.getMessageDTO();
Session session = task.getSession();
// 更新任务状态
currentTask = task;
task.setStartTime(LocalDateTime.now());
task.setStatus("processing");
log.info("开始处理打印任务: {}", printRequest.getFileUrl());
// 发送任务开始处理的通知
notificationService.notifyTaskStarted(printRequest, printQueue.size());
try {
// 执行打印
printService.print(printRequest);
log.info("打印任务完成: {}", printRequest.getFileUrl());
// 更新任务状态
task.setEndTime(LocalDateTime.now());
task.setStatus("completed");
// 发送任务完成的通知
notificationService.notifyTaskCompleted(printRequest, printQueue.size());
statisticsPanel.incrementCompletedTasks();
historyService.addTaskToHistory(convertToHistoryTask(task));
// 发送成功响应
Map<String, String> map = new HashMap<>();
map.put("status", "success");
map.put("msg", "");
messageDTO.setType("RESPONSE");
messageDTO.setPayload(JSONUtil.toJsonStr(map));
session.getBasicRemote().sendText(JSONUtil.toJsonStr(messageDTO));
} catch (Exception e) {
log.error("打印失败: {}", printRequest.getFileUrl(), e);
// 更新任务状态
task.setEndTime(LocalDateTime.now());
task.setStatus("failed");
// 获取错误消息
String errorMsg = e.getMessage();
if (errorMsg == null) {
errorMsg = "未知错误";
} else {
// 转义JSON特殊字符
errorMsg = errorMsg.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n");
}
// 发送任务失败的通知
notificationService.notifyTaskFailed(printRequest, errorMsg, printQueue.size());
statisticsPanel.incrementFailedTasks();
historyService.addTaskToHistory(convertToHistoryTask(task));
Map<String, String> map = new HashMap<>();
map.put("status", "fail");
map.put("msg", errorMsg);
messageDTO.setType("RESPONSE");
messageDTO.setPayload(JSONUtil.toJsonStr(map));
try {
session.getBasicRemote().sendText(JSONUtil.toJsonStr(messageDTO));
} catch (IOException ex) {
log.error("发送打印失败消息时发生错误", ex);
}
} finally {
currentTask = null;
}
}
/**
* 获取当前队列长度
*
* @return 队列长度
*/
public int getQueueSize() {
return printQueue.size();
}
/**
* 获取当前正在处理的任务信息
*
* @return 当前任务信息如果没有则返回null
*/
public Map<String, Object> getCurrentTaskInfo() {
if (currentTask == null) {
return null;
}
return currentTask.toMap();
}
/**
* 获取当前正在处理的任务
*
* @return 当前任务如果没有则返回null
*/
public com.goeing.printserver.main.domain.PrintTask getCurrentTask() {
if (currentTask == null) {
return null;
}
return convertToHistoryTask(currentTask);
}
/**
* 获取队列中所有任务的信息
*
* @return 任务信息列表
*/
public List<Map<String, Object>> getQueuedTasksInfo() {
List<Map<String, Object>> tasksInfo = new ArrayList<>();
for (PrintTask task : printQueue) {
tasksInfo.add(task.toMap());
}
return tasksInfo;
}
/**
* 获取队列中所有任务
*
* @return 任务列表
*/
public List<com.goeing.printserver.main.domain.PrintTask> getQueuedTasks() {
List<com.goeing.printserver.main.domain.PrintTask> tasks = new ArrayList<>();
for (PrintTask task : printQueue) {
tasks.add(convertToHistoryTask(task));
}
return tasks;
}
/**
* 设置最大队列大小
*
* @param maxQueueSize 最大队列大小
*/
public void setMaxQueueSize(int maxQueueSize) {
if (maxQueueSize < 1) {
log.warn("最大队列大小不能小于1设置为默认值: {}", DEFAULT_MAX_QUEUE_SIZE);
this.maxQueueSize = DEFAULT_MAX_QUEUE_SIZE;
} else {
this.maxQueueSize = maxQueueSize;
log.info("最大队列大小已设置为: {}", maxQueueSize);
}
}
/**
* 获取最大队列大小
*
* @return 最大队列大小
*/
public int getMaxQueueSize() {
return maxQueueSize;
}
/**
* 关闭打印任务处理线程池
*/
@PreDestroy
public void shutdown() {
log.info("正在关闭打印队列服务...");
printExecutor.shutdown();
try {
if (!printExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
printExecutor.shutdownNow();
}
} catch (InterruptedException e) {
printExecutor.shutdownNow();
Thread.currentThread().interrupt();
}
log.info("打印任务处理线程池已关闭");
}
/**
* 将内部PrintTask转换为域对象PrintTask
*
* @param task 内部PrintTask对象
* @return 域对象PrintTask
*/
private com.goeing.printserver.main.domain.PrintTask convertToHistoryTask(PrintTask task) {
com.goeing.printserver.main.domain.PrintTask historyTask = new com.goeing.printserver.main.domain.PrintTask();
historyTask.setFileUrl(task.getPrintRequest().getFileUrl());
historyTask.setPrinter(task.getPrintRequest().getPrinterName());
historyTask.setStatus(task.getStatus());
historyTask.setQueuedTime(task.getQueuedTime());
historyTask.setStartTime(task.getStartTime());
historyTask.setEndTime(task.getEndTime());
historyTask.setPrintOptions(task.getPrintRequest().getPrintOption());
return historyTask;
}
}

View File

@ -0,0 +1,18 @@
package com.goeing.printserver.main.service;
import com.goeing.printserver.main.domain.request.PrintRequest;
/**
* 打印服务接口定义打印相关操作
* 用于解决PrintController和PrintQueueService之间的循环依赖
*/
public interface PrintService {
/**
* 执行打印操作
*
* @param request 打印请求
* @return 打印结果
*/
String print(PrintRequest request);
}

View File

@ -4,8 +4,10 @@ import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import com.goeing.printserver.main.PrintController;
import com.goeing.printserver.main.config.PrintServerConfig;
import com.goeing.printserver.main.domain.dto.WebSocketMessageDTO;
import com.goeing.printserver.main.domain.request.PrintRequest;
import com.goeing.printserver.main.service.PrintQueueService;
import jakarta.websocket.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
@ -27,21 +29,19 @@ import java.util.stream.Collectors;
@ClientEndpoint
@Component
@Slf4j
public class PrinterClient implements ApplicationRunner {
public class PrinterClient implements ApplicationRunner {
private final PrintQueueService printQueueService;
private final PrintServerConfig config;
private Session session;
@Value("${print.printer.id}")
private String printerId;
@Value("${print.websocket.url}")
private String serverUri;
@Value("${print.websocket.apiKey}")
private String apiKey;
private final ScheduledExecutorService reconnectExecutor = Executors.newSingleThreadScheduledExecutor();
private final ScheduledExecutorService heartbeatExecutor = Executors.newSingleThreadScheduledExecutor();
private boolean isConnecting = false;
// 无参构造函数由Spring管理
public PrinterClient() {
// 构造函数注入PrintQueueService和PrintServerConfig
public PrinterClient(PrintQueueService printQueueService, PrintServerConfig config) {
this.printQueueService = printQueueService;
this.config = config;
// 构造函数不做连接操作在run方法中进行连接
}
@ -63,47 +63,31 @@ public class PrinterClient implements ApplicationRunner {
if ("print".equals(type)) {
String payload = webSocketMessageDTO.getPayload();
PrintRequest printRequest = JSONUtil.toBean(payload, PrintRequest.class);
PrintController bean = SpringUtil.getBean(PrintController.class);
// 处理打印任务
log.info("收到打印任务: {}, ", printRequest);
// 将打印任务添加到队列
log.info("收到打印任务: {}, 添加到打印队列", printRequest);
printQueueService.addPrintTask(printRequest, webSocketMessageDTO, session);
// 发送任务已接收的确认消息
Map<String,String> map = new HashMap<>();
map.put("status", "queued");
map.put("msg", "打印任务已加入队列,等待处理");
map.put("queueSize", String.valueOf(printQueueService.getQueueSize()));
WebSocketMessageDTO queuedResponse = new WebSocketMessageDTO();
queuedResponse.setType("QUEUED");
queuedResponse.setRequestId(webSocketMessageDTO.getRequestId());
queuedResponse.setPayload(JSONUtil.toJsonStr(map));
try {
bean.print(printRequest);
log.info("打印任务完成: {}", printRequest.getFileUrl());
Map<String,String> map = new HashMap<>();
map.put("status", "success");
map.put("msg", "");
webSocketMessageDTO.setType("RESPONSE");
webSocketMessageDTO.setPayload(JSONUtil.toJsonStr(map));
session.getBasicRemote().sendText(JSONUtil.toJsonStr(webSocketMessageDTO));
} catch (Exception e) {
log.error("打印失败: {}", printRequest.getFileUrl(), e);
// 发送打印失败消息
String errorMsg = e.getMessage();
if (errorMsg == null) {
errorMsg = "未知错误";
} else {
// 转义JSON特殊字符
errorMsg = errorMsg.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n");
}
Map<String,String> map = new HashMap<>();
map.put("status", "fail");
map.put("msg", errorMsg);
webSocketMessageDTO.setType("RESPONSE");
webSocketMessageDTO.setPayload(JSONUtil.toJsonStr(map));
session.getBasicRemote().sendText(JSONUtil.toJsonStr(webSocketMessageDTO));
session.getBasicRemote().sendText(JSONUtil.toJsonStr(queuedResponse));
} catch (IOException e) {
log.error("发送队列确认消息失败", e);
}
} else if ("heartbeat_ack".equals(type)) {
// 心跳响应可以记录最后一次心跳时间
log.debug("收到心跳响应");
}else if ("printerList".equals(type)) {
} else if ("printerList".equals(type)) {
PrintService[] printServices = PrinterJob.lookupPrintServices();
Set<String> collect = Arrays.stream(printServices).map(PrintService::getName).collect(Collectors.toSet());
@ -112,6 +96,26 @@ public class PrinterClient implements ApplicationRunner {
webSocketMessageDTO.setType("RESPONSE");
webSocketMessageDTO.setPayload(JSONUtil.toJsonStr(collect1));
session.getBasicRemote().sendText(JSONUtil.toJsonStr(webSocketMessageDTO));
} else if ("queueStatus".equals(type)) {
// 返回当前打印队列状态
Map<String, Object> status = new HashMap<>();
status.put("queueSize", printQueueService.getQueueSize());
status.put("timestamp", System.currentTimeMillis());
status.put("currentTask", printQueueService.getCurrentTaskInfo());
webSocketMessageDTO.setType("RESPONSE");
webSocketMessageDTO.setPayload(JSONUtil.toJsonStr(status));
session.getBasicRemote().sendText(JSONUtil.toJsonStr(webSocketMessageDTO));
} else if ("queueTasks".equals(type)) {
// 返回打印队列中的所有任务
Map<String, Object> result = new HashMap<>();
result.put("currentTask", printQueueService.getCurrentTaskInfo());
result.put("queuedTasks", printQueueService.getQueuedTasksInfo());
result.put("timestamp", System.currentTimeMillis());
webSocketMessageDTO.setType("RESPONSE");
webSocketMessageDTO.setPayload(JSONUtil.toJsonStr(result));
session.getBasicRemote().sendText(JSONUtil.toJsonStr(webSocketMessageDTO));
}
} catch (Exception e) {
@ -143,6 +147,12 @@ public class PrinterClient implements ApplicationRunner {
if (isConnecting || (session != null && session.isOpen())) {
return; // 已经连接或正在连接中
}
// 从配置对象中获取最新的连接参数
String serverUri = config.getWebsocketUrl();
String printerId = config.getPrinterId();
String apiKey = config.getApiKey();
String tempUrl = serverUri+"?printerId="+printerId+"&apiKey="+apiKey;
isConnecting = true;
@ -205,6 +215,58 @@ public class PrinterClient implements ApplicationRunner {
log.info("WebSocket客户端已关闭");
}
/**
* 重新连接WebSocket服务器用于配置更改后
*/
public void reconnect() {
log.info("配置已更改重新连接WebSocket服务器...");
if (session != null && session.isOpen()) {
try {
session.close();
} catch (Exception e) {
log.error("关闭现有连接失败", e);
}
}
session = null;
isConnecting = false;
connect();
}
/**
* 获取WebSocket连接状态
*/
public boolean isConnected() {
return session != null && session.isOpen();
}
/**
* 获取当前连接的URL
*/
public String getCurrentConnectionUrl() {
if (isConnected()) {
String serverUri = config.getWebsocketUrl();
String printerId = config.getPrinterId();
String apiKey = config.getApiKey();
return serverUri + "?printerId=" + printerId + "&apiKey=" + apiKey;
}
return null;
}
/**
* 断开WebSocket连接
*/
public void disconnect() {
if (session != null && session.isOpen()) {
try {
session.close();
log.info("WebSocket连接已手动断开");
} catch (Exception e) {
log.error("断开WebSocket连接失败", e);
throw new RuntimeException("断开连接失败", e);
}
}
}
@Override
public void run(ApplicationArguments args) {
// 应用启动后连接到WebSocket服务器

View File

@ -0,0 +1,14 @@
package com.goeing.printserver.main.utils;
import java.util.Locale;
/**
* 语言变更监听器接口
*/
public interface LocaleChangeListener {
/**
* 当语言变更时调用
* @param newLocale 新的语言区域
*/
void onLocaleChanged(Locale newLocale);
}

View File

@ -0,0 +1,79 @@
package com.goeing.printserver.main.utils;
import org.springframework.context.i18n.LocaleContextHolder;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
/**
* 语言管理器
*/
public class LocaleManager {
private static final LocaleManager instance = new LocaleManager();
private final List<LocaleChangeListener> listeners = new ArrayList<>();
private Locale currentLocale;
private LocaleManager() {
// 初始化为系统默认语言
currentLocale = Locale.getDefault();
LocaleContextHolder.setLocale(currentLocale);
}
public static LocaleManager getInstance() {
return instance;
}
/**
* 获取当前语言
*/
public Locale getCurrentLocale() {
return currentLocale;
}
/**
* 设置当前语言
*/
public void setCurrentLocale(Locale locale) {
if (locale != null && !locale.equals(currentLocale)) {
this.currentLocale = locale;
LocaleContextHolder.setLocale(locale);
notifyListeners();
}
}
/**
* 添加语言变更监听器
*/
public void addLocaleChangeListener(LocaleChangeListener listener) {
if (listener != null && !listeners.contains(listener)) {
listeners.add(listener);
}
}
/**
* 移除语言变更监听器
*/
public void removeLocaleChangeListener(LocaleChangeListener listener) {
listeners.remove(listener);
}
/**
* 通知所有监听器语言已变更
*/
private void notifyListeners() {
for (LocaleChangeListener listener : listeners) {
listener.onLocaleChanged(currentLocale);
}
}
/**
* 获取支持的语言列表
*/
public List<Locale> getSupportedLocales() {
List<Locale> locales = new ArrayList<>();
locales.add(Locale.CHINESE);
locales.add(Locale.ENGLISH);
return locales;
}
}

View File

@ -0,0 +1,97 @@
package com.goeing.printserver.main.utils;
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Autowired;
import jakarta.annotation.PostConstruct;
import java.util.Locale;
/**
* 国际化消息工具类
*/
@Component
public class MessageUtils {
private static MessageSource messageSource;
// 静态初始化块确保在类加载时就初始化一个默认的MessageSource
static {
ResourceBundleMessageSource defaultMessageSource = new ResourceBundleMessageSource();
defaultMessageSource.setBasenames("messages");
defaultMessageSource.setDefaultEncoding("UTF-8");
messageSource = defaultMessageSource;
// 静态初始化时无法使用日志使用System.out
System.out.println("MessageUtils static initialization with default messageSource");
}
@Autowired
private MessageSource autowiredMessageSource;
@PostConstruct
public void init() {
// Spring容器启动后使用注入的MessageSource替换默认的
messageSource = autowiredMessageSource;
// 初始化时的调试信息使用System.out
System.out.println("MessageUtils initialized with autowired messageSource: " + (messageSource != null));
}
/**
* 获取国际化消息
*
* @param code 消息代码
* @return 国际化消息
*/
public static String getMessage(String code) {
return getMessage(code, null);
}
/**
* 获取国际化消息
*
* @param code 消息代码
* @param args 参数
* @return 国际化消息
*/
public static String getMessage(String code, Object[] args) {
return getMessage(code, args, "");
}
/**
* 获取国际化消息
*
* @param code 消息代码
* @param args 参数
* @param defaultMessage 默认消息
* @return 国际化消息
*/
public static String getMessage(String code, Object[] args, String defaultMessage) {
// 添加空检查如果messageSource为null返回默认消息或代码
if (messageSource == null) {
// MessageSource为null时的警告使用System.err避免循环依赖
System.err.println("Warning: MessageSource is null when getting message for code: " + code);
return defaultMessage.isEmpty() ? code : defaultMessage;
}
Locale locale = LocaleContextHolder.getLocale();
return messageSource.getMessage(code, args, defaultMessage, locale);
}
/**
* 获取国际化消息指定语言
*
* @param code 消息代码
* @param args 参数
* @param locale 区域
* @return 国际化消息
*/
public static String getMessage(String code, Object[] args, Locale locale) {
// 添加空检查如果messageSource为null返回代码
if (messageSource == null) {
System.err.println("Warning: MessageSource is null when getting message for code: " + code);
return code;
}
return messageSource.getMessage(code, args, "", locale);
}
}

View File

@ -8,7 +8,7 @@ import org.apache.pdfbox.printing.Orientation;
import org.apache.pdfbox.printing.PDFPrintable;
import org.apache.pdfbox.printing.Scaling;
import javax.print.PrintService;
// 使用完全限定名称避免与自定义PrintService接口冲突
import javax.print.PrintServiceLookup;
import javax.print.attribute.HashPrintRequestAttributeSet;
import javax.print.attribute.PrintRequestAttributeSet;
@ -125,12 +125,12 @@ public class PdfPrinter {
return false;
}
PrintService[] services = PrintServiceLookup.lookupPrintServices(null, null);
javax.print.PrintService[] services = PrintServiceLookup.lookupPrintServices(null, null);
if (services == null || services.length == 0) {
return false;
}
for (PrintService service : services) {
for (javax.print.PrintService service : services) {
if (service.getName().equalsIgnoreCase(printerName)) {
return true;
}
@ -151,13 +151,13 @@ public class PdfPrinter {
throw new IllegalArgumentException("Printer name cannot be null or empty");
}
PrintService[] services = PrintServiceLookup.lookupPrintServices(null, null);
javax.print.PrintService[] services = PrintServiceLookup.lookupPrintServices(null, null);
if (services == null || services.length == 0) {
throw new RuntimeException("No working printers found");
}
for (PrintService service : services) {
for (javax.print.PrintService service : services) {
if (service.getName().equalsIgnoreCase(printerName)) {
PrinterJob job = PrinterJob.getPrinterJob();
job.setPrintService(service);

View File

@ -5,3 +5,15 @@ print.websocket.url=ws://127.0.0.1:8080/print-websocket
print.printer.id=123456
print.websocket.apiKey=519883ab-3677-ce4b-59ba-7263870d0a26
# 临时允许循环依赖,后续应该通过重构完全消除
spring.main.allow-circular-references=true
# 在macOS系统上如果遇到HeadlessException可以设置为true强制使用无头模式
# 或者在启动时添加JVM参数-Djava.awt.headless=true
app.force.headless=false
# 日志配置
# 设置打印机状态面板和设置面板的日志级别为WARN减少日志输出
logging.level.com.goeing.printserver.main.gui.PrinterStatusPanel=WARN
logging.level.com.goeing.printserver.main.gui.PrintSettingsPanel=WARN

View File

@ -0,0 +1,158 @@
# Default Messages (English)
# Common
common.close=Close
common.save=Save
common.reset=Reset
common.refresh=Refresh
common.search=Search
common.exit=Exit
common.about=About
common.view=View
common.file=File
common.help=Help
common.status=Status
common.available=Available
common.default=Default
common.all=All
common.success=Success
common.error=Error
common.warning=Warning
common.info=Information
common.confirm=Confirm
common.cancel=Cancel
common.yes=Yes
common.no=No
# Main Window
main.title=Print Queue Monitor
main.status.idle=Queue Status: Idle
main.status.processing=Queue Status: Processing Task (Queue has {0} more tasks)
main.status.waiting=Queue Status: Waiting (Queue has {0} tasks)
# Tabs
tab.current=Current Task
tab.queued=Queued Tasks
tab.printers=Printer Status
tab.statistics=Statistics
tab.search=Task Search
tab.settings=Settings
# Tab Titles
tab.current.task=Current Task
tab.queued.tasks=Queued Tasks
tab.printer.status=Printer Status
tab.task.search=Task Search
# Tab Titles
# Table Headers
table.fileUrl=File URL
table.printer=Printer
table.status=Status
table.queuedTime=Queued Time
table.startTime=Start Time
table.endTime=End Time
table.printerName=Printer Name
# Table Column Headers
table.header.file.url=File URL
table.header.printer=Printer
table.header.status=Status
table.header.queued.time=Queued Time
table.header.start.time=Start Time
table.header.end.time=End Time
# Printer Status Panel
printer.status=Printer Status
printer.refresh=Refresh
printer.status.title=Printer Status
printer.status.available=Available
# Buttons
button.refresh=Refresh
# Table Column Headers (Printer Status)
table.header.printer.name=Printer Name
table.header.default=Default
# Statistics Panel
stats.title=Print Statistics
stats.totalTasks=Total Tasks: {0}
stats.completedTasks=Completed Tasks: {0}
stats.failedTasks=Failed Tasks: {0}
stats.queueSize=Current Queue Size: {0}
stats.uptime=Uptime: {0} hours {1} minutes
stats.currentTime=Current Time: {0}
stats.reset=Reset Statistics
# Search Panel
search.printer=Printer:
search.status=Status:
search.fileUrl=File URL:
search.includeHistory=Include History
search.clearResults=Clear Results
search.viewDetails=View Details
search.noResults=No tasks found matching the criteria
search.selectTask=Please select a task first
# Settings Panel
settings.title=Print Server Settings
settings.defaultPrinter=Default Printer:
settings.maxQueueSize=Max Queue Size:
settings.notifications=Enable System Notifications
settings.startMinimized=Start Minimized to System Tray
settings.autoStart=Start Automatically at Login
settings.websocketUrl=WebSocket URL:
settings.printerId=Printer ID:
settings.apiKey=API Key:
settings.save=Save Settings
settings.reset=Reset to Defaults
settings.saved=Settings have been saved
settings.reset.confirm=Are you sure you want to reset all settings to default values?
settings.reset.success=Settings have been reset to default values
settings.save.error=Failed to save settings: {0}
settings.reset.error=Failed to reset settings: {0}
# Dialog titles
dialog.success=Success
dialog.error=Error
dialog.confirm=Confirm
# Log messages
log.settings.save.error=Error occurred while saving settings
log.settings.reset.error=Error occurred while resetting settings
log.settings.applied=Applied settings: Default printer={0}, Max queue size={1}, Enable notifications={2}, Start minimized={3}, Auto start={4}
log.printer.list.update.error=Error occurred while updating printer list
log.printer.list.updated=Updated printer list, total {0} printers
log.printer.list.event.received=Received printer list update event, updated printer dropdown list
# Task Detail Dialog
taskDetail.title=Print Task Details
taskDetail.options=Print Options
# About Dialog
about.title=About Print Server
about.version=Version: {0}
about.buildDate=Build Date: {0}
about.jdkVersion=JDK Version: {0}
about.os=Operating System: {0} {1}
about.copyright=© {0} Goeing. All rights reserved.
# Menu Items
menu.file=File
menu.file.refresh=Refresh
menu.file.exit=Exit
menu.view=View
menu.view.always.on.top=Always on Top
menu.language=Language
# System Tray
app.name=Print Queue Monitor
tray.open=Open Main Window
tray.exit=Exit
tray.notification.title=Print Server Started
tray.notification.message=Print Server is running in the background, click the tray icon to open the main window
app.status.running=Running
menu.help=Help
menu.help.about=About

View File

@ -0,0 +1,190 @@
# English Messages
# Common
common.close=Close
common.save=Save
common.reset=Reset
common.refresh=Refresh
common.search=Search
common.exit=Exit
common.about=About
common.view=View
common.file=File
common.help=Help
common.status=Status
common.available=Available
common.default=Default
common.all=All
common.success=Success
common.error=Error
common.warning=Warning
common.info=Information
common.confirm=Confirm
common.cancel=Cancel
common.yes=Yes
common.no=No
# Main Window
main.title=Print Queue Monitor
main.status.idle=Queue Status: Idle
main.status.processing=Queue Status: Processing Task (Queue has {0} more tasks)
main.status.waiting=Queue Status: Waiting (Queue has {0} tasks)
# Tabs
tab.current=Current Task
tab.queued=Queued Tasks
tab.printers=Printer Status
tab.search=Task Search
# Tab Titles
# Tab Titles
tab.current.task=Current Task
tab.queued.tasks=Queued Tasks
tab.printer.status=Printer Status
tab.statistics=Statistics
tab.task.search=Task Search
tab.settings=Settings
tab.websocket.status=WebSocket Status
tab.system.log=System Log
# Table Headers
table.fileUrl=File URL
table.printer=Printer
table.status=Status
table.queuedTime=Queued Time
table.startTime=Start Time
table.endTime=End Time
table.printerName=Printer Name
# Table Column Headers
table.header.file.url=File URL
table.header.printer=Printer
table.header.queued.time=Queued Time
table.header.start.time=Start Time
table.header.end.time=End Time
# Printer Status Panel
printer.status=Printer Status
printer.refresh=Refresh
printer.status.title=Printer Status
printer.status.available=Available
# Buttons
button.refresh=Refresh
# Table Column Headers (Printer Status)
table.header.printer.name=Printer Name
table.header.status=Status
table.header.default=Default
# Statistics Panel
stats.title=Print Statistics
stats.totalTasks=Total Tasks: {0}
stats.completedTasks=Completed Tasks: {0}
stats.failedTasks=Failed Tasks: {0}
stats.queueSize=Current Queue Size: {0}
stats.uptime=Uptime: {0} hours {1} minutes
stats.currentTime=Current Time: {0}
stats.reset=Reset Statistics
# Search Panel
search.printer=Printer:
search.status=Status:
search.fileUrl=File URL:
search.includeHistory=Include History
search.clearResults=Clear Results
search.viewDetails=View Details
search.noResults=No tasks found matching the criteria
search.selectTask=Please select a task first
# Settings Panel
settings.title=Print Server Settings
settings.defaultPrinter=Default Printer:
settings.maxQueueSize=Max Queue Size:
settings.notifications=Enable System Notifications
settings.startMinimized=Start Minimized to System Tray
settings.autoStart=Start Automatically at Login
settings.websocketUrl=WebSocket URL:
settings.printerId=Printer ID:
settings.apiKey=API Key:
settings.save=Save Settings
settings.reset=Reset to Defaults
settings.saved=Settings have been saved
settings.websocket.reconnected=WebSocket configuration has been changed and the connection has been automatically reconnected.
settings.reset.confirm=Are you sure you want to reset all settings to default values?
settings.reset.success=Settings have been reset to default values
settings.save.error=Failed to save settings: {0}
settings.reset.error=Failed to reset settings: {0}
# Dialog titles
dialog.success=Success
dialog.error=Error
dialog.confirm=Confirm
# Log messages
log.settings.save.error=Error occurred while saving settings
log.settings.reset.error=Error occurred while resetting settings
log.settings.applied=Applied settings: Default printer={0}, Max queue size={1}, Enable notifications={2}, Start minimized={3}, Auto start={4}
log.printer.list.update.error=Error occurred while updating printer list
log.printer.list.updated=Updated printer list, total {0} printers
log.printer.list.event.received=Received printer list update event, updated printer dropdown list
# Task Detail Dialog
taskDetail.title=Print Task Details
taskDetail.options=Print Options
# About Dialog
about.title=About Print Server
about.version=Version: {0}
about.buildDate=Build Date: {0}
about.jdkVersion=JDK Version: {0}
about.os=Operating System: {0} {1}
about.copyright=© {0} Goeing. All rights reserved.
# Menu Items
menu.file=File
menu.file.refresh=Refresh
menu.file.exit=Exit
menu.view=View
menu.view.always.on.top=Always on Top
menu.language=Language
# System Tray
app.name=Print Queue Monitor
tray.open=Open Main Window
tray.exit=Exit
tray.notification.title=Print Server Started
tray.notification.message=Print Server is running in the background, click the tray icon to open the main window
app.status.running=Running
menu.help=Help
menu.help.about=About
# Tab titles
tab.websocket.status=WebSocket Status
tab.system.log=System Log
# WebSocket Status Panel
websocket.status.title=WebSocket Status
websocket.status.connected=Connected
websocket.status.disconnected=Disconnected
websocket.status.url=Connection URL:
websocket.status.last.connect=Last Connect Time:
websocket.status.reconnect.count=Reconnect Count:
websocket.button.reconnect=Reconnect
websocket.button.disconnect=Disconnect
websocket.error.reconnect=Failed to reconnect: {0}
websocket.error.disconnect=Failed to disconnect: {0}
# System Log Panel
log.panel.title=System Log
log.level.all=All
log.level.error=Error
log.level.warn=Warning
log.level.info=Info
log.level.debug=Debug
log.auto.scroll=Auto Scroll
log.button.clear=Clear
log.button.save=Save
log.save.success=Log saved successfully to: {0}
log.save.error=Failed to save log: {0}

View File

@ -0,0 +1,191 @@
# 中文消息 (简体中文)
# 通用
common.close=关闭
common.save=保存
common.reset=重置
common.refresh=刷新
common.search=搜索
common.exit=退出
common.about=关于
common.view=视图
common.file=文件
common.help=帮助
common.status=状态
common.available=可用
common.default=默认
common.all=全部
common.success=成功
common.error=错误
common.warning=警告
common.info=信息
common.confirm=确认
common.cancel=取消
common.yes=
common.no=
# 主窗口
main.title=打印队列监控
main.status.idle=队列状态: 空闲
main.status.processing=队列状态: 正在处理任务 (队列中还有 {0} 个任务)
main.status.waiting=队列状态: 等待处理 (队列中有 {0} 个任务)
# 选项卡
tab.current=当前任务
tab.queued=队列任务
tab.printers=打印机状态
tab.statistics=统计信息
tab.search=任务搜索
tab.settings=设置
# 选项卡标题
tab.current.task=当前任务
tab.queued.tasks=队列任务
tab.printer.status=打印机状态
tab.task.search=任务搜索
tab.websocket.status=WebSocket状态
tab.system.log=系统日志
# 表格标题
table.fileUrl=文件URL
table.printer=打印机
table.status=状态
table.queuedTime=队列时间
table.startTime=开始时间
table.endTime=结束时间
table.printerName=打印机名称
# 表格列标题
table.header.file.url=文件URL
table.header.printer=打印机
table.header.queued.time=队列时间
table.header.start.time=开始时间
table.header.end.time=结束时间
# 打印机状态面板
printer.status=打印机状态
printer.refresh=刷新
printer.status.title=打印机状态
printer.status.available=可用
# 按钮
button.refresh=刷新
# 表格列标题(打印机状态)
table.header.printer.name=打印机名称
table.header.status=状态
table.header.default=默认
# 统计面板
stats.title=打印统计
stats.totalTasks=总任务数: {0}
stats.completedTasks=已完成任务: {0}
stats.failedTasks=失败任务: {0}
stats.queueSize=当前队列长度: {0}
stats.uptime=运行时间: {0}小时{1}分钟
stats.currentTime=当前时间: {0}
stats.reset=重置统计
# 搜索面板
search.printer=打印机:
search.status=状态:
search.fileUrl=文件URL:
search.includeHistory=包含历史记录
search.clearResults=清空结果
search.viewDetails=查看详情
search.noResults=没有找到符合条件的任务
search.selectTask=请先选择一个任务
# 设置面板
settings.title=打印服务器设置
settings.defaultPrinter=默认打印机:
settings.maxQueueSize=最大队列大小:
settings.notifications=启用系统通知
settings.startMinimized=启动时最小化到系统托盘
settings.autoStart=开机自动启动
settings.websocketUrl=WebSocket地址:
settings.printerId=打印机ID:
settings.apiKey=API密钥:
settings.save=保存设置
settings.reset=重置默认
settings.saved=设置已保存
settings.websocket.reconnected=WebSocket配置已更改连接已自动重新建立。
settings.reset.confirm=确定要重置所有设置为默认值吗?
settings.reset.success=设置已重置为默认值
settings.save.error=保存设置失败: {0}
settings.reset.error=重置设置失败: {0}
# 对话框标题
dialog.success=成功
dialog.error=错误
dialog.confirm=确认
# 日志消息
log.settings.save.error=保存设置时发生错误
log.settings.reset.error=重置设置时发生错误
log.settings.applied=应用设置: 默认打印机={0}, 最大队列大小={1}, 启用通知={2}, 启动时最小化={3}, 开机自启动={4}
log.printer.list.update.error=更新打印机列表时发生错误
log.printer.list.updated=已更新打印机列表,共{0}个打印机
log.printer.list.event.received=收到打印机列表更新事件,已更新打印机下拉列表
# 任务详情对话框
taskDetail.title=打印任务详情
taskDetail.options=打印选项
# 关于对话框
about.title=关于打印服务器
about.version=版本: {0}
about.buildDate=构建日期: {0}
about.jdkVersion=JDK版本: {0}
about.os=操作系统: {0} {1}
about.copyright=© {0} Goeing. 保留所有权利。
# 菜单项
menu.file=文件
menu.file.refresh=刷新
menu.file.exit=退出
menu.view=视图
menu.view.always.on.top=窗口置顶
menu.language=语言
menu.help=帮助
menu.help.about=关于
# 系统托盘
app.name=打印队列监控
tray.open=打开主窗口
tray.exit=退出
tray.notification.title=打印服务器已启动
tray.notification.message=打印服务器正在后台运行,点击托盘图标可打开主窗口
app.status.running=运行中
system.tray.tooltip=打印服务器
system.tray.show=显示主窗口
system.tray.hide=隐藏主窗口
system.tray.exit=退出应用程序
# 选项卡标题
tab.websocket.status=WebSocket状态
tab.system.log=系统日志
# WebSocket状态面板
websocket.status.title=WebSocket连接状态
websocket.status.connection=连接状态:
websocket.status.url=服务器地址:
websocket.status.last.connect=最后连接时间:
websocket.status.reconnect.count=重连次数:
websocket.status.connected=已连接
websocket.status.disconnected=未连接
websocket.button.reconnect=重新连接
websocket.button.disconnect=断开连接
websocket.error.reconnect=重新连接失败:{0}
websocket.error.disconnect=断开连接失败:{0}
# 系统日志面板
log.panel.title=系统日志
log.level.filter=日志级别:
log.auto.scroll=自动滚动
log.button.clear=清空
log.button.save=保存到文件
log.clear.confirm=确定要清空所有日志吗?
log.save.dialog.title=保存日志文件
log.save.success=日志文件保存成功:{0}
log.save.error=保存日志文件失败:{0}

89
start-printserver.sh Executable file
View File

@ -0,0 +1,89 @@
#!/bin/bash
# 打印服务器启动脚本
# 此脚本提供了多种启动模式,适应不同的环境需求
# 默认配置
JAR_FILE="target/goeingPrintServer.jar"
JAVA_OPTS=""
HEADLESS_MODE=false
DEBUG_MODE=false
MEMORY="512m"
# 显示帮助信息
show_help() {
echo "打印服务器启动脚本"
echo "用法: $0 [选项]"
echo ""
echo "选项:"
echo " -h, --help 显示此帮助信息"
echo " -j, --jar FILE 指定JAR文件路径 (默认: $JAR_FILE)"
echo " --headless 以无头模式运行 (无图形界面)"
echo " --debug 启用远程调试 (端口: 5005)"
echo " -m, --memory SIZE 设置最大内存 (默认: $MEMORY)"
echo ""
echo "示例:"
echo " $0 --headless 以无头模式启动服务器"
echo " $0 --memory 1g 设置最大内存为1GB"
echo " $0 --jar custom.jar 使用自定义JAR文件"
exit 0
}
# 解析命令行参数
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
show_help
;;
-j|--jar)
JAR_FILE="$2"
shift
;;
--headless)
HEADLESS_MODE=true
;;
--debug)
DEBUG_MODE=true
;;
-m|--memory)
MEMORY="$2"
shift
;;
*)
echo "未知选项: $1"
echo "使用 --help 查看帮助信息"
exit 1
;;
esac
shift
done
# 检查JAR文件是否存在
if [ ! -f "$JAR_FILE" ]; then
echo "错误: JAR文件 '$JAR_FILE' 不存在"
echo "请先构建项目或使用 --jar 选项指定正确的JAR文件路径"
exit 1
fi
# 构建Java选项
JAVA_OPTS="$JAVA_OPTS -Xmx$MEMORY"
# 添加无头模式选项
if [ "$HEADLESS_MODE" = true ]; then
JAVA_OPTS="$JAVA_OPTS -Djava.awt.headless=true"
echo "启用无头模式 (无图形界面)"
fi
# 添加调试选项
if [ "$DEBUG_MODE" = true ]; then
JAVA_OPTS="$JAVA_OPTS -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005"
echo "启用远程调试模式 (端口: 5005)"
fi
# 启动应用程序
echo "正在启动打印服务器..."
echo "使用JAR文件: $JAR_FILE"
echo "Java选项: $JAVA_OPTS"
echo ""
java $JAVA_OPTS -jar "$JAR_FILE"