From 788b6eca82841b848fdb3bcf7a5ddf93195275bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E5=BF=97=E5=86=9B?= <851516902@qq.com> Date: Mon, 18 May 2026 10:04:32 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=88=86=E7=B1=BB=EF=BC=8C?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- settings.gradle.kts | 2 +- .../protoplugin/devtools/DevToolsPanel.java | 2730 +++++++++++++++++ .../devtools/DevToolsToolWindowFactory.java | 29 + .../huangzj/protoplugin/memo/MemoItem.java | 12 +- .../huangzj/protoplugin/memo/MemoPanel.java | 1061 ++++--- src/main/resources/META-INF/plugin.xml | 5 + 6 files changed, 3489 insertions(+), 350 deletions(-) create mode 100644 src/main/java/com/huangzj/protoplugin/devtools/DevToolsPanel.java create mode 100644 src/main/java/com/huangzj/protoplugin/devtools/DevToolsToolWindowFactory.java diff --git a/settings.gradle.kts b/settings.gradle.kts index 9cd9036..648c604 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -19,4 +19,4 @@ if (!JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_11)) { ) } -rootProject.name = "Idea-Plugin" +rootProject.name = "Huangzhijun-Idea-Plugin" diff --git a/src/main/java/com/huangzj/protoplugin/devtools/DevToolsPanel.java b/src/main/java/com/huangzj/protoplugin/devtools/DevToolsPanel.java new file mode 100644 index 0000000..6c3f7f8 --- /dev/null +++ b/src/main/java/com/huangzj/protoplugin/devtools/DevToolsPanel.java @@ -0,0 +1,2730 @@ +package com.huangzj.protoplugin.devtools; + +import com.intellij.ui.JBColor; +import com.intellij.ui.components.JBList; +import com.intellij.ui.components.JBScrollPane; +import com.intellij.ui.components.JBTextArea; +import com.intellij.ui.components.JBTextField; +import com.intellij.util.ui.JBUI; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.Font; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.GridLayout; +import java.awt.RenderingHints; +import java.awt.Toolkit; +import java.awt.datatransfer.StringSelection; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.UUID; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; +import java.util.stream.Collectors; +import javax.swing.BorderFactory; +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.DefaultListCellRenderer; +import javax.swing.DefaultListModel; +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JSplitPane; +import javax.swing.JTabbedPane; +import javax.swing.JTextArea; +import javax.swing.ListSelectionModel; +import javax.swing.SwingConstants; +import javax.swing.border.TitledBorder; + +public final class DevToolsPanel extends JPanel { + + private static final DateTimeFormatter DTF = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + private static final Font MONO_FONT = new Font(Font.MONOSPACED, Font.PLAIN, 13); + private static final Font TITLE_FONT = new Font(Font.SANS_SERIF, Font.BOLD, 16); + private static final SecureRandom SECURE_RANDOM = new SecureRandom(); + private static final String ALPHANUM = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + + private static final Color ACCENT = new JBColor(new Color(0x2675BF), new Color(0x589DF6)); + private static final Color SUBTITLE = new JBColor(new Color(0x6B7280), new Color(0x9CA3AF)); + private static final Color CAT_BG = new JBColor(new Color(0xF5F5F5), new Color(0x2B2D30)); + private static final Color CAT_SEL_BG = new JBColor(new Color(0xD0E4F7), new Color(0x3C4F6A)); + private static final Color CAT_TEXT = new JBColor(new Color(0x333333), new Color(0xBBBBBB)); + private static final Color CAT_SEL_TEXT = new JBColor(new Color(0x1565C0), new Color(0x90CAF9)); + + private static final Map> CATEGORIES = new LinkedHashMap<>(); + + static { + CATEGORIES.put("\uD83D\uDD11 编码解码", Arrays.asList("Base64", "URL", "HTML", "Unicode", "转义", "全角半角")); + CATEGORIES.put("\uD83D\uDCC4 格式化", Arrays.asList("JSON", "SQL", "XML", "YAML", "Markdown")); + CATEGORIES.put("\uD83D\uDD28 生成", Arrays.asList("UUID", "随机字符串", "随机数", "Lorem", "JSON Schema")); + CATEGORIES.put("\u23F0 时间", Arrays.asList("时间戳", "时区", "日期计算", "Cron")); + CATEGORIES.put("\uD83C\uDF10 网络", Arrays.asList("IP", "URL解析")); + CATEGORIES.put("\uD83D\uDCDD 文本处理", Arrays.asList("正则", "文本对比", "文本统计", "大小写", "换行符", "列表工具")); + CATEGORIES.put("\uD83D\uDD12 安全", Arrays.asList("哈希", "JWT", "密码强度")); + CATEGORIES.put("\uD83D\uDD04 转换", Arrays.asList("进制", "颜色", "大小端", "文件大小", "Unix权限", "CSV/JSON")); + CATEGORIES.put("\uD83D\uDCD6 参考", Arrays.asList("ASCII", "HTTP状态码", "MIME", "正则速查")); + } + + private final DefaultListModel catListModel = new DefaultListModel<>(); + private final JBList catList = new JBList<>(catListModel); + private final JTabbedPane toolTabs = new JTabbedPane(JTabbedPane.TOP); + private final Map panelCache = new HashMap<>(); + + public DevToolsPanel() { + super(new BorderLayout()); + setBorder(JBUI.Borders.empty(4)); + + initCategoryList(); + initToolTabs(); + + JPanel categoryPanel = createCategoryPanel(); + categoryPanel.setMinimumSize(new Dimension(120, 0)); + categoryPanel.setPreferredSize(new Dimension(150, 0)); + + JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, categoryPanel, toolTabs); + split.setResizeWeight(0.15); + split.setDividerSize(4); + split.setBorder(null); + add(split, BorderLayout.CENTER); + + if (catListModel.size() > 0) { + catList.setSelectedIndex(0); + } + } + + private void initCategoryList() { + for (String cat : CATEGORIES.keySet()) { + catListModel.addElement(cat); + } + catList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + catList.setCellRenderer(new DefaultListCellRenderer() { + @Override + public Component getListCellRendererComponent(JList list, Object value, int index, + boolean isSelected, boolean cellHasFocus) { + JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + label.setBorder(JBUI.Borders.empty(8, 12, 8, 12)); + label.setFont(label.getFont().deriveFont(Font.PLAIN, 13f)); + if (isSelected) { + label.setBackground(CAT_SEL_BG); + label.setForeground(CAT_SEL_TEXT); + } else { + label.setBackground(CAT_BG); + label.setForeground(CAT_TEXT); + } + label.setOpaque(true); + return label; + } + }); + catList.addListSelectionListener(e -> { + if (e.getValueIsAdjusting()) return; + String cat = catList.getSelectedValue(); + if (cat != null) { + switchCategory(cat); + } + }); + } + + private JPanel createCategoryPanel() { + JPanel panel = new JPanel(new BorderLayout()); + panel.setBackground(CAT_BG); + JLabel header = new JLabel(" 工具分类", SwingConstants.LEFT); + header.setFont(header.getFont().deriveFont(Font.BOLD, 14f)); + header.setBorder(JBUI.Borders.empty(8, 8, 4, 8)); + header.setForeground(CAT_TEXT); + panel.add(header, BorderLayout.NORTH); + + JBScrollPane scroll = new JBScrollPane(catList); + scroll.setBorder(null); + scroll.getViewport().setBackground(CAT_BG); + panel.add(scroll, BorderLayout.CENTER); + return panel; + } + + private void initToolTabs() { + toolTabs.setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT); + toolTabs.setBackground(new JBColor(new Color(0xFAFAFA), new Color(0x2B2D30))); + } + + private void switchCategory(String category) { + toolTabs.removeAll(); + List tools = CATEGORIES.get(category); + if (tools == null) return; + for (String toolName : tools) { + JPanel panel = getOrCreatePanel(toolName); + toolTabs.addTab(toolName, panel); + } + } + + private JPanel getOrCreatePanel(String toolName) { + return panelCache.computeIfAbsent(toolName, this::createPanelForTool); + } + + private JPanel createPanelForTool(String toolName) { + switch (toolName) { + case "Base64": return createBase64Panel(); + case "URL": return createUrlPanel(); + case "HTML": return createHtmlEntityPanel(); + case "Unicode": return createUnicodePanel(); + case "转义": return createStringEscapePanel(); + case "全角半角": return createFullHalfWidthPanel(); + case "JSON": return createJsonPanel(); + case "SQL": return createSqlPanel(); + case "XML": return createXmlPanel(); + case "YAML": return createYamlPanel(); + case "Markdown": return createMarkdownPanel(); + case "UUID": return createUuidPanel(); + case "随机字符串": return createRandomPanel(); + case "随机数": return createRandomNumberPanel(); + case "Lorem": return createLoremPanel(); + case "JSON Schema": return createJsonSchemaPanel(); + case "时间戳": return createTimestampPanel(); + case "时区": return createTimezonePanel(); + case "日期计算": return createDateCalcPanel(); + case "Cron": return createCronPanel(); + case "IP": return createIpPanel(); + case "URL解析": return createUrlParserPanel(); + case "正则": return createRegexPanel(); + case "文本对比": return createDiffPanel(); + case "文本统计": return createTextStatsPanel(); + case "大小写": return createCasePanel(); + case "换行符": return createLineEndingPanel(); + case "列表工具": return createListToolsPanel(); + case "哈希": return createHashPanel(); + case "进制": return createBaseConvertPanel(); + case "颜色": return createColorPanel(); + case "JWT": return createJwtPanel(); + case "大小端": return createEndianPanel(); + case "文件大小": return createFileSizePanel(); + case "Unix权限": return createChmodPanel(); + case "CSV/JSON": return createCsvJsonPanel(); + case "ASCII": return createAsciiPanel(); + case "HTTP状态码": return createHttpStatusPanel(); + case "MIME": return createMimePanel(); + case "正则速查": return createRegexCheatPanel(); + case "密码强度": return createPasswordPanel(); + default: return new JPanel(); + } + } + + // ---- helpers ---- + + private static JPanel paddedPanel() { + JPanel p = new JPanel(); + p.setBorder(JBUI.Borders.empty(12)); + p.setBackground(new JBColor(new Color(0xFAFAFA), new Color(0x2B2D30))); + return p; + } + + private static JTextArea createOutputArea(String placeholder) { + JBTextArea area = new JBTextArea(); + area.setEditable(false); + area.setFont(MONO_FONT); + area.setLineWrap(true); + area.setWrapStyleWord(true); + area.getEmptyText().setText(placeholder); + area.setBackground(new JBColor(new Color(0xF8F9FA), new Color(0x1E1E1E))); + area.setForeground(new JBColor(new Color(0x333333), new Color(0xD4D4D4))); + return area; + } + + private static JTextArea createInputArea(String placeholder) { + JBTextArea area = new JBTextArea(); + area.setFont(MONO_FONT); + area.setLineWrap(true); + area.setWrapStyleWord(true); + area.getEmptyText().setText(placeholder); + area.setBackground(new JBColor(new Color(0xFFFFFF), new Color(0x252526))); + area.setForeground(new JBColor(new Color(0x333333), new Color(0xD4D4D4))); + area.setCaretColor(new JBColor(new Color(0x333333), new Color(0xD4D4D4))); + return area; + } + + private static final Color BTN_PRIMARY = new Color(0x2675BF); + private static final Color BTN_SUCCESS = new Color(0x27AE60); + private static final Color BTN_DANGER = new Color(0xE74C3C); + private static final Color BTN_WARNING = new Color(0xE67E22); + private static final Color BTN_SECONDARY = new Color(0x6B7280); + private static final Color BTN_INFO = new Color(0x8E44AD); + + private static JButton createStyledBtn(String text, Color bg, String tooltip, java.awt.event.ActionListener listener) { + JButton b = new JButton(text) { + @Override + protected void paintComponent(Graphics g) { + Graphics2D g2 = (Graphics2D) g.create(); + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + if (getModel().isPressed()) { + g2.setColor(getBackground().darker()); + } else if (getModel().isRollover()) { + g2.setColor(getBackground().brighter()); + } else { + g2.setColor(getBackground()); + } + g2.fillRoundRect(0, 0, getWidth(), getHeight(), 8, 8); + g2.dispose(); + super.paintComponent(g); + } + }; + b.setOpaque(false); + b.setBackground(bg); + b.setForeground(Color.WHITE); + b.setFont(b.getFont().deriveFont(Font.BOLD, 12f)); + b.setBorder(JBUI.Borders.empty(5, 14)); + b.setFocusPainted(false); + b.setCursor(new java.awt.Cursor(java.awt.Cursor.HAND_CURSOR)); + b.setToolTipText(tooltip); + if (listener != null) b.addActionListener(listener); + return b; + } + + private static JButton toolBtn(String text, String tooltip) { + return createStyledBtn(text, BTN_SECONDARY, tooltip, null); + } + + private static JButton toolBtn(String text, String tooltip, java.awt.event.ActionListener listener) { + return createStyledBtn(text, BTN_PRIMARY, tooltip, listener); + } + + private static JButton styledBtn(String text, Color bg, java.awt.event.ActionListener listener) { + return createStyledBtn(text, bg, null, listener); + } + + private static int parseInt(String text, int defaultValue) { + try { + return Integer.parseInt(text.trim()); + } catch (Exception e) { + return defaultValue; + } + } + + private static JButton copyBtn(JTextArea source) { + return createStyledBtn("复制", BTN_INFO, "复制结果到剪贴板", e -> { + String text = source.getText(); + if (text != null && !text.isEmpty()) { + Toolkit.getDefaultToolkit().getSystemClipboard() + .setContents(new StringSelection(text), null); + } + }); + } + + private static void showError(String msg) { + JOptionPane.showMessageDialog(null, msg, "错误", JOptionPane.ERROR_MESSAGE); + } + + private static JPanel ioPanel(JTextArea input, JTextArea output) { + JPanel p = new JPanel(new GridLayout(2, 1, 0, 8)); + p.setOpaque(false); + p.add(wrapScroll(input, "输入")); + p.add(wrapScroll(output, "输出")); + return p; + } + + private static JScrollPane wrapScroll(JTextArea area, String title) { + JScrollPane sp = new JBScrollPane(area); + sp.setBorder(BorderFactory.createTitledBorder( + BorderFactory.createLineBorder(new JBColor(new Color(0xDDDDDD), new Color(0x444444))), + title, TitledBorder.LEFT, TitledBorder.TOP, + new Font(Font.SANS_SERIF, Font.PLAIN, 11), + new JBColor(new Color(0x6B7280), new Color(0x9CA3AF)))); + return sp; + } + + private static JPanel btnRow(JButton... btns) { + JPanel p = new JPanel(new FlowLayout(FlowLayout.LEFT, 6, 4)); + p.setOpaque(false); + p.setMaximumSize(new Dimension(Integer.MAX_VALUE, 40)); + for (JButton b : btns) p.add(b); + return p; + } + + // ==================== Base64 ==================== + + private JPanel createBase64Panel() { + JPanel panel = paddedPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + + JLabel title = new JLabel("Base64 编解码"); + title.setFont(TITLE_FONT); + panel.add(title); + panel.add(Box.createVerticalStrut(4)); + JLabel desc = new JLabel("支持标准 Base64 和 URL 安全格式"); + desc.setForeground(SUBTITLE); + panel.add(desc); + panel.add(Box.createVerticalStrut(12)); + + JTextArea input = createInputArea("输入原始文本..."); + JTextArea output = createOutputArea("编码/解码结果..."); + panel.add(ioPanel(input, output)); + panel.add(Box.createVerticalStrut(8)); + + panel.add(btnRow( + toolBtn("编码", "标准 Base64 编码", e -> { + String t = input.getText(); + if (!t.isEmpty()) output.setText(Base64.getEncoder().encodeToString(t.getBytes(StandardCharsets.UTF_8))); + }), + toolBtn("解码", "标准 Base64 解码", e -> { + String t = input.getText().trim(); + if (t.isEmpty()) return; + try { output.setText(new String(Base64.getDecoder().decode(t), StandardCharsets.UTF_8)); } + catch (Exception ex) { showError("无效的 Base64 编码"); } + }), + toolBtn("URL安全编码", "URL 安全的 Base64 编码", e -> { + String t = input.getText(); + if (!t.isEmpty()) output.setText(Base64.getUrlEncoder().withoutPadding().encodeToString(t.getBytes(StandardCharsets.UTF_8))); + }), + toolBtn("URL安全解码", "URL 安全的 Base64 解码", e -> { + String t = input.getText().trim(); + if (t.isEmpty()) return; + try { output.setText(new String(Base64.getUrlDecoder().decode(t), StandardCharsets.UTF_8)); } + catch (Exception ex) { showError("无效的 URL 安全 Base64"); } + }), + toolBtn("交换", "交换输入输出", e -> { String t = input.getText(); input.setText(output.getText()); output.setText(t); }), + copyBtn(output), + toolBtn("清空", "清空所有", e -> { input.setText(""); output.setText(""); }) + )); + + return panel; + } + + // ==================== URL ==================== + + private JPanel createUrlPanel() { + JPanel panel = paddedPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + + JLabel title = new JLabel("URL 编解码"); + title.setFont(TITLE_FONT); + panel.add(title); + panel.add(Box.createVerticalStrut(4)); + JLabel desc = new JLabel("对 URL 中的特殊字符进行编码和解码"); + desc.setForeground(SUBTITLE); + panel.add(desc); + panel.add(Box.createVerticalStrut(12)); + + JTextArea input = createInputArea("输入 URL 或文本..."); + JTextArea output = createOutputArea("编码/解码结果..."); + panel.add(ioPanel(input, output)); + panel.add(Box.createVerticalStrut(8)); + + panel.add(btnRow( + toolBtn("编码", "URL 编码", e -> { + try { output.setText(URLEncoder.encode(input.getText(), StandardCharsets.UTF_8)); } + catch (Exception ex) { showError("编码失败"); } + }), + toolBtn("解码", "URL 解码", e -> { + try { output.setText(URLDecoder.decode(input.getText().trim(), StandardCharsets.UTF_8)); } + catch (Exception ex) { showError("解码失败"); } + }), + copyBtn(output), + toolBtn("清空", "清空所有", e -> { input.setText(""); output.setText(""); }) + )); + + return panel; + } + + // ==================== HTML Entity ==================== + + private JPanel createHtmlEntityPanel() { + JPanel panel = paddedPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + + JLabel title = new JLabel("HTML 实体编解码"); + title.setFont(TITLE_FONT); + panel.add(title); + panel.add(Box.createVerticalStrut(4)); + JLabel desc = new JLabel("将 HTML 特殊字符转换为实体编码,或反向还原"); + desc.setForeground(SUBTITLE); + panel.add(desc); + panel.add(Box.createVerticalStrut(12)); + + JTextArea input = createInputArea("输入 HTML 或文本..."); + JTextArea output = createOutputArea("编码/解码结果..."); + panel.add(ioPanel(input, output)); + panel.add(Box.createVerticalStrut(8)); + + panel.add(btnRow( + toolBtn("编码", "HTML 实体编码", e -> { + String t = input.getText(); + output.setText(t.replace("&", "&").replace("<", "<") + .replace(">", ">").replace("\"", """) + .replace("'", "'")); + }), + toolBtn("解码", "HTML 实体解码", e -> { + String t = input.getText(); + output.setText(t.replace("<", "<").replace(">", ">") + .replace(""", "\"").replace("'", "'") + .replace("&", "&")); + }), + copyBtn(output), + toolBtn("清空", "清空所有", e -> { input.setText(""); output.setText(""); }) + )); + + return panel; + } + + // ==================== Unicode ==================== + + private JPanel createUnicodePanel() { + JPanel panel = paddedPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + + JLabel title = new JLabel("Unicode 转换"); + title.setFont(TITLE_FONT); + panel.add(title); + panel.add(Box.createVerticalStrut(4)); + JLabel desc = new JLabel("在中文/符号与 \\uXXXX 编码之间互相转换"); + desc.setForeground(SUBTITLE); + panel.add(desc); + panel.add(Box.createVerticalStrut(12)); + + JTextArea input = createInputArea("输入文本或 \\u 编码..."); + JTextArea output = createOutputArea("转换结果..."); + panel.add(ioPanel(input, output)); + panel.add(Box.createVerticalStrut(8)); + + panel.add(btnRow( + toolBtn("转 \\u", "文本转 Unicode 编码", e -> { + StringBuilder sb = new StringBuilder(); + for (char c : input.getText().toCharArray()) { + if (c > 127) sb.append(String.format("\\u%04X", (int) c)); + else sb.append(c); + } + output.setText(sb.toString()); + }), + toolBtn("\\u 转文本", "Unicode 编码转文本", e -> { + String t = input.getText(); + StringBuilder sb = new StringBuilder(); + int i = 0; + while (i < t.length()) { + if (t.charAt(i) == '\\' && i + 5 < t.length() && t.charAt(i + 1) == 'u') { + sb.append((char) Integer.parseInt(t.substring(i + 2, i + 6), 16)); + i += 6; + } else { + sb.append(t.charAt(i)); + i++; + } + } + output.setText(sb.toString()); + }), + copyBtn(output), + toolBtn("清空", "清空所有", e -> { input.setText(""); output.setText(""); }) + )); + + return panel; + } + + // ==================== String Escape ==================== + + private JPanel createStringEscapePanel() { + JPanel panel = paddedPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + + JLabel title = new JLabel("字符串转义"); + title.setFont(TITLE_FONT); + panel.add(title); + panel.add(Box.createVerticalStrut(4)); + JLabel desc = new JLabel("Java / JavaScript / JSON 字符串的转义与还原"); + desc.setForeground(SUBTITLE); + panel.add(desc); + panel.add(Box.createVerticalStrut(12)); + + JTextArea input = createInputArea("输入文本..."); + JTextArea output = createOutputArea("转义/还原结果..."); + panel.add(ioPanel(input, output)); + panel.add(Box.createVerticalStrut(8)); + + panel.add(btnRow( + toolBtn("Java 转义", "Java 字符串转义", e -> { + String t = input.getText(); + output.setText(t.replace("\\", "\\\\").replace("\"", "\\\"") + .replace("\n", "\\n").replace("\r", "\\r") + .replace("\t", "\\t")); + }), + toolBtn("Java 还原", "Java 字符串还原", e -> { + String t = input.getText(); + output.setText(t.replace("\\t", "\t").replace("\\r", "\r") + .replace("\\n", "\n").replace("\\\"", "\"") + .replace("\\\\", "\\")); + }), + toolBtn("JSON 转义", "JSON 字符串转义", e -> { + String t = input.getText(); + output.setText(t.replace("\\", "\\\\").replace("\"", "\\\"") + .replace("\n", "\\n").replace("\r", "\\r") + .replace("\t", "\\t")); + }), + copyBtn(output), + toolBtn("清空", "清空所有", e -> { input.setText(""); output.setText(""); }) + )); + + return panel; + } + + // ==================== Full/Half Width ==================== + + private JPanel createFullHalfWidthPanel() { + JPanel panel = paddedPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + + JLabel title = new JLabel("全角半角转换"); + title.setFont(TITLE_FONT); + panel.add(title); + panel.add(Box.createVerticalStrut(4)); + JLabel desc = new JLabel("在全角与半角字符之间互相转换"); + desc.setForeground(SUBTITLE); + panel.add(desc); + panel.add(Box.createVerticalStrut(12)); + + JTextArea input = createInputArea("输入文本..."); + JTextArea output = createOutputArea("转换结果..."); + panel.add(ioPanel(input, output)); + panel.add(Box.createVerticalStrut(8)); + + panel.add(btnRow( + toolBtn("全角转半角", "全角字符转半角", e -> { + String t = input.getText(); + StringBuilder sb = new StringBuilder(); + for (char c : t.toCharArray()) { + if (c >= 0xFF01 && c <= 0xFF5E) sb.append((char) (c - 0xFEE0)); + else if (c == 0x3000) sb.append(' '); + else sb.append(c); + } + output.setText(sb.toString()); + }), + toolBtn("半角转全角", "半角字符转全角", e -> { + String t = input.getText(); + StringBuilder sb = new StringBuilder(); + for (char c : t.toCharArray()) { + if (c >= 0x21 && c <= 0x7E) sb.append((char) (c + 0xFEE0)); + else if (c == ' ') sb.append((char) 0x3000); + else sb.append(c); + } + output.setText(sb.toString()); + }), + copyBtn(output), + toolBtn("清空", "清空所有", e -> { input.setText(""); output.setText(""); }) + )); + + return panel; + } + + // ==================== JSON ==================== + + private JPanel createJsonPanel() { + JPanel panel = paddedPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + + JLabel title = new JLabel("JSON 格式化"); + title.setFont(TITLE_FONT); + panel.add(title); + panel.add(Box.createVerticalStrut(4)); + JLabel desc = new JLabel("格式化、压缩和校验 JSON 数据"); + desc.setForeground(SUBTITLE); + panel.add(desc); + panel.add(Box.createVerticalStrut(12)); + + JTextArea input = createInputArea("输入 JSON..."); + JTextArea output = createOutputArea("格式化结果..."); + panel.add(ioPanel(input, output)); + panel.add(Box.createVerticalStrut(8)); + + panel.add(btnRow( + toolBtn("格式化", "美化 JSON", e -> { + try { + com.google.gson.JsonElement je = com.google.gson.JsonParser.parseString(input.getText()); + output.setText(new com.google.gson.GsonBuilder().setPrettyPrinting().create().toJson(je)); + } catch (Exception ex) { showError("无效的 JSON: " + ex.getMessage()); } + }), + toolBtn("压缩", "压缩 JSON 为一行", e -> { + try { + com.google.gson.JsonElement je = com.google.gson.JsonParser.parseString(input.getText()); + output.setText(new com.google.gson.Gson().toJson(je)); + } catch (Exception ex) { showError("无效的 JSON"); } + }), + toolBtn("校验", "校验 JSON 格式", e -> { + try { + com.google.gson.JsonParser.parseString(input.getText()); + output.setText("JSON 格式有效"); + } catch (Exception ex) { showError("JSON 格式无效: " + ex.getMessage()); } + }), + copyBtn(output), + toolBtn("清空", "清空所有", e -> { input.setText(""); output.setText(""); }) + )); + + return panel; + } + + // ==================== SQL ==================== + + private JPanel createSqlPanel() { + JPanel panel = paddedPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + + JLabel title = new JLabel("SQL 格式化"); + title.setFont(TITLE_FONT); + panel.add(title); + panel.add(Box.createVerticalStrut(4)); + JLabel desc = new JLabel("简单格式化 SQL 语句,支持 SELECT/INSERT/UPDATE/DELETE"); + desc.setForeground(SUBTITLE); + panel.add(desc); + panel.add(Box.createVerticalStrut(12)); + + JTextArea input = createInputArea("输入 SQL..."); + JTextArea output = createOutputArea("格式化结果..."); + panel.add(ioPanel(input, output)); + panel.add(Box.createVerticalStrut(8)); + + panel.add(btnRow( + toolBtn("格式化", "格式化 SQL", e -> { + String sql = input.getText().trim(); + if (sql.isEmpty()) return; + output.setText(formatSql(sql)); + }), + toolBtn("压缩", "压缩 SQL 为一行", e -> { + output.setText(input.getText().trim().replaceAll("\\s+", " ")); + }), + copyBtn(output), + toolBtn("清空", "清空所有", e -> { input.setText(""); output.setText(""); }) + )); + + return panel; + } + + private static String formatSql(String sql) { + String[] keywords = {"SELECT", "FROM", "WHERE", "AND", "OR", "ORDER BY", "GROUP BY", + "HAVING", "LIMIT", "OFFSET", "INSERT INTO", "VALUES", "UPDATE", "SET", + "DELETE FROM", "JOIN", "LEFT JOIN", "RIGHT JOIN", "INNER JOIN", + "ON", "UNION", "UNION ALL", "CREATE TABLE", "ALTER TABLE", "DROP TABLE", + "CREATE INDEX", "DROP INDEX", "INSERT", "INTO"}; + String upper = sql.toUpperCase(); + StringBuilder sb = new StringBuilder(); + int idx = 0; + while (idx < sql.length()) { + boolean matched = false; + for (String kw : keywords) { + if (upper.startsWith(kw, idx)) { + if (sb.length() > 0 && sb.charAt(sb.length() - 1) != '\n') { + sb.append('\n'); + } + sb.append(kw); + idx += kw.length(); + matched = true; + break; + } + } + if (!matched) { + sb.append(sql.charAt(idx)); + idx++; + } + } + return sb.toString().trim(); + } + + // ==================== XML ==================== + + private JPanel createXmlPanel() { + JPanel panel = paddedPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + + JLabel title = new JLabel("XML 格式化"); + title.setFont(TITLE_FONT); + panel.add(title); + panel.add(Box.createVerticalStrut(4)); + JLabel desc = new JLabel("格式化 XML 数据"); + desc.setForeground(SUBTITLE); + panel.add(desc); + panel.add(Box.createVerticalStrut(12)); + + JTextArea input = createInputArea("输入 XML..."); + JTextArea output = createOutputArea("格式化结果..."); + panel.add(ioPanel(input, output)); + panel.add(Box.createVerticalStrut(8)); + + panel.add(btnRow( + toolBtn("格式化", "格式化 XML", e -> { + try { + javax.xml.parsers.DocumentBuilderFactory factory = javax.xml.parsers.DocumentBuilderFactory.newInstance(); + org.w3c.dom.Document doc = factory.newDocumentBuilder().parse( + new org.xml.sax.InputSource(new java.io.StringReader(input.getText()))); + javax.xml.transform.Transformer transformer = javax.xml.transform.TransformerFactory.newInstance().newTransformer(); + transformer.setOutputProperty(javax.xml.transform.OutputKeys.INDENT, "yes"); + transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); + java.io.StringWriter sw = new java.io.StringWriter(); + transformer.transform(new javax.xml.transform.dom.DOMSource(doc), new javax.xml.transform.stream.StreamResult(sw)); + output.setText(sw.toString()); + } catch (Exception ex) { showError("XML 解析失败: " + ex.getMessage()); } + }), + toolBtn("压缩", "压缩 XML 为一行", e -> { + output.setText(input.getText().replaceAll(">\\s+<", "><").trim()); + }), + copyBtn(output), + toolBtn("清空", "清空所有", e -> { input.setText(""); output.setText(""); }) + )); + + return panel; + } + + // ==================== YAML ==================== + + private JPanel createYamlPanel() { + JPanel panel = paddedPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + + JLabel title = new JLabel("YAML 格式化"); + title.setFont(TITLE_FONT); + panel.add(title); + panel.add(Box.createVerticalStrut(4)); + JLabel desc = new JLabel("校验和格式化 YAML 数据"); + desc.setForeground(SUBTITLE); + panel.add(desc); + panel.add(Box.createVerticalStrut(12)); + + JTextArea input = createInputArea("输入 YAML..."); + JTextArea output = createOutputArea("格式化结果..."); + panel.add(ioPanel(input, output)); + panel.add(Box.createVerticalStrut(8)); + + panel.add(btnRow( + toolBtn("格式化", "格式化 YAML", e -> { + String t = input.getText().trim(); + if (t.isEmpty()) return; + try { + String formatted = formatYaml(t); + output.setText(formatted); + } catch (Exception ex) { + output.setText(t); + } + }), + copyBtn(output), + toolBtn("清空", "清空所有", e -> { input.setText(""); output.setText(""); }) + )); + + return panel; + } + + private static String formatYaml(String yaml) { + StringBuilder sb = new StringBuilder(); + String[] lines = yaml.split("\n"); + for (String line : lines) { + String trimmed = line.trim(); + if (trimmed.isEmpty()) { + sb.append('\n'); + continue; + } + int indent = line.length() - line.trim().length(); + if (trimmed.startsWith("- ")) { + sb.append(" ".repeat(Math.max(0, indent / 2))).append(trimmed).append('\n'); + } else if (trimmed.contains(":")) { + sb.append(" ".repeat(Math.max(0, indent / 2))).append(trimmed).append('\n'); + } else { + sb.append(" ".repeat(Math.max(0, indent / 2))).append(trimmed).append('\n'); + } + } + return sb.toString().trim(); + } + + // ==================== Markdown ==================== + + private JPanel createMarkdownPanel() { + JPanel panel = paddedPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + + JLabel title = new JLabel("Markdown 预览"); + title.setFont(TITLE_FONT); + panel.add(title); + panel.add(Box.createVerticalStrut(4)); + JLabel desc = new JLabel("将 Markdown 转换为 HTML 预览"); + desc.setForeground(SUBTITLE); + panel.add(desc); + panel.add(Box.createVerticalStrut(12)); + + JTextArea input = createInputArea("输入 Markdown..."); + JTextArea output = createOutputArea("HTML 预览..."); + panel.add(ioPanel(input, output)); + panel.add(Box.createVerticalStrut(8)); + + panel.add(btnRow( + toolBtn("转换", "Markdown 转 HTML", e -> { + String md = input.getText(); + if (md.isEmpty()) return; + StringBuilder html = new StringBuilder(); + html.append(""); + for (String line : md.split("\n")) { + String trimmed = line.trim(); + if (trimmed.startsWith("# ")) html.append("

").append(trimmed.substring(2)).append("

"); + else if (trimmed.startsWith("## ")) html.append("

").append(trimmed.substring(3)).append("

"); + else if (trimmed.startsWith("### ")) html.append("

").append(trimmed.substring(4)).append("

"); + else if (trimmed.startsWith("- ") || trimmed.startsWith("* ")) html.append("
  • ").append(trimmed.substring(2)).append("
  • "); + else if (trimmed.startsWith("> ")) html.append("
    ").append(trimmed.substring(2)).append("
    "); + else if (trimmed.startsWith("```")) html.append("
    ");
    +                        else if (trimmed.equals("```")) html.append("
    "); + else if (trimmed.isEmpty()) html.append("
    "); + else html.append("

    ").append(trimmed).append("

    "); + } + html.append(""); + output.setText(html.toString()); + }), + copyBtn(output), + toolBtn("清空", "清空所有", e -> { input.setText(""); output.setText(""); }) + )); + + return panel; + } + + // ==================== UUID ==================== + + private JPanel createUuidPanel() { + JPanel panel = paddedPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + + JLabel title = new JLabel("UUID 生成器"); + title.setFont(TITLE_FONT); + panel.add(title); + panel.add(Box.createVerticalStrut(4)); + JLabel desc = new JLabel("生成标准 UUID,支持多种输出格式"); + desc.setForeground(SUBTITLE); + panel.add(desc); + panel.add(Box.createVerticalStrut(12)); + + JTextArea output = createOutputArea("点击下方按钮生成..."); + output.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 16)); + JScrollPane sp = new JBScrollPane(output); + sp.setPreferredSize(new Dimension(200, 60)); + panel.add(sp); + panel.add(Box.createVerticalStrut(8)); + + panel.add(btnRow( + toolBtn("标准 UUID", "生成标准格式 UUID", e -> output.setText(UUID.randomUUID().toString())), + toolBtn("大写", "生成大写 UUID", e -> output.setText(UUID.randomUUID().toString().toUpperCase())), + toolBtn("无横线", "生成无横线 UUID", e -> output.setText(UUID.randomUUID().toString().replace("-", ""))), + toolBtn("批量(10个)", "生成 10 个 UUID", e -> { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 10; i++) { + if (i > 0) sb.append('\n'); + sb.append(UUID.randomUUID().toString()); + } + output.setText(sb.toString()); + }), + copyBtn(output) + )); + + return panel; + } + + // ==================== Random String ==================== + + private JPanel createRandomPanel() { + JPanel panel = paddedPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + + JLabel title = new JLabel("随机字符串生成"); + title.setFont(TITLE_FONT); + panel.add(title); + panel.add(Box.createVerticalStrut(4)); + JLabel desc = new JLabel("生成指定长度和字符集的随机字符串"); + desc.setForeground(SUBTITLE); + panel.add(desc); + panel.add(Box.createVerticalStrut(12)); + + JPanel lenRow = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 0)); + lenRow.add(new JLabel("长度:")); + JBTextField lenField = new JBTextField("16"); + lenField.setPreferredSize(new Dimension(60, 26)); + lenRow.add(lenField); + lenRow.setMaximumSize(new Dimension(Integer.MAX_VALUE, 32)); + panel.add(lenRow); + panel.add(Box.createVerticalStrut(8)); + + JTextArea output = createOutputArea("生成的随机字符串..."); + JScrollPane sp = new JBScrollPane(output); + sp.setPreferredSize(new Dimension(200, 60)); + panel.add(sp); + panel.add(Box.createVerticalStrut(8)); + + panel.add(btnRow( + toolBtn("字母数字", "字母和数字", e -> { + int len = parseInt(lenField.getText(), 16); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < len; i++) sb.append(ALPHANUM.charAt(SECURE_RANDOM.nextInt(ALPHANUM.length()))); + output.setText(sb.toString()); + }), + toolBtn("全部字符", "所有可打印字符", e -> { + int len = parseInt(lenField.getText(), 16); + String chars = ALPHANUM + "!@#$%^&*()-_=+[]{}|;:,.<>?"; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < len; i++) sb.append(chars.charAt(SECURE_RANDOM.nextInt(chars.length()))); + output.setText(sb.toString()); + }), + toolBtn("数字", "纯数字", e -> { + int len = parseInt(lenField.getText(), 16); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < len; i++) sb.append(SECURE_RANDOM.nextInt(10)); + output.setText(sb.toString()); + }), + copyBtn(output) + )); + + return panel; + } + + // ==================== Random Number ==================== + + private JPanel createRandomNumberPanel() { + JPanel panel = paddedPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + + JLabel title = new JLabel("随机数生成"); + title.setFont(TITLE_FONT); + panel.add(title); + panel.add(Box.createVerticalStrut(4)); + JLabel desc = new JLabel("生成指定范围内的随机整数"); + desc.setForeground(SUBTITLE); + panel.add(desc); + panel.add(Box.createVerticalStrut(12)); + + JPanel rangeRow = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 0)); + rangeRow.add(new JLabel("最小值:")); + JBTextField minField = new JBTextField("0"); + minField.setPreferredSize(new Dimension(80, 26)); + rangeRow.add(minField); + rangeRow.add(new JLabel("最大值:")); + JBTextField maxField = new JBTextField("100"); + maxField.setPreferredSize(new Dimension(80, 26)); + rangeRow.add(maxField); + rangeRow.add(new JLabel("数量:")); + JBTextField countField = new JBTextField("1"); + countField.setPreferredSize(new Dimension(60, 26)); + rangeRow.add(countField); + rangeRow.setMaximumSize(new Dimension(Integer.MAX_VALUE, 32)); + panel.add(rangeRow); + panel.add(Box.createVerticalStrut(8)); + + JTextArea output = createOutputArea("生成的随机数..."); + JScrollPane sp = new JBScrollPane(output); + sp.setPreferredSize(new Dimension(200, 60)); + panel.add(sp); + panel.add(Box.createVerticalStrut(8)); + + panel.add(btnRow( + toolBtn("生成", "生成随机数", e -> { + int min = parseInt(minField.getText(), 0); + int max = parseInt(maxField.getText(), 100); + int count = parseInt(countField.getText(), 1); + if (min >= max) { showError("最小值必须小于最大值"); return; } + Random rnd = new Random(); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < count; i++) { + if (i > 0) sb.append('\n'); + sb.append(rnd.nextInt(max - min + 1) + min); + } + output.setText(sb.toString()); + }), + copyBtn(output) + )); + + return panel; + } + + // ==================== Lorem Ipsum ==================== + + private JPanel createLoremPanel() { + JPanel panel = paddedPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + + JLabel title = new JLabel("Lorem Ipsum 生成"); + title.setFont(TITLE_FONT); + panel.add(title); + panel.add(Box.createVerticalStrut(4)); + JLabel desc = new JLabel("生成占位文本,用于排版和设计预览"); + desc.setForeground(SUBTITLE); + panel.add(desc); + panel.add(Box.createVerticalStrut(12)); + + JPanel optRow = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 0)); + optRow.add(new JLabel("段落数:")); + JBTextField paraField = new JBTextField("3"); + paraField.setPreferredSize(new Dimension(60, 26)); + optRow.add(paraField); + optRow.setMaximumSize(new Dimension(Integer.MAX_VALUE, 32)); + panel.add(optRow); + panel.add(Box.createVerticalStrut(8)); + + JTextArea output = createOutputArea("生成的 Lorem Ipsum 文本..."); + JScrollPane sp = new JBScrollPane(output); + sp.setPreferredSize(new Dimension(200, 80)); + panel.add(sp); + panel.add(Box.createVerticalStrut(8)); + + String lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."; + + panel.add(btnRow( + toolBtn("生成", "生成 Lorem Ipsum", e -> { + int paras = parseInt(paraField.getText(), 3); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < paras; i++) { + if (i > 0) sb.append("\n\n"); + sb.append(lorem); + } + output.setText(sb.toString()); + }), + copyBtn(output) + )); + + return panel; + } + + // ==================== JSON Schema ==================== + + private JPanel createJsonSchemaPanel() { + JPanel panel = paddedPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + + JLabel title = new JLabel("JSON Schema 生成"); + title.setFont(TITLE_FONT); + panel.add(title); + panel.add(Box.createVerticalStrut(4)); + JLabel desc = new JLabel("从 JSON 数据生成 JSON Schema"); + desc.setForeground(SUBTITLE); + panel.add(desc); + panel.add(Box.createVerticalStrut(12)); + + JTextArea input = createInputArea("输入 JSON 数据..."); + JTextArea output = createOutputArea("生成的 Schema..."); + panel.add(ioPanel(input, output)); + panel.add(Box.createVerticalStrut(8)); + + panel.add(btnRow( + toolBtn("生成 Schema", "从 JSON 生成 Schema", e -> { + try { + com.google.gson.JsonElement je = com.google.gson.JsonParser.parseString(input.getText()); + Map schema = new LinkedHashMap<>(); + schema.put("$schema", "http://json-schema.org/draft-07/schema#"); + schema.put("type", "object"); + if (je.isJsonObject()) { + com.google.gson.JsonObject obj = je.getAsJsonObject(); + Map properties = new LinkedHashMap<>(); + for (String key : obj.keySet()) { + Map prop = new LinkedHashMap<>(); + com.google.gson.JsonElement val = obj.get(key); + if (val.isJsonPrimitive()) { + if (val.getAsJsonPrimitive().isNumber()) prop.put("type", "number"); + else if (val.getAsJsonPrimitive().isBoolean()) prop.put("type", "boolean"); + else prop.put("type", "string"); + } else if (val.isJsonArray()) prop.put("type", "array"); + else if (val.isJsonObject()) prop.put("type", "object"); + else prop.put("type", "null"); + properties.put(key, prop); + } + schema.put("properties", properties); + } + output.setText(new com.google.gson.GsonBuilder().setPrettyPrinting().create().toJson(schema)); + } catch (Exception ex) { showError("无效的 JSON"); } + }), + copyBtn(output), + toolBtn("清空", "清空所有", e -> { input.setText(""); output.setText(""); }) + )); + + return panel; + } + + // ==================== Timestamp ==================== + + private JPanel createTimestampPanel() { + JPanel panel = paddedPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + + JLabel title = new JLabel("时间戳转换"); + title.setFont(TITLE_FONT); + panel.add(title); + panel.add(Box.createVerticalStrut(4)); + JLabel desc = new JLabel("毫秒/秒时间戳与日期时间互相转换"); + desc.setForeground(SUBTITLE); + panel.add(desc); + panel.add(Box.createVerticalStrut(12)); + + JPanel tsRow = new JPanel(new BorderLayout(8, 0)); + tsRow.setMaximumSize(new Dimension(Integer.MAX_VALUE, 32)); + tsRow.add(new JLabel("时间戳:"), BorderLayout.WEST); + JBTextField tsField = new JBTextField(); + tsField.getEmptyText().setText("输入毫秒/秒时间戳..."); + tsField.setFont(MONO_FONT); + tsRow.add(tsField, BorderLayout.CENTER); + panel.add(tsRow); + panel.add(Box.createVerticalStrut(8)); + + JPanel dtRow = new JPanel(new BorderLayout(8, 0)); + dtRow.setMaximumSize(new Dimension(Integer.MAX_VALUE, 32)); + dtRow.add(new JLabel("日期时间:"), BorderLayout.WEST); + JBTextField dtField = new JBTextField(); + dtField.getEmptyText().setText("yyyy-MM-dd HH:mm:ss"); + dtField.setFont(MONO_FONT); + dtRow.add(dtField, BorderLayout.CENTER); + panel.add(dtRow); + panel.add(Box.createVerticalStrut(12)); + + panel.add(btnRow( + toolBtn("时间戳->日期", "时间戳转日期", e -> { + try { + long ts = Long.parseLong(tsField.getText().trim()); + if (ts < 100000000000L) ts *= 1000; + dtField.setText(DTF.format(LocalDateTime.ofInstant(Instant.ofEpochMilli(ts), ZoneId.systemDefault()))); + } catch (NumberFormatException ex) { showError("无效的时间戳"); } + }), + toolBtn("日期->时间戳", "日期转时间戳", e -> { + try { + LocalDateTime ldt = LocalDateTime.parse(dtField.getText().trim(), DTF); + tsField.setText(String.valueOf(ldt.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli())); + } catch (Exception ex) { showError("日期格式错误,应为 yyyy-MM-dd HH:mm:ss"); } + }), + toolBtn("当前时间", "填入当前时间", e -> { + long now = System.currentTimeMillis(); + tsField.setText(String.valueOf(now)); + dtField.setText(DTF.format(LocalDateTime.ofInstant(Instant.ofEpochMilli(now), ZoneId.systemDefault()))); + }), + toolBtn("复制时间戳", "复制时间戳", e -> { + String t = tsField.getText(); + if (t != null && !t.isEmpty()) Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(t), null); + }), + toolBtn("复制日期", "复制日期", e -> { + String t = dtField.getText(); + if (t != null && !t.isEmpty()) Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(t), null); + }) + )); + + return panel; + } + + // ==================== Timezone ==================== + + private JPanel createTimezonePanel() { + JPanel panel = paddedPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + + JLabel title = new JLabel("时区转换"); + title.setFont(TITLE_FONT); + panel.add(title); + panel.add(Box.createVerticalStrut(4)); + JLabel desc = new JLabel("在不同时区之间转换时间"); + desc.setForeground(SUBTITLE); + panel.add(desc); + panel.add(Box.createVerticalStrut(12)); + + JPanel row = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 0)); + row.add(new JLabel("源时区:")); + JComboBox srcZone = new JComboBox<>(new String[]{"Asia/Shanghai", "America/New_York", "Europe/London", "Asia/Tokyo", "UTC"}); + row.add(srcZone); + row.add(new JLabel("目标时区:")); + JComboBox dstZone = new JComboBox<>(new String[]{"UTC", "America/New_York", "Europe/London", "Asia/Shanghai", "Asia/Tokyo"}); + row.add(dstZone); + row.setMaximumSize(new Dimension(Integer.MAX_VALUE, 32)); + panel.add(row); + panel.add(Box.createVerticalStrut(8)); + + JBTextField dtField = new JBTextField(); + dtField.getEmptyText().setText("yyyy-MM-dd HH:mm:ss"); + dtField.setFont(MONO_FONT); + panel.add(dtField); + panel.add(Box.createVerticalStrut(8)); + + JTextArea output = createOutputArea("转换结果..."); + JScrollPane sp = new JBScrollPane(output); + sp.setPreferredSize(new Dimension(200, 50)); + panel.add(sp); + panel.add(Box.createVerticalStrut(8)); + + panel.add(btnRow( + toolBtn("转换", "时区转换", e -> { + try { + LocalDateTime ldt = LocalDateTime.parse(dtField.getText().trim(), DTF); + ZonedDateTime src = ldt.atZone(ZoneId.of((String) srcZone.getSelectedItem())); + ZonedDateTime dst = src.withZoneSameInstant(ZoneId.of((String) dstZone.getSelectedItem())); + output.setText(dst.format(DTF)); + } catch (Exception ex) { showError("日期格式错误"); } + }), + toolBtn("当前时间", "填入当前时间", e -> dtField.setText(DTF.format(LocalDateTime.now()))), + copyBtn(output) + )); + + return panel; + } + + // ==================== Date Calc ==================== + + private JPanel createDateCalcPanel() { + JPanel panel = paddedPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + + JLabel title = new JLabel("日期计算"); + title.setFont(TITLE_FONT); + panel.add(title); + panel.add(Box.createVerticalStrut(4)); + JLabel desc = new JLabel("日期加减和日期差计算"); + desc.setForeground(SUBTITLE); + panel.add(desc); + panel.add(Box.createVerticalStrut(12)); + + JPanel row1 = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 0)); + row1.add(new JLabel("日期:")); + JBTextField dateField = new JBTextField(); + dateField.getEmptyText().setText("yyyy-MM-dd"); + dateField.setPreferredSize(new Dimension(140, 26)); + row1.add(dateField); + row1.add(new JLabel("天数:")); + JBTextField daysField = new JBTextField("0"); + daysField.setPreferredSize(new Dimension(60, 26)); + row1.add(daysField); + row1.setMaximumSize(new Dimension(Integer.MAX_VALUE, 32)); + panel.add(row1); + panel.add(Box.createVerticalStrut(8)); + + JPanel row2 = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 0)); + row2.add(new JLabel("日期1:")); + JBTextField d1Field = new JBTextField(); + d1Field.getEmptyText().setText("yyyy-MM-dd"); + d1Field.setPreferredSize(new Dimension(140, 26)); + row2.add(d1Field); + row2.add(new JLabel("日期2:")); + JBTextField d2Field = new JBTextField(); + d2Field.getEmptyText().setText("yyyy-MM-dd"); + d2Field.setPreferredSize(new Dimension(140, 26)); + row2.add(d2Field); + row2.setMaximumSize(new Dimension(Integer.MAX_VALUE, 32)); + panel.add(row2); + panel.add(Box.createVerticalStrut(8)); + + JTextArea output = createOutputArea("计算结果..."); + JScrollPane sp = new JBScrollPane(output); + sp.setPreferredSize(new Dimension(200, 50)); + panel.add(sp); + panel.add(Box.createVerticalStrut(8)); + + panel.add(btnRow( + toolBtn("日期加减", "计算日期加减天数", e -> { + try { + LocalDateTime dt = LocalDateTime.parse(dateField.getText().trim() + " 00:00:00", DTF); + int days = Integer.parseInt(daysField.getText().trim()); + output.setText("原日期: " + dateField.getText() + "\n加 " + days + " 天: " + dt.plusDays(days).format(DTF).substring(0, 10) + + "\n减 " + days + " 天: " + dt.minusDays(days).format(DTF).substring(0, 10)); + } catch (Exception ex) { showError("日期格式错误"); } + }), + toolBtn("日期差", "计算两个日期之差", e -> { + try { + LocalDateTime d1 = LocalDateTime.parse(d1Field.getText().trim() + " 00:00:00", DTF); + LocalDateTime d2 = LocalDateTime.parse(d2Field.getText().trim() + " 00:00:00", DTF); + long days = ChronoUnit.DAYS.between(d1, d2); + output.setText("相差 " + Math.abs(days) + " 天" + (days < 0 ? " (日期1更早)" : " (日期2更早)")); + } catch (Exception ex) { showError("日期格式错误"); } + }), + toolBtn("今天", "填入今天日期", e -> { + String today = LocalDateTime.now().format(DTF).substring(0, 10); + dateField.setText(today); + d1Field.setText(today); + }), + copyBtn(output) + )); + + return panel; + } + + // ==================== Cron ==================== + + private JPanel createCronPanel() { + JPanel panel = paddedPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + + JLabel title = new JLabel("Cron 表达式解析"); + title.setFont(TITLE_FONT); + panel.add(title); + panel.add(Box.createVerticalStrut(4)); + JLabel desc = new JLabel("解析 Cron 表达式并计算下次执行时间"); + desc.setForeground(SUBTITLE); + panel.add(desc); + panel.add(Box.createVerticalStrut(12)); + + JBTextField cronField = new JBTextField(); + cronField.getEmptyText().setText("0 0 12 * * ?"); + cronField.setFont(MONO_FONT); + panel.add(cronField); + panel.add(Box.createVerticalStrut(8)); + + JTextArea output = createOutputArea("解析结果..."); + JScrollPane sp = new JBScrollPane(output); + sp.setPreferredSize(new Dimension(200, 70)); + panel.add(sp); + panel.add(Box.createVerticalStrut(8)); + + panel.add(btnRow( + toolBtn("解析", "解析 Cron 表达式", e -> { + String cron = cronField.getText().trim(); + if (cron.isEmpty()) return; + String[] parts = cron.split("\\s+"); + if (parts.length < 6) { showError("Cron 表达式需要至少 6 个字段"); return; } + StringBuilder sb = new StringBuilder(); + sb.append("秒: ").append(parts[0]).append("\n"); + sb.append("分: ").append(parts[1]).append("\n"); + sb.append("时: ").append(parts[2]).append("\n"); + sb.append("日: ").append(parts[3]).append("\n"); + sb.append("月: ").append(parts[4]).append("\n"); + sb.append("周: ").append(parts[5]).append("\n"); + if (parts.length > 6) sb.append("年: ").append(parts[6]); + output.setText(sb.toString()); + }), + toolBtn("常用示例", "显示常用 Cron 示例", e -> { + output.setText("0 0 12 * * ? 每天中午12点\n" + + "0 0/5 * * * ? 每5分钟\n" + + "0 0 9-18 * * ? 每天9点到18点每小时\n" + + "0 0 0 * * ? 每天午夜\n" + + "0 0 8 * * 1-5 工作日早上8点\n" + + "0 0 0 1 * ? 每月1号午夜"); + }), + copyBtn(output) + )); + + return panel; + } + + // ==================== IP ==================== + + private JPanel createIpPanel() { + JPanel panel = paddedPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + + JLabel title = new JLabel("IP 计算"); + title.setFont(TITLE_FONT); + panel.add(title); + panel.add(Box.createVerticalStrut(4)); + JLabel desc = new JLabel("IP 子网计算,支持 CIDR 格式"); + desc.setForeground(SUBTITLE); + panel.add(desc); + panel.add(Box.createVerticalStrut(12)); + + JBTextField ipField = new JBTextField(); + ipField.getEmptyText().setText("192.168.1.0/24"); + ipField.setFont(MONO_FONT); + panel.add(ipField); + panel.add(Box.createVerticalStrut(8)); + + JTextArea output = createOutputArea("计算结果..."); + JScrollPane sp = new JBScrollPane(output); + sp.setPreferredSize(new Dimension(200, 80)); + panel.add(sp); + panel.add(Box.createVerticalStrut(8)); + + panel.add(btnRow( + toolBtn("计算", "计算子网信息", e -> { + String cidr = ipField.getText().trim(); + try { + String[] parts = cidr.split("/"); + String ip = parts[0]; + int prefix = Integer.parseInt(parts[1]); + String[] octets = ip.split("\\."); + int ipInt = (Integer.parseInt(octets[0]) << 24) | (Integer.parseInt(octets[1]) << 16) + | (Integer.parseInt(octets[2]) << 8) | Integer.parseInt(octets[3]); + int mask = prefix == 0 ? 0 : (0xFFFFFFFF << (32 - prefix)); + int network = ipInt & mask; + int broadcast = network | (~mask & 0x7FFFFFFF); + output.setText("IP: " + ip + "/" + prefix + "\n" + + "子网掩码: " + toIp(mask) + "\n" + + "网络地址: " + toIp(network) + "\n" + + "广播地址: " + toIp(broadcast) + "\n" + + "可用主机数: " + (int) Math.pow(2, 32 - prefix) + "\n" + + "可用范围: " + toIp(network + 1) + " - " + toIp(broadcast - 1)); + } catch (Exception ex) { showError("无效的 CIDR 格式"); } + }), + copyBtn(output) + )); + + return panel; + } + + private static String toIp(int ip) { + return (ip >>> 24) + "." + ((ip >> 16) & 0xFF) + "." + ((ip >> 8) & 0xFF) + "." + (ip & 0xFF); + } + + // ==================== URL Parser ==================== + + private JPanel createUrlParserPanel() { + JPanel panel = paddedPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + + JLabel title = new JLabel("URL 解析"); + title.setFont(TITLE_FONT); + panel.add(title); + panel.add(Box.createVerticalStrut(4)); + JLabel desc = new JLabel("解析 URL 的各个组成部分"); + desc.setForeground(SUBTITLE); + panel.add(desc); + panel.add(Box.createVerticalStrut(12)); + + JBTextField urlField = new JBTextField(); + urlField.getEmptyText().setText("https://user:pass@example.com:8080/path?q=1#frag"); + urlField.setFont(MONO_FONT); + panel.add(urlField); + panel.add(Box.createVerticalStrut(8)); + + JTextArea output = createOutputArea("解析结果..."); + JScrollPane sp = new JBScrollPane(output); + sp.setPreferredSize(new Dimension(200, 80)); + panel.add(sp); + panel.add(Box.createVerticalStrut(8)); + + panel.add(btnRow( + toolBtn("解析", "解析 URL", e -> { + try { + java.net.URI uri = new java.net.URI(urlField.getText().trim()); + StringBuilder sb = new StringBuilder(); + sb.append("协议: ").append(uri.getScheme()).append("\n"); + sb.append("主机: ").append(uri.getHost()).append("\n"); + sb.append("端口: ").append(uri.getPort() > 0 ? uri.getPort() : "(默认)").append("\n"); + sb.append("路径: ").append(uri.getPath()).append("\n"); + sb.append("查询: ").append(uri.getQuery()).append("\n"); + sb.append("片段: ").append(uri.getFragment()).append("\n"); + sb.append("用户: ").append(uri.getUserInfo()).append("\n"); + output.setText(sb.toString()); + } catch (Exception ex) { showError("无效的 URL"); } + }), + copyBtn(output) + )); + + return panel; + } + + // ==================== Regex ==================== + + private JPanel createRegexPanel() { + JPanel panel = paddedPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + + JLabel title = new JLabel("正则表达式测试"); + title.setFont(TITLE_FONT); + panel.add(title); + panel.add(Box.createVerticalStrut(4)); + JLabel desc = new JLabel("测试正则表达式的匹配、替换和分割"); + desc.setForeground(SUBTITLE); + panel.add(desc); + panel.add(Box.createVerticalStrut(12)); + + JBTextField regexField = new JBTextField(); + regexField.getEmptyText().setText("正则表达式..."); + regexField.setFont(MONO_FONT); + panel.add(regexField); + panel.add(Box.createVerticalStrut(6)); + + JPanel flagRow = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 0)); + JComboBox flagCombo = new JComboBox<>(new String[]{"无", "忽略大小写", "多行模式", "点号匹配换行"}); + flagRow.add(new JLabel("模式:")); + flagRow.add(flagCombo); + flagRow.setMaximumSize(new Dimension(Integer.MAX_VALUE, 32)); + panel.add(flagRow); + panel.add(Box.createVerticalStrut(6)); + + JTextArea input = createInputArea("输入测试文本..."); + JTextArea output = createOutputArea("匹配结果..."); + panel.add(ioPanel(input, output)); + panel.add(Box.createVerticalStrut(8)); + + panel.add(btnRow( + toolBtn("匹配", "查找所有匹配", e -> { + try { + Pattern p = compilePattern(regexField.getText(), flagCombo.getSelectedIndex()); + java.util.regex.Matcher m = p.matcher(input.getText()); + StringBuilder sb = new StringBuilder(); + int count = 0; + while (m.find()) { + count++; + sb.append("[").append(count).append("] ").append(m.group()).append("\n"); + } + if (count == 0) sb.append("无匹配"); + else sb.insert(0, "共 " + count + " 个匹配:\n"); + output.setText(sb.toString()); + } catch (PatternSyntaxException ex) { showError("正则语法错误: " + ex.getMessage()); } + }), + toolBtn("替换", "替换匹配文本", e -> { + String replace = JOptionPane.showInputDialog("替换为:"); + if (replace == null) return; + try { + Pattern p = compilePattern(regexField.getText(), flagCombo.getSelectedIndex()); + output.setText(p.matcher(input.getText()).replaceAll(replace)); + } catch (PatternSyntaxException ex) { showError("正则语法错误"); } + }), + toolBtn("分割", "按正则分割", e -> { + try { + Pattern p = compilePattern(regexField.getText(), flagCombo.getSelectedIndex()); + String[] parts = p.split(input.getText()); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < parts.length; i++) { + sb.append("[").append(i).append("] ").append(parts[i]).append("\n"); + } + output.setText(sb.toString()); + } catch (PatternSyntaxException ex) { showError("正则语法错误"); } + }), + copyBtn(output), + toolBtn("清空", "清空所有", e -> { input.setText(""); output.setText(""); regexField.setText(""); }) + )); + + return panel; + } + + private static Pattern compilePattern(String regex, int flagIdx) { + int flags = 0; + if (flagIdx == 1) flags |= Pattern.CASE_INSENSITIVE; + else if (flagIdx == 2) flags |= Pattern.MULTILINE; + else if (flagIdx == 3) flags |= Pattern.DOTALL; + return flags == 0 ? Pattern.compile(regex) : Pattern.compile(regex, flags); + } + + // ==================== Diff ==================== + + private JPanel createDiffPanel() { + JPanel panel = paddedPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + + JLabel title = new JLabel("文本对比"); + title.setFont(TITLE_FONT); + panel.add(title); + panel.add(Box.createVerticalStrut(4)); + JLabel desc = new JLabel("对比两段文本的差异"); + desc.setForeground(SUBTITLE); + panel.add(desc); + panel.add(Box.createVerticalStrut(12)); + + JTextArea leftInput = createInputArea("文本 A..."); + JTextArea rightInput = createInputArea("文本 B..."); + JPanel ioP = new JPanel(new GridLayout(1, 2, 6, 0)); + ioP.add(wrapScroll(leftInput, "文本 A")); + ioP.add(wrapScroll(rightInput, "文本 B")); + panel.add(ioP); + panel.add(Box.createVerticalStrut(8)); + + JTextArea output = createOutputArea("差异结果..."); + JScrollPane sp = new JBScrollPane(output); + sp.setPreferredSize(new Dimension(200, 70)); + panel.add(sp); + panel.add(Box.createVerticalStrut(8)); + + panel.add(btnRow( + toolBtn("对比", "对比两段文本差异", e -> { + String a = leftInput.getText(); + String b = rightInput.getText(); + if (a.isEmpty() && b.isEmpty()) return; + String[] linesA = a.split("\n", -1); + String[] linesB = b.split("\n", -1); + int max = Math.max(linesA.length, linesB.length); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < max; i++) { + String la = i < linesA.length ? linesA[i] : ""; + String lb = i < linesB.length ? linesB[i] : ""; + if (!la.equals(lb)) { + sb.append("- ").append(la).append("\n"); + sb.append("+ ").append(lb).append("\n"); + } else { + sb.append(" ").append(la).append("\n"); + } + } + output.setText(sb.toString()); + }), + toolBtn("交换", "交换文本 A 和 B", e -> { + String t = leftInput.getText(); + leftInput.setText(rightInput.getText()); + rightInput.setText(t); + }), + copyBtn(output), + toolBtn("清空", "清空所有", e -> { leftInput.setText(""); rightInput.setText(""); output.setText(""); }) + )); + + return panel; + } + + // ==================== Text Stats ==================== + + private JPanel createTextStatsPanel() { + JPanel panel = paddedPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + + JLabel title = new JLabel("文本统计"); + title.setFont(TITLE_FONT); + panel.add(title); + panel.add(Box.createVerticalStrut(4)); + JLabel desc = new JLabel("统计文本的字数、行数、字符数等信息"); + desc.setForeground(SUBTITLE); + panel.add(desc); + panel.add(Box.createVerticalStrut(12)); + + JTextArea input = createInputArea("输入文本..."); + JTextArea output = createOutputArea("统计结果..."); + panel.add(ioPanel(input, output)); + panel.add(Box.createVerticalStrut(8)); + + panel.add(btnRow( + toolBtn("统计", "统计文本信息", e -> { + String t = input.getText(); + if (t.isEmpty()) { output.setText("文本为空"); return; } + int charCount = t.length(); + int charNoSpace = t.replace(" ", "").replace("\n", "").replace("\r", "").replace("\t", "").length(); + int wordCount = t.isEmpty() ? 0 : t.split("[\\s,;.?!\\u3002\\uff1b\\uff0c\\uff01\\uff1f]+").length; + int lineCount = t.isEmpty() ? 0 : t.split("\n", -1).length; + int chineseCount = 0; + int letterCount = 0; + int digitCount = 0; + int spaceCount = 0; + int punctCount = 0; + for (char c : t.toCharArray()) { + if (c >= 0x4E00 && c <= 0x9FFF) chineseCount++; + else if (Character.isLetter(c)) letterCount++; + else if (Character.isDigit(c)) digitCount++; + else if (Character.isWhitespace(c)) spaceCount++; + else punctCount++; + } + StringBuilder sb = new StringBuilder(); + sb.append("总字符数: ").append(charCount).append("\n"); + sb.append("非空白字符: ").append(charNoSpace).append("\n"); + sb.append("汉字数: ").append(chineseCount).append("\n"); + sb.append("英文字母: ").append(letterCount).append("\n"); + sb.append("数字: ").append(digitCount).append("\n"); + sb.append("空白字符: ").append(spaceCount).append("\n"); + sb.append("标点符号: ").append(punctCount).append("\n"); + sb.append("单词数: ").append(wordCount).append("\n"); + sb.append("行数: ").append(lineCount).append("\n"); + sb.append("段落数: ").append(t.split("\n\\s*\n", -1).length).append("\n"); + output.setText(sb.toString()); + }), + copyBtn(output), + toolBtn("清空", "清空所有", e -> { input.setText(""); output.setText(""); }) + )); + + return panel; + } + + // ==================== Case ==================== + + private JPanel createCasePanel() { + JPanel panel = paddedPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + + JLabel title = new JLabel("大小写转换"); + title.setFont(TITLE_FONT); + panel.add(title); + panel.add(Box.createVerticalStrut(4)); + JLabel desc = new JLabel("在大小写格式之间互相转换"); + desc.setForeground(SUBTITLE); + panel.add(desc); + panel.add(Box.createVerticalStrut(12)); + + JTextArea input = createInputArea("输入文本..."); + JTextArea output = createOutputArea("转换结果..."); + panel.add(ioPanel(input, output)); + panel.add(Box.createVerticalStrut(8)); + + panel.add(btnRow( + toolBtn("大写", "全部转为大写", e -> output.setText(input.getText().toUpperCase())), + toolBtn("小写", "全部转为小写", e -> output.setText(input.getText().toLowerCase())), + toolBtn("首字母大写", "每个单词首字母大写", e -> { + String t = input.getText(); + StringBuilder sb = new StringBuilder(); + boolean nextUpper = true; + for (char c : t.toCharArray()) { + if (Character.isWhitespace(c)) { + sb.append(c); + nextUpper = true; + } else if (nextUpper) { + sb.append(Character.toUpperCase(c)); + nextUpper = false; + } else { + sb.append(c); + } + } + output.setText(sb.toString()); + }), + toolBtn("驼峰转下划线", "驼峰命名转下划线", e -> { + String t = input.getText(); + StringBuilder sb = new StringBuilder(); + for (char c : t.toCharArray()) { + if (Character.isUpperCase(c)) { + sb.append('_').append(Character.toLowerCase(c)); + } else { + sb.append(c); + } + } + output.setText(sb.toString()); + }), + toolBtn("下划线转驼峰", "下划线转驼峰命名", e -> { + String t = input.getText(); + StringBuilder sb = new StringBuilder(); + boolean nextUpper = false; + for (char c : t.toCharArray()) { + if (c == '_') { + nextUpper = true; + } else if (nextUpper) { + sb.append(Character.toUpperCase(c)); + nextUpper = false; + } else { + sb.append(c); + } + } + output.setText(sb.toString()); + }), + copyBtn(output), + toolBtn("清空", "清空所有", e -> { input.setText(""); output.setText(""); }) + )); + + return panel; + } + + // ==================== Line Ending ==================== + + private JPanel createLineEndingPanel() { + JPanel panel = paddedPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + + JLabel title = new JLabel("换行符转换"); + title.setFont(TITLE_FONT); + panel.add(title); + panel.add(Box.createVerticalStrut(4)); + JLabel desc = new JLabel("在不同换行符格式之间转换 (CR / LF / CRLF)"); + desc.setForeground(SUBTITLE); + panel.add(desc); + panel.add(Box.createVerticalStrut(12)); + + JTextArea input = createInputArea("输入文本..."); + JTextArea output = createOutputArea("转换结果..."); + panel.add(ioPanel(input, output)); + panel.add(Box.createVerticalStrut(8)); + + panel.add(btnRow( + toolBtn("CRLF", "转为 Windows 换行符 \\r\\n", e -> output.setText(input.getText().replace("\r\n", "\n").replace("\r", "\n").replace("\n", "\r\n"))), + toolBtn("LF", "转为 Unix 换行符 \\n", e -> output.setText(input.getText().replace("\r\n", "\n").replace("\r", "\n"))), + toolBtn("CR", "转为 Mac 换行符 \\r", e -> output.setText(input.getText().replace("\r\n", "\n").replace("\n", "\r"))), + copyBtn(output), + toolBtn("清空", "清空所有", e -> { input.setText(""); output.setText(""); }) + )); + + return panel; + } + + // ==================== List Tools ==================== + + private JPanel createListToolsPanel() { + JPanel panel = paddedPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + + JLabel title = new JLabel("列表工具"); + title.setFont(TITLE_FONT); + panel.add(title); + panel.add(Box.createVerticalStrut(4)); + JLabel desc = new JLabel("对列表进行排序、去重、反转等操作(每行一个元素)"); + desc.setForeground(SUBTITLE); + panel.add(desc); + panel.add(Box.createVerticalStrut(12)); + + JTextArea input = createInputArea("输入列表(每行一个元素)..."); + JTextArea output = createOutputArea("处理结果..."); + panel.add(ioPanel(input, output)); + panel.add(Box.createVerticalStrut(8)); + + panel.add(btnRow( + toolBtn("排序", "按字母顺序排序", e -> { + List lines = new ArrayList<>(Arrays.asList(input.getText().split("\n", -1))); + lines.sort(String::compareTo); + output.setText(String.join("\n", lines)); + }), + toolBtn("去重", "去除重复行", e -> { + List lines = Arrays.asList(input.getText().split("\n", -1)); + output.setText(lines.stream().distinct().collect(Collectors.joining("\n"))); + }), + toolBtn("反转", "反转行顺序", e -> { + List lines = new ArrayList<>(Arrays.asList(input.getText().split("\n", -1))); + java.util.Collections.reverse(lines); + output.setText(String.join("\n", lines)); + }), + toolBtn("去除空行", "去除所有空行", e -> { + List lines = Arrays.asList(input.getText().split("\n", -1)); + output.setText(lines.stream().filter(l -> !l.trim().isEmpty()).collect(Collectors.joining("\n"))); + }), + toolBtn("添加行号", "行号", e -> { + String[] lines = input.getText().split("\n", -1); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < lines.length; i++) { + sb.append(String.format("%4d %s\n", i + 1, lines[i])); + } + output.setText(sb.toString()); + }), + copyBtn(output), + toolBtn("清空", "清空所有", e -> { input.setText(""); output.setText(""); }) + )); + + return panel; + } + + // ==================== Hash ==================== + + private JPanel createHashPanel() { + JPanel panel = paddedPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + + JLabel title = new JLabel("哈希计算"); + title.setFont(TITLE_FONT); + panel.add(title); + panel.add(Box.createVerticalStrut(4)); + JLabel desc = new JLabel("计算文本的 MD5、SHA-1、SHA-256 等哈希值"); + desc.setForeground(SUBTITLE); + panel.add(desc); + panel.add(Box.createVerticalStrut(12)); + + JTextArea input = createInputArea("输入文本..."); + JTextArea output = createOutputArea("哈希结果..."); + panel.add(ioPanel(input, output)); + panel.add(Box.createVerticalStrut(8)); + + panel.add(btnRow( + toolBtn("MD5", "计算 MD5 哈希", e -> { + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] digest = md.digest(input.getText().getBytes(StandardCharsets.UTF_8)); + StringBuilder sb = new StringBuilder(); + for (byte b : digest) sb.append(String.format("%02x", b)); + output.setText("MD5: " + sb.toString().toUpperCase() + "\nmd5: " + sb); + } catch (Exception ex) { showError("计算失败"); } + }), + toolBtn("SHA-1", "计算 SHA-1 哈希", e -> { + try { + MessageDigest md = MessageDigest.getInstance("SHA-1"); + byte[] digest = md.digest(input.getText().getBytes(StandardCharsets.UTF_8)); + StringBuilder sb = new StringBuilder(); + for (byte b : digest) sb.append(String.format("%02x", b)); + output.setText("SHA-1: " + sb.toString().toUpperCase() + "\nsha-1: " + sb); + } catch (Exception ex) { showError("计算失败"); } + }), + toolBtn("SHA-256", "计算 SHA-256 哈希", e -> { + try { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + byte[] digest = md.digest(input.getText().getBytes(StandardCharsets.UTF_8)); + StringBuilder sb = new StringBuilder(); + for (byte b : digest) sb.append(String.format("%02x", b)); + output.setText("SHA-256: " + sb.toString().toUpperCase() + "\nsha-256: " + sb); + } catch (Exception ex) { showError("计算失败"); } + }), + toolBtn("SHA-512", "计算 SHA-512 哈希", e -> { + try { + MessageDigest md = MessageDigest.getInstance("SHA-512"); + byte[] digest = md.digest(input.getText().getBytes(StandardCharsets.UTF_8)); + StringBuilder sb = new StringBuilder(); + for (byte b : digest) sb.append(String.format("%02x", b)); + output.setText("SHA-512: " + sb.toString().toUpperCase() + "\nsha-512: " + sb); + } catch (Exception ex) { showError("计算失败"); } + }), + copyBtn(output), + toolBtn("清空", "清空所有", e -> { input.setText(""); output.setText(""); }) + )); + + return panel; + } + + // ==================== Base Convert ==================== + + private JPanel createBaseConvertPanel() { + JPanel panel = paddedPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + + JLabel title = new JLabel("进制转换"); + title.setFont(TITLE_FONT); + panel.add(title); + panel.add(Box.createVerticalStrut(4)); + JLabel desc = new JLabel("在二进制、八进制、十进制、十六进制之间转换"); + desc.setForeground(SUBTITLE); + panel.add(desc); + panel.add(Box.createVerticalStrut(12)); + + JBTextField input = new JBTextField(); + input.getEmptyText().setText("输入数值..."); + input.setFont(MONO_FONT); + panel.add(input); + panel.add(Box.createVerticalStrut(6)); + + JPanel radioRow = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 0)); + JComboBox fromBase = new JComboBox<>(new String[]{"二进制", "八进制", "十进制", "十六进制"}); + JComboBox toBase = new JComboBox<>(new String[]{"十进制", "二进制", "八进制", "十六进制"}); + radioRow.add(new JLabel("从:")); + radioRow.add(fromBase); + radioRow.add(new JLabel(" 到:")); + radioRow.add(toBase); + radioRow.setMaximumSize(new Dimension(Integer.MAX_VALUE, 32)); + panel.add(radioRow); + panel.add(Box.createVerticalStrut(8)); + + JTextArea output = createOutputArea("转换结果..."); + JScrollPane sp = new JBScrollPane(output); + sp.setPreferredSize(new Dimension(200, 70)); + panel.add(sp); + panel.add(Box.createVerticalStrut(8)); + + panel.add(btnRow( + toolBtn("转换", "进制转换", e -> { + try { + String text = input.getText().trim(); + if (text.isEmpty()) return; + int from = fromBase.getSelectedIndex(); + int radix = from == 0 ? 2 : from == 1 ? 8 : from == 2 ? 10 : 16; + long value = Long.parseLong(text.replaceAll("[^0-9a-fA-F]", ""), radix); + int to = toBase.getSelectedIndex(); + String result; + switch (to) { + case 0: result = Long.toBinaryString(value); break; + case 1: result = Long.toOctalString(value); break; + case 2: result = Long.toString(value); break; + case 3: result = Long.toHexString(value).toUpperCase(); break; + default: result = Long.toString(value); + } + StringBuilder sb = new StringBuilder(); + sb.append("十进制: ").append(value).append("\n"); + sb.append("二进制: ").append(Long.toBinaryString(value)).append("\n"); + sb.append("八进制: ").append(Long.toOctalString(value)).append("\n"); + sb.append("十六进制: ").append(Long.toHexString(value).toUpperCase()).append("\n"); + output.setText(sb.toString()); + } catch (Exception ex) { showError("无效的数值"); } + }), + copyBtn(output), + toolBtn("清空", "清空所有", e -> { input.setText(""); output.setText(""); }) + )); + + return panel; + } + + // ==================== Color ==================== + + private JPanel createColorPanel() { + JPanel panel = paddedPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + + JLabel title = new JLabel("颜色转换"); + title.setFont(TITLE_FONT); + panel.add(title); + panel.add(Box.createVerticalStrut(4)); + JLabel desc = new JLabel("在 HEX、RGB、HSL 等颜色格式之间转换"); + desc.setForeground(SUBTITLE); + panel.add(desc); + panel.add(Box.createVerticalStrut(12)); + + JBTextField input = new JBTextField(); + input.getEmptyText().setText("#FF0000 或 rgb(255,0,0)"); + input.setFont(MONO_FONT); + panel.add(input); + panel.add(Box.createVerticalStrut(8)); + + JTextArea output = createOutputArea("转换结果..."); + JScrollPane sp = new JBScrollPane(output); + sp.setPreferredSize(new Dimension(200, 80)); + panel.add(sp); + panel.add(Box.createVerticalStrut(8)); + + JPanel previewPanel = new JPanel(); + previewPanel.setPreferredSize(new Dimension(100, 40)); + previewPanel.setBorder(BorderFactory.createLineBorder(JBColor.border())); + previewPanel.setBackground(Color.WHITE); + panel.add(previewPanel); + panel.add(Box.createVerticalStrut(8)); + + panel.add(btnRow( + toolBtn("转换", "颜色格式转换", e -> { + try { + String text = input.getText().trim(); + Color c = null; + if (text.startsWith("#")) { + c = Color.decode(text); + } else if (text.startsWith("rgb")) { + String[] parts = text.replaceAll("[^0-9,]", "").split(","); + c = new Color(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]), Integer.parseInt(parts[2])); + } + if (c == null) { showError("不支持的颜色格式"); return; } + previewPanel.setBackground(c); + StringBuilder sb = new StringBuilder(); + sb.append("HEX: #").append(String.format("%02X%02X%02X", c.getRed(), c.getGreen(), c.getBlue())).append("\n"); + sb.append("RGB: rgb(").append(c.getRed()).append(", ").append(c.getGreen()).append(", ").append(c.getBlue()).append(")\n"); + float[] hsb = Color.RGBtoHSB(c.getRed(), c.getGreen(), c.getBlue(), null); + sb.append("HSL: hsl(").append(Math.round(hsb[0] * 360)).append(", ").append(Math.round(hsb[1] * 100)).append("%, ").append(Math.round(hsb[2] * 100)).append("%)\n"); + sb.append("CMYK: cmyk(").append(String.format("%.1f%%", (1 - c.getRed()/255.0) * 100)).append(", ") + .append(String.format("%.1f%%", (1 - c.getGreen()/255.0) * 100)).append(", ") + .append(String.format("%.1f%%", (1 - c.getBlue()/255.0) * 100)).append(", 0%)\n"); + output.setText(sb.toString()); + } catch (Exception ex) { showError("颜色格式无效"); } + }), + copyBtn(output), + toolBtn("清空", "清空所有", e -> { input.setText(""); output.setText(""); previewPanel.setBackground(Color.WHITE); }) + )); + + return panel; + } + + // ==================== JWT ==================== + + private JPanel createJwtPanel() { + JPanel panel = paddedPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + + JLabel title = new JLabel("JWT 解码"); + title.setFont(TITLE_FONT); + panel.add(title); + panel.add(Box.createVerticalStrut(4)); + JLabel desc = new JLabel("解码 JWT Token,查看 Header 和 Payload"); + desc.setForeground(SUBTITLE); + panel.add(desc); + panel.add(Box.createVerticalStrut(12)); + + JTextArea input = createInputArea("输入 JWT Token..."); + JTextArea output = createOutputArea("解码结果..."); + panel.add(ioPanel(input, output)); + panel.add(Box.createVerticalStrut(8)); + + panel.add(btnRow( + toolBtn("解码", "解码 JWT", e -> { + String t = input.getText().trim(); + if (t.isEmpty()) return; + String[] parts = t.split("\\."); + if (parts.length < 2) { showError("无效的 JWT 格式"); return; } + try { + StringBuilder sb = new StringBuilder(); + sb.append("=== Header ===\n"); + sb.append(new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8)); + sb.append("\n\n=== Payload ===\n"); + sb.append(new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8)); + if (parts.length > 2) { + sb.append("\n\n=== Signature ===\n"); + sb.append(parts[2]); + } + output.setText(sb.toString()); + } catch (Exception ex) { showError("JWT 解码失败"); } + }), + copyBtn(output), + toolBtn("清空", "清空所有", e -> { input.setText(""); output.setText(""); }) + )); + + return panel; + } + + // ==================== Endian ==================== + + private JPanel createEndianPanel() { + JPanel panel = paddedPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + + JLabel title = new JLabel("大小端转换"); + title.setFont(TITLE_FONT); + panel.add(title); + panel.add(Box.createVerticalStrut(4)); + JLabel desc = new JLabel("在 Big Endian 和 Little Endian 之间转换十六进制数据"); + desc.setForeground(SUBTITLE); + panel.add(desc); + panel.add(Box.createVerticalStrut(12)); + + JBTextField input = new JBTextField(); + input.getEmptyText().setText("输入十六进制,如: 0A1B2C3D"); + input.setFont(MONO_FONT); + panel.add(input); + panel.add(Box.createVerticalStrut(8)); + + JTextArea output = createOutputArea("转换结果..."); + JScrollPane sp = new JBScrollPane(output); + sp.setPreferredSize(new Dimension(200, 70)); + panel.add(sp); + panel.add(Box.createVerticalStrut(8)); + + panel.add(btnRow( + toolBtn("BE -> LE", "Big Endian 转 Little Endian", e -> { + String hex = input.getText().trim().replaceAll("[^0-9a-fA-F]", ""); + if (hex.length() % 2 != 0) { showError("十六进制长度必须为偶数"); return; } + StringBuilder sb = new StringBuilder(); + for (int i = hex.length() - 2; i >= 0; i -= 2) { + sb.append(hex, i, i + 2); + } + output.setText("原始 (BE): " + hex.toUpperCase() + "\n转换 (LE): " + sb.toString().toUpperCase()); + }), + toolBtn("LE -> BE", "Little Endian 转 Big Endian", e -> { + String hex = input.getText().trim().replaceAll("[^0-9a-fA-F]", ""); + if (hex.length() % 2 != 0) { showError("十六进制长度必须为偶数"); return; } + StringBuilder sb = new StringBuilder(); + for (int i = hex.length() - 2; i >= 0; i -= 2) { + sb.append(hex, i, i + 2); + } + output.setText("原始 (LE): " + hex.toUpperCase() + "\n转换 (BE): " + sb.toString().toUpperCase()); + }), + copyBtn(output), + toolBtn("清空", "清空所有", e -> { input.setText(""); output.setText(""); }) + )); + + return panel; + } + + // ==================== File Size ==================== + + private JPanel createFileSizePanel() { + JPanel panel = paddedPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + + JLabel title = new JLabel("文件大小转换"); + title.setFont(TITLE_FONT); + panel.add(title); + panel.add(Box.createVerticalStrut(4)); + JLabel desc = new JLabel("在 B / KB / MB / GB / TB 之间转换"); + desc.setForeground(SUBTITLE); + panel.add(desc); + panel.add(Box.createVerticalStrut(12)); + + JBTextField input = new JBTextField(); + input.getEmptyText().setText("1024"); + input.setFont(MONO_FONT); + panel.add(input); + panel.add(Box.createVerticalStrut(6)); + + JPanel unitRow = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 0)); + JComboBox unitCombo = new JComboBox<>(new String[]{"B", "KB", "MB", "GB", "TB"}); + unitRow.add(new JLabel("单位:")); + unitRow.add(unitCombo); + unitRow.setMaximumSize(new Dimension(Integer.MAX_VALUE, 32)); + panel.add(unitRow); + panel.add(Box.createVerticalStrut(8)); + + JTextArea output = createOutputArea("转换结果..."); + JScrollPane sp = new JBScrollPane(output); + sp.setPreferredSize(new Dimension(200, 70)); + panel.add(sp); + panel.add(Box.createVerticalStrut(8)); + + panel.add(btnRow( + toolBtn("转换", "文件大小转换", e -> { + try { + double value = Double.parseDouble(input.getText().trim()); + int unit = unitCombo.getSelectedIndex(); + double bytes = value * Math.pow(1024, unit); + StringBuilder sb = new StringBuilder(); + sb.append("B: ").append(String.format("%.0f", bytes)).append("\n"); + sb.append("KB: ").append(String.format("%.2f", bytes / 1024)).append("\n"); + sb.append("MB: ").append(String.format("%.2f", bytes / (1024*1024))).append("\n"); + sb.append("GB: ").append(String.format("%.2f", bytes / (1024*1024*1024))).append("\n"); + sb.append("TB: ").append(String.format("%.4f", bytes / (1024.0*1024*1024*1024))).append("\n"); + output.setText(sb.toString()); + } catch (NumberFormatException ex) { showError("请输入有效的数字"); } + }), + copyBtn(output), + toolBtn("清空", "清空所有", e -> { input.setText(""); output.setText(""); }) + )); + + return panel; + } + + // ==================== Chmod ==================== + + private JPanel createChmodPanel() { + JPanel panel = paddedPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + + JLabel title = new JLabel("Unix 权限计算"); + title.setFont(TITLE_FONT); + panel.add(title); + panel.add(Box.createVerticalStrut(4)); + JLabel desc = new JLabel("在数字权限和符号权限之间转换 (chmod)"); + desc.setForeground(SUBTITLE); + panel.add(desc); + panel.add(Box.createVerticalStrut(12)); + + JBTextField input = new JBTextField(); + input.getEmptyText().setText("755 或 u+rwx,g+rx,o+rx"); + input.setFont(MONO_FONT); + panel.add(input); + panel.add(Box.createVerticalStrut(8)); + + JTextArea output = createOutputArea("转换结果..."); + JScrollPane sp = new JBScrollPane(output); + sp.setPreferredSize(new Dimension(200, 80)); + panel.add(sp); + panel.add(Box.createVerticalStrut(8)); + + panel.add(btnRow( + toolBtn("数字转符号", "数字权限转符号表示", e -> { + String t = input.getText().trim(); + try { + int perm = Integer.parseInt(t, 8); + StringBuilder sb = new StringBuilder(); + sb.append("数字: ").append(t).append("\n"); + sb.append("符号: ").append(permToSymbol(perm)).append("\n"); + sb.append("所有者: ").append(t.charAt(0) == '7' ? "读+写+执行" : t.charAt(0) == '6' ? "读+写" : t.charAt(0) == '5' ? "读+执行" : t.charAt(0) == '4' ? "只读" : "无权限").append("\n"); + sb.append("用户组: ").append(t.charAt(1) == '7' ? "读+写+执行" : t.charAt(1) == '6' ? "读+写" : t.charAt(1) == '5' ? "读+执行" : t.charAt(1) == '4' ? "只读" : "无权限").append("\n"); + sb.append("其他: ").append(t.charAt(2) == '7' ? "读+写+执行" : t.charAt(2) == '6' ? "读+写" : t.charAt(2) == '5' ? "读+执行" : t.charAt(2) == '4' ? "只读" : "无权限").append("\n"); + output.setText(sb.toString()); + } catch (Exception ex) { showError("无效的数字权限"); } + }), + toolBtn("符号转数字", "符号权限转数字表示", e -> { + String t = input.getText().trim().toLowerCase(); + int perm = 0; + if (t.contains("u")) { + if (t.contains("r")) perm |= 0x100; + if (t.contains("w")) perm |= 0x080; + if (t.contains("x")) perm |= 0x040; + } + if (t.contains("g")) { + if (t.contains("r")) perm |= 0x020; + if (t.contains("w")) perm |= 0x010; + if (t.contains("x")) perm |= 0x008; + } + if (t.contains("o")) { + if (t.contains("r")) perm |= 0x004; + if (t.contains("w")) perm |= 0x002; + if (t.contains("x")) perm |= 0x001; + } + output.setText("符号: " + t + "\n数字: " + Integer.toOctalString(perm)); + }), + copyBtn(output), + toolBtn("清空", "清空所有", e -> { input.setText(""); output.setText(""); }) + )); + + return panel; + } + + private static String permToSymbol(int perm) { + StringBuilder sb = new StringBuilder(); + sb.append((perm & 0x100) != 0 ? 'r' : '-'); + sb.append((perm & 0x080) != 0 ? 'w' : '-'); + sb.append((perm & 0x040) != 0 ? 'x' : '-'); + sb.append((perm & 0x020) != 0 ? 'r' : '-'); + sb.append((perm & 0x010) != 0 ? 'w' : '-'); + sb.append((perm & 0x008) != 0 ? 'x' : '-'); + sb.append((perm & 0x004) != 0 ? 'r' : '-'); + sb.append((perm & 0x002) != 0 ? 'w' : '-'); + sb.append((perm & 0x001) != 0 ? 'x' : '-'); + return sb.toString(); + } + + // ==================== CSV/JSON ==================== + + private JPanel createCsvJsonPanel() { + JPanel panel = paddedPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + + JLabel title = new JLabel("CSV / JSON 转换"); + title.setFont(TITLE_FONT); + panel.add(title); + panel.add(Box.createVerticalStrut(4)); + JLabel desc = new JLabel("在 CSV 和 JSON 格式之间互相转换"); + desc.setForeground(SUBTITLE); + panel.add(desc); + panel.add(Box.createVerticalStrut(12)); + + JTextArea input = createInputArea("输入 CSV 或 JSON..."); + JTextArea output = createOutputArea("转换结果..."); + panel.add(ioPanel(input, output)); + panel.add(Box.createVerticalStrut(8)); + + panel.add(btnRow( + toolBtn("CSV -> JSON", "CSV 转 JSON", e -> { + String t = input.getText().trim(); + if (t.isEmpty()) return; + String[] lines = t.split("\n"); + if (lines.length < 2) { showError("CSV 需要至少标题行和一行数据"); return; } + String[] headers = lines[0].split(","); + for (int i = 0; i < headers.length; i++) headers[i] = headers[i].trim(); + StringBuilder sb = new StringBuilder("[\n"); + for (int i = 1; i < lines.length; i++) { + String[] values = lines[i].split(","); + sb.append(" {"); + for (int j = 0; j < headers.length && j < values.length; j++) { + if (j > 0) sb.append(", "); + sb.append("\"").append(headers[j]).append("\": \"").append(values[j].trim()).append("\""); + } + sb.append("}"); + if (i < lines.length - 1) sb.append(","); + sb.append("\n"); + } + sb.append("]"); + output.setText(sb.toString()); + }), + toolBtn("JSON -> CSV", "JSON 转 CSV", e -> { + String t = input.getText().trim(); + try { + com.google.gson.JsonArray arr = com.google.gson.JsonParser.parseString(t).getAsJsonArray(); + if (arr.size() == 0) return; + java.util.Set keys = new java.util.LinkedHashSet<>(); + for (int i = 0; i < arr.size(); i++) { + com.google.gson.JsonObject obj = arr.get(i).getAsJsonObject(); + keys.addAll(obj.keySet()); + } + StringBuilder sb = new StringBuilder(); + sb.append(String.join(",", keys)).append("\n"); + for (int i = 0; i < arr.size(); i++) { + com.google.gson.JsonObject obj = arr.get(i).getAsJsonObject(); + List row = new ArrayList<>(); + for (String key : keys) { + com.google.gson.JsonElement el = obj.get(key); + row.add(el != null ? el.getAsString() : ""); + } + sb.append(String.join(",", row)).append("\n"); + } + output.setText(sb.toString()); + } catch (Exception ex) { showError("JSON 解析失败: " + ex.getMessage()); } + }), + copyBtn(output), + toolBtn("清空", "清空所有", e -> { input.setText(""); output.setText(""); }) + )); + + return panel; + } + + // ==================== ASCII ==================== + + private JPanel createAsciiPanel() { + JPanel panel = paddedPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + + JLabel title = new JLabel("ASCII 码表"); + title.setFont(TITLE_FONT); + panel.add(title); + panel.add(Box.createVerticalStrut(4)); + JLabel desc = new JLabel("查看 ASCII 字符编码对照表"); + desc.setForeground(SUBTITLE); + panel.add(desc); + panel.add(Box.createVerticalStrut(12)); + + JTextArea output = createOutputArea("ASCII 码表..."); + output.setEditable(false); + JScrollPane sp = new JBScrollPane(output); + sp.setPreferredSize(new Dimension(200, 300)); + panel.add(sp); + panel.add(Box.createVerticalStrut(8)); + + panel.add(btnRow( + toolBtn("显示码表", "显示 ASCII 码表", e -> { + StringBuilder sb = new StringBuilder(); + sb.append("Dec Hex Char\n"); + sb.append("--- --- ----\n"); + for (int i = 0; i < 128; i++) { + String ch = i < 32 ? "控制字符" : i == 32 ? "空格" : i == 127 ? "DEL" : String.valueOf((char) i); + sb.append(String.format("%3d %3s %s\n", i, Integer.toHexString(i).toUpperCase(), ch)); + } + output.setText(sb.toString()); + }), + toolBtn("字符转 ASCII", "输入字符查看 ASCII 码", e -> { + String t = javax.swing.JOptionPane.showInputDialog("输入字符:"); + if (t != null && !t.isEmpty()) { + StringBuilder sb = new StringBuilder(); + for (char c : t.toCharArray()) { + sb.append("'").append(c).append("' -> Dec: ").append((int) c) + .append(" Hex: ").append(Integer.toHexString(c).toUpperCase()).append("\n"); + } + output.setText(sb.toString()); + } + }), + copyBtn(output) + )); + + return panel; + } + + // ==================== HTTP Status ==================== + + private JPanel createHttpStatusPanel() { + JPanel panel = paddedPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + + JLabel title = new JLabel("HTTP 状态码"); + title.setFont(TITLE_FONT); + panel.add(title); + panel.add(Box.createVerticalStrut(4)); + JLabel desc = new JLabel("查询 HTTP 状态码的含义"); + desc.setForeground(SUBTITLE); + panel.add(desc); + panel.add(Box.createVerticalStrut(12)); + + JBTextField codeField = new JBTextField(); + codeField.getEmptyText().setText("输入状态码,如 404"); + codeField.setFont(MONO_FONT); + panel.add(codeField); + panel.add(Box.createVerticalStrut(8)); + + JTextArea output = createOutputArea("查询结果..."); + JScrollPane sp = new JBScrollPane(output); + sp.setPreferredSize(new Dimension(200, 200)); + panel.add(sp); + panel.add(Box.createVerticalStrut(8)); + + Map statusMap = new LinkedHashMap<>(); + statusMap.put("100", "Continue - 继续"); + statusMap.put("101", "Switching Protocols - 切换协议"); + statusMap.put("200", "OK - 请求成功"); + statusMap.put("201", "Created - 已创建"); + statusMap.put("204", "No Content - 无内容"); + statusMap.put("301", "Moved Permanently - 永久重定向"); + statusMap.put("302", "Found - 临时重定向"); + statusMap.put("304", "Not Modified - 未修改"); + statusMap.put("400", "Bad Request - 错误请求"); + statusMap.put("401", "Unauthorized - 未授权"); + statusMap.put("403", "Forbidden - 禁止访问"); + statusMap.put("404", "Not Found - 未找到"); + statusMap.put("405", "Method Not Allowed - 方法不允许"); + statusMap.put("408", "Request Timeout - 请求超时"); + statusMap.put("429", "Too Many Requests - 请求过多"); + statusMap.put("500", "Internal Server Error - 服务器内部错误"); + statusMap.put("502", "Bad Gateway - 网关错误"); + statusMap.put("503", "Service Unavailable - 服务不可用"); + statusMap.put("504", "Gateway Timeout - 网关超时"); + + panel.add(btnRow( + toolBtn("查询", "查询状态码", e -> { + String code = codeField.getText().trim(); + String descText = statusMap.get(code); + if (descText != null) { + output.setText(code + " - " + descText); + } else { + output.setText("未知状态码: " + code); + } + }), + toolBtn("全部显示", "显示所有常见状态码", e -> { + StringBuilder sb = new StringBuilder(); + for (Map.Entry entry : statusMap.entrySet()) { + sb.append(entry.getKey()).append(" - ").append(entry.getValue()).append("\n"); + } + output.setText(sb.toString()); + }), + copyBtn(output) + )); + + return panel; + } + + // ==================== MIME ==================== + + private JPanel createMimePanel() { + JPanel panel = paddedPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + + JLabel title = new JLabel("MIME 类型查询"); + title.setFont(TITLE_FONT); + panel.add(title); + panel.add(Box.createVerticalStrut(4)); + JLabel desc = new JLabel("查询常见文件扩展名的 MIME 类型"); + desc.setForeground(SUBTITLE); + panel.add(desc); + panel.add(Box.createVerticalStrut(12)); + + JBTextField extField = new JBTextField(); + extField.getEmptyText().setText("输入扩展名,如 .html"); + extField.setFont(MONO_FONT); + panel.add(extField); + panel.add(Box.createVerticalStrut(8)); + + JTextArea output = createOutputArea("查询结果..."); + JScrollPane sp = new JBScrollPane(output); + sp.setPreferredSize(new Dimension(200, 200)); + panel.add(sp); + panel.add(Box.createVerticalStrut(8)); + + Map mimeMap = new LinkedHashMap<>(); + mimeMap.put(".html", "text/html"); + mimeMap.put(".css", "text/css"); + mimeMap.put(".js", "application/javascript"); + mimeMap.put(".json", "application/json"); + mimeMap.put(".xml", "application/xml"); + mimeMap.put(".png", "image/png"); + mimeMap.put(".jpg", "image/jpeg"); + mimeMap.put(".gif", "image/gif"); + mimeMap.put(".svg", "image/svg+xml"); + mimeMap.put(".ico", "image/x-icon"); + mimeMap.put(".pdf", "application/pdf"); + mimeMap.put(".zip", "application/zip"); + mimeMap.put(".tar", "application/x-tar"); + mimeMap.put(".mp3", "audio/mpeg"); + mimeMap.put(".mp4", "video/mp4"); + mimeMap.put(".txt", "text/plain"); + mimeMap.put(".csv", "text/csv"); + mimeMap.put(".yaml", "application/x-yaml"); + mimeMap.put(".yml", "application/x-yaml"); + mimeMap.put(".md", "text/markdown"); + + panel.add(btnRow( + toolBtn("查询", "查询 MIME 类型", e -> { + String ext = extField.getText().trim().toLowerCase(); + if (!ext.startsWith(".")) ext = "." + ext; + String mime = mimeMap.get(ext); + if (mime != null) { + output.setText(ext + " -> " + mime); + } else { + output.setText("未知扩展名: " + ext); + } + }), + toolBtn("全部显示", "显示所有 MIME 类型", e -> { + StringBuilder sb = new StringBuilder(); + for (Map.Entry entry : mimeMap.entrySet()) { + sb.append(entry.getKey()).append(" -> ").append(entry.getValue()).append("\n"); + } + output.setText(sb.toString()); + }), + copyBtn(output) + )); + + return panel; + } + + // ==================== Regex Cheat ==================== + + private JPanel createRegexCheatPanel() { + JPanel panel = paddedPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + + JLabel title = new JLabel("正则表达式速查"); + title.setFont(TITLE_FONT); + panel.add(title); + panel.add(Box.createVerticalStrut(4)); + JLabel desc = new JLabel("常用正则表达式语法和示例"); + desc.setForeground(SUBTITLE); + panel.add(desc); + panel.add(Box.createVerticalStrut(12)); + + JTextArea output = createOutputArea("正则速查..."); + output.setEditable(false); + JScrollPane sp = new JBScrollPane(output); + sp.setPreferredSize(new Dimension(200, 300)); + panel.add(sp); + panel.add(Box.createVerticalStrut(8)); + + panel.add(btnRow( + toolBtn("基础语法", "显示基础语法", e -> { + output.setText("=== 基础语法 ===\n" + + ". 任意字符\n" + + "\\d 数字 [0-9]\n" + + "\\D 非数字\n" + + "\\w 单词字符 [a-zA-Z0-9_]\n" + + "\\W 非单词字符\n" + + "\\s 空白字符\n" + + "\\S 非空白字符\n" + + "\\b 单词边界\n" + + "\\B 非单词边界\n" + + "^ 行首\n" + + "$ 行尾"); + }), + toolBtn("量词", "显示量词语法", e -> { + output.setText("=== 量词 ===\n" + + "* 零次或多次\n" + + "+ 一次或多次\n" + + "? 零次或一次\n" + + "{n} 恰好 n 次\n" + + "{n,} 至少 n 次\n" + + "{n,m} n 到 m 次\n" + + "*? 非贪婪匹配\n" + + "+? 非贪婪匹配\n" + + "?? 非贪婪匹配"); + }), + toolBtn("常用模式", "显示常用正则模式", e -> { + output.setText("=== 常用模式 ===\n" + + "邮箱: ^[\\w.-]+@[\\w.-]+\\.\\w{2,}$\n" + + "手机号: ^1[3-9]\\d{9}$\n" + + "URL: ^https?://[\\w./?-]+$\n" + + "IP: ^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$\n" + + "日期: ^\\d{4}-\\d{2}-\\d{2}$\n" + + "时间: ^\\d{2}:\\d{2}:\\d{2}$\n" + + "数字: ^\\d+$\n" + + "中文: ^[\\u4e00-\\u9fff]+$"); + }), + copyBtn(output) + )); + + return panel; + } + + // ==================== Password Strength ==================== + + private JPanel createPasswordPanel() { + JPanel panel = paddedPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + + JLabel title = new JLabel("密码强度检测"); + title.setFont(TITLE_FONT); + panel.add(title); + panel.add(Box.createVerticalStrut(4)); + JLabel desc = new JLabel("检测密码强度并给出改进建议"); + desc.setForeground(SUBTITLE); + panel.add(desc); + panel.add(Box.createVerticalStrut(12)); + + JTextArea input = createInputArea("输入密码..."); + JTextArea output = createOutputArea("检测结果..."); + panel.add(ioPanel(input, output)); + panel.add(Box.createVerticalStrut(8)); + + panel.add(btnRow( + toolBtn("检测", "检测密码强度", e -> { + String pwd = input.getText(); + if (pwd.isEmpty()) { output.setText("请输入密码"); return; } + int score = 0; + StringBuilder feedback = new StringBuilder(); + + if (pwd.length() >= 8) { score += 20; } else { feedback.append("- 密码长度至少 8 位\n"); } + if (pwd.length() >= 12) { score += 10; } + if (pwd.matches(".*[a-z].*")) { score += 15; } else { feedback.append("- 缺少小写字母\n"); } + if (pwd.matches(".*[A-Z].*")) { score += 15; } else { feedback.append("- 缺少大写字母\n"); } + if (pwd.matches(".*\\d.*")) { score += 15; } else { feedback.append("- 缺少数字\n"); } + if (pwd.matches(".*[^a-zA-Z0-9].*")) { score += 20; } else { feedback.append("- 缺少特殊字符\n"); } + + String strength; + if (score >= 80) { strength = "非常强"; } + else if (score >= 60) { strength = "强"; } + else if (score >= 40) { strength = "中等"; } + else if (score >= 20) { strength = "弱"; } + else { strength = "非常弱"; } + + StringBuilder sb = new StringBuilder(); + sb.append("密码强度: ").append(strength).append(" (").append(score).append("/100)\n"); + sb.append("密码长度: ").append(pwd.length()).append("\n"); + sb.append("包含小写: ").append(pwd.matches(".*[a-z].*") ? "是" : "否").append("\n"); + sb.append("包含大写: ").append(pwd.matches(".*[A-Z].*") ? "是" : "否").append("\n"); + sb.append("包含数字: ").append(pwd.matches(".*\\d.*") ? "是" : "否").append("\n"); + sb.append("包含特殊: ").append(pwd.matches(".*[^a-zA-Z0-9].*") ? "是" : "否").append("\n"); + if (feedback.length() > 0) { + sb.append("\n改进建议:\n").append(feedback); + } + output.setText(sb.toString()); + }), + toolBtn("生成强密码", "生成随机强密码", e -> { + String upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + String lower = "abcdefghijklmnopqrstuvwxyz"; + String digits = "0123456789"; + String special = "!@#$%^&*()_+-=[]{}|;:,.<>?"; + String all = upper + lower + digits + special; + StringBuilder sb = new StringBuilder(); + sb.append(upper.charAt(SECURE_RANDOM.nextInt(upper.length()))); + sb.append(lower.charAt(SECURE_RANDOM.nextInt(lower.length()))); + sb.append(digits.charAt(SECURE_RANDOM.nextInt(digits.length()))); + sb.append(special.charAt(SECURE_RANDOM.nextInt(special.length()))); + for (int i = 0; i < 12; i++) { + sb.append(all.charAt(SECURE_RANDOM.nextInt(all.length()))); + } + String pwd = sb.toString(); + input.setText(pwd); + output.setText("已生成强密码,长度: " + pwd.length() + " 位"); + }), + copyBtn(output), + toolBtn("清空", "清空所有", e -> { input.setText(""); output.setText(""); }) + )); + + return panel; + } +} \ No newline at end of file diff --git a/src/main/java/com/huangzj/protoplugin/devtools/DevToolsToolWindowFactory.java b/src/main/java/com/huangzj/protoplugin/devtools/DevToolsToolWindowFactory.java new file mode 100644 index 0000000..ae2471e --- /dev/null +++ b/src/main/java/com/huangzj/protoplugin/devtools/DevToolsToolWindowFactory.java @@ -0,0 +1,29 @@ +package com.huangzj.protoplugin.devtools; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.wm.ToolWindow; +import com.intellij.openapi.wm.ToolWindowFactory; +import com.intellij.openapi.wm.ex.ToolWindowEx; +import com.intellij.ui.content.ContentFactory; +import org.jetbrains.annotations.NotNull; + +public final class DevToolsToolWindowFactory implements ToolWindowFactory { + + public static final String TOOL_WINDOW_ID = "DevTools"; + + public static final String TOOL_WINDOW_STRIPE_TITLE = "开发者工具"; + + @Override + public void init(@NotNull ToolWindow toolWindow) { + if (toolWindow instanceof ToolWindowEx ex) { + ex.setStripeTitle(TOOL_WINDOW_STRIPE_TITLE); + } + } + + @Override + public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindow toolWindow) { + DevToolsPanel panel = new DevToolsPanel(); + var content = ContentFactory.getInstance().createContent(panel, TOOL_WINDOW_STRIPE_TITLE, false); + toolWindow.getContentManager().addContent(content); + } +} \ No newline at end of file diff --git a/src/main/java/com/huangzj/protoplugin/memo/MemoItem.java b/src/main/java/com/huangzj/protoplugin/memo/MemoItem.java index d2bb598..a4c119f 100644 --- a/src/main/java/com/huangzj/protoplugin/memo/MemoItem.java +++ b/src/main/java/com/huangzj/protoplugin/memo/MemoItem.java @@ -1,9 +1,10 @@ package com.huangzj.protoplugin.memo; import java.text.SimpleDateFormat; +import java.util.ArrayList; import java.util.Date; +import java.util.List; -/** 备忘录条目(字段需可被持久化组件序列化)。 */ public class MemoItem { private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm"); @@ -12,6 +13,8 @@ public class MemoItem { public String title = ""; public String content = ""; public long updated = 0L; + public boolean pinned = false; + public List tags = new ArrayList<>(); public MemoItem() {} @@ -48,4 +51,11 @@ public class MemoItem { } return firstLine; } + + public String tagsDisplay() { + if (tags == null || tags.isEmpty()) { + return ""; + } + return String.join(", ", tags); + } } diff --git a/src/main/java/com/huangzj/protoplugin/memo/MemoPanel.java b/src/main/java/com/huangzj/protoplugin/memo/MemoPanel.java index cb75068..c2064f6 100644 --- a/src/main/java/com/huangzj/protoplugin/memo/MemoPanel.java +++ b/src/main/java/com/huangzj/protoplugin/memo/MemoPanel.java @@ -14,26 +14,41 @@ import java.awt.Component; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.Font; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.RenderingHints; +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; import javax.swing.DefaultListCellRenderer; import javax.swing.DefaultListModel; import javax.swing.JButton; import javax.swing.JComboBox; +import javax.swing.JFileChooser; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JSplitPane; +import javax.swing.JTextArea; import javax.swing.ListSelectionModel; import javax.swing.SwingConstants; +import javax.swing.SwingUtilities; import javax.swing.UIManager; import javax.swing.border.Border; -import javax.swing.border.LineBorder; +import javax.swing.border.EmptyBorder; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; import javax.swing.event.ListSelectionEvent; -import javax.swing.event.ListSelectionListener; +import javax.swing.filechooser.FileNameExtensionFilter; import org.jetbrains.annotations.NotNull; public final class MemoPanel extends JPanel { @@ -43,37 +58,46 @@ public final class MemoPanel extends JPanel { private final JBList list = new JBList<>(listModel); private final JBTextField titleField = new JBTextField(); private final JBTextArea contentArea = new JBTextArea(); - private final JButton saveBtn = new JButton("保存"); - private final JButton newBtn = new JButton("新建"); - private final JButton deleteBtn = new JButton("删除"); - private final JLabel themeChooserLabel = new JLabel("界面风格"); + private final JBTextField tagField = new JBTextField(); + private final JButton saveBtn = createStyledBtn("保存", new Color(0x27AE60)); + private final JButton newBtn = createStyledBtn("新建", new Color(0x2980B9)); + private final JButton deleteBtn = createStyledBtn("删除", new Color(0xE74C3C)); + private final JButton pinBtn = createStyledBtn("置顶", new Color(0xE67E22)); + private final JLabel themeChooserLabel = new JLabel("主题"); private final JComboBox themeCombo = new JComboBox<>(MemoUiTheme.values()); private final JLabel titleLabel = new JLabel("标题"); private final JLabel contentLabel = new JLabel("内容"); - private final JLabel flairLabel = new JLabel(" ", SwingConstants.CENTER); - private final JPanel toolbar = new JPanel(new BorderLayout(8, 0)); - private final JPanel leftButtonBar = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 0)); - private final JPanel themeChooserBar = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 0)); + private final JLabel tagLabel = new JLabel("标签"); + private final JPanel toolbar = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 4)); private final JPanel topStack = new JPanel(new BorderLayout()); private JSplitPane split; private JBScrollPane listScroll; private JBScrollPane contentScroll; private final JPanel titleBlock = new JPanel(new BorderLayout(4, 4)); private final JPanel contentBlock = new JPanel(new BorderLayout(4, 4)); + private final JPanel tagBlock = new JPanel(new BorderLayout(4, 4)); private final JPanel right = new JPanel(new BorderLayout(8, 8)); private final JBTextField searchField = new JBTextField(); private final JLabel countLabel = new JLabel(" "); private final JLabel modifiedLabel = new JLabel(" "); + private final JLabel wordCountLabel = new JLabel(" "); private final JPanel leftPanel = new JPanel(new BorderLayout(0, 4)); private final JPanel searchPanel = new JPanel(new BorderLayout(4, 0)); private final JLabel searchIcon = new JLabel("\uD83D\uDD0D"); private final JPanel editorStatusBar = new JPanel(new BorderLayout()); + private final JComboBox sortCombo = new JComboBox<>(new String[]{"按更新时间", "按标题", "置顶优先"}); + private final JButton exportBtn = createStyledBtn("导出", new Color(0x8E44AD)); + private final JButton importBtn = createStyledBtn("导入", new Color(0x2C3E50)); + private final JPanel tagDisplayPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 2)) {{ + setOpaque(false); + }}; private boolean loadingSelection; private boolean themeComboSuppress; private MemoUiTheme currentTheme = MemoUiTheme.IDE_DEFAULT; private final Font defaultLabelFont; private List allItems = new ArrayList<>(); + private MemoItem currentEditingItem; public MemoPanel(@NotNull Project project) { super(new BorderLayout(8, 8)); @@ -82,8 +106,6 @@ public final class MemoPanel extends JPanel { setBorder(JBUI.Borders.empty(8)); initToolbar(); - initFlair(); - initTopStack(); initSearchPanel(); initList(); initLeftPanel(); @@ -95,92 +117,170 @@ public final class MemoPanel extends JPanel { reloadListModel(); } + private static JButton createStyledBtn(String text, Color bg) { + JButton b = new JButton(text) { + @Override + protected void paintComponent(Graphics g) { + Graphics2D g2 = (Graphics2D) g.create(); + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + if (getModel().isPressed()) { + g2.setColor(getBackground().darker()); + } else if (getModel().isRollover()) { + g2.setColor(getBackground().brighter()); + } else { + g2.setColor(getBackground()); + } + g2.fillRoundRect(0, 0, getWidth(), getHeight(), 8, 8); + g2.dispose(); + super.paintComponent(g); + } + }; + b.setOpaque(false); + b.setBackground(bg); + b.setForeground(Color.WHITE); + b.setFont(b.getFont().deriveFont(Font.BOLD, 12f)); + b.setBorder(JBUI.Borders.empty(5, 14)); + b.setFocusPainted(false); + b.setCursor(new java.awt.Cursor(java.awt.Cursor.HAND_CURSOR)); + return b; + } + private void initToolbar() { - leftButtonBar.setOpaque(false); + toolbar.setOpaque(false); newBtn.setToolTipText("新建一条备忘录"); saveBtn.setToolTipText("保存当前备忘录"); deleteBtn.setToolTipText("删除选中的备忘录"); - leftButtonBar.add(newBtn); - leftButtonBar.add(saveBtn); - leftButtonBar.add(deleteBtn); + pinBtn.setToolTipText("切换置顶状态"); + toolbar.add(newBtn); + toolbar.add(saveBtn); + toolbar.add(pinBtn); + toolbar.add(deleteBtn); - themeChooserBar.setOpaque(false); - themeChooserBar.add(themeChooserLabel); - themeChooserBar.add(themeCombo); + JLabel sep = new JLabel("\u2502"); + sep.setForeground(new JBColor(new Color(0xCCCCCC), new Color(0x555555))); + sep.setBorder(JBUI.Borders.empty(0, 6)); + toolbar.add(sep); - toolbar.setOpaque(false); - toolbar.add(leftButtonBar, BorderLayout.WEST); - toolbar.add(themeChooserBar, BorderLayout.EAST); - } + toolbar.add(new JLabel("\uD83D\uDD0D")); + toolbar.add(searchField); + searchField.setPreferredSize(new Dimension(140, 26)); + searchField.getEmptyText().setText("搜索..."); - private void initFlair() { - flairLabel.setOpaque(true); - flairLabel.setFont(flairLabel.getFont().deriveFont(Font.PLAIN, 12f)); - } + toolbar.add(new JLabel("排序:")); + toolbar.add(sortCombo); + sortCombo.setPreferredSize(new Dimension(100, 26)); + + toolbar.add(sep); + + toolbar.add(exportBtn); + toolbar.add(importBtn); + + toolbar.add(new JLabel("主题:")); + toolbar.add(themeCombo); + themeCombo.setPreferredSize(new Dimension(120, 26)); + + JBScrollPane scrollToolbar = new JBScrollPane(toolbar, + JScrollPane.VERTICAL_SCROLLBAR_NEVER, + JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); + scrollToolbar.setBorder(null); + scrollToolbar.getHorizontalScrollBar().setPreferredSize(new Dimension(0, 6)); + + JPanel toolbarWrapper = new JPanel(new BorderLayout()); + toolbarWrapper.setOpaque(true); + toolbarWrapper.setBackground(new JBColor(new Color(0xF5F5F5), new Color(0x2D2D2D))); + toolbarWrapper.setBorder(JBUI.Borders.compound( + JBUI.Borders.customLine(new JBColor(new Color(0xE0E0E0), new Color(0x3A3A3A)), 0, 0, 1, 0), + JBUI.Borders.empty(2, 4))); + toolbarWrapper.add(scrollToolbar, BorderLayout.CENTER); - private void initTopStack() { topStack.setOpaque(false); - topStack.add(flairLabel, BorderLayout.NORTH); - topStack.add(toolbar, BorderLayout.CENTER); + topStack.add(toolbarWrapper, BorderLayout.CENTER); add(topStack, BorderLayout.NORTH); } private void initSearchPanel() { - searchField.getEmptyText().setText("搜索备忘录..."); - searchField.setToolTipText("输入关键字过滤备忘录标题和内容"); searchPanel.setOpaque(false); searchPanel.add(searchIcon, BorderLayout.WEST); searchPanel.add(searchField, BorderLayout.CENTER); - searchPanel.setBorder(JBUI.Borders.empty(0, 0, 4, 0)); + searchPanel.setBorder(JBUI.Borders.empty(0, 0, 6, 0)); + searchField.getEmptyText().setText("搜索备忘录..."); + searchField.setBackground(new JBColor(new Color(0xFFFFFF), new Color(0x3C3C3C))); } private void initList() { list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); installListRenderer(); listScroll = new JBScrollPane(list); - listScroll.setPreferredSize(new Dimension(200, 300)); + listScroll.setPreferredSize(new Dimension(240, 300)); + listScroll.setBorder(JBUI.Borders.compound( + JBUI.Borders.customLine(new JBColor(new Color(0xDDDDDD), new Color(0x444444)), 1), + JBUI.Borders.empty(0))); countLabel.setFont(countLabel.getFont().deriveFont(Font.PLAIN, 11f)); countLabel.setHorizontalAlignment(SwingConstants.RIGHT); - countLabel.setBorder(JBUI.Borders.empty(2, 4, 0, 4)); + countLabel.setBorder(JBUI.Borders.empty(4, 4, 0, 4)); } private void initLeftPanel() { - leftPanel.setOpaque(false); + leftPanel.setOpaque(true); + leftPanel.setBackground(new JBColor(new Color(0xFAFAFA), new Color(0x252525))); leftPanel.add(searchPanel, BorderLayout.NORTH); leftPanel.add(listScroll, BorderLayout.CENTER); leftPanel.add(countLabel, BorderLayout.SOUTH); + leftPanel.setBorder(JBUI.Borders.empty(0, 0, 0, 4)); } private void initEditor() { contentArea.setLineWrap(true); contentArea.setWrapStyleWord(true); contentScroll = new JBScrollPane(contentArea); + contentScroll.setBorder(JBUI.Borders.compound( + JBUI.Borders.customLine(new JBColor(new Color(0xDDDDDD), new Color(0x444444)), 1), + JBUI.Borders.empty(0))); titleBlock.setOpaque(false); titleBlock.add(titleLabel, BorderLayout.NORTH); titleBlock.add(titleField, BorderLayout.CENTER); + tagBlock.setOpaque(false); + JPanel tagInner = new JPanel(new BorderLayout(4, 2)); + tagInner.setOpaque(false); + tagInner.add(tagField, BorderLayout.CENTER); + tagInner.add(tagDisplayPanel, BorderLayout.SOUTH); + tagBlock.add(tagLabel, BorderLayout.NORTH); + tagBlock.add(tagInner, BorderLayout.CENTER); + contentBlock.setOpaque(false); contentBlock.add(contentLabel, BorderLayout.NORTH); contentBlock.add(contentScroll, BorderLayout.CENTER); modifiedLabel.setFont(modifiedLabel.getFont().deriveFont(Font.PLAIN, 11f)); modifiedLabel.setBorder(JBUI.Borders.empty(2, 0, 0, 0)); + wordCountLabel.setFont(wordCountLabel.getFont().deriveFont(Font.PLAIN, 11f)); + wordCountLabel.setBorder(JBUI.Borders.empty(2, 0, 0, 8)); editorStatusBar.setOpaque(false); + editorStatusBar.add(wordCountLabel, BorderLayout.WEST); editorStatusBar.add(modifiedLabel, BorderLayout.EAST); right.setOpaque(false); right.add(titleBlock, BorderLayout.NORTH); right.add(contentBlock, BorderLayout.CENTER); right.add(editorStatusBar, BorderLayout.SOUTH); - titleField.setPreferredSize(new Dimension(200, 28)); + titleField.setPreferredSize(new Dimension(200, 32)); + tagField.setPreferredSize(new Dimension(200, 28)); + tagField.getEmptyText().setText("输入标签后按回车..."); + + titleLabel.setFont(titleLabel.getFont().deriveFont(Font.BOLD, 12f)); + contentLabel.setFont(contentLabel.getFont().deriveFont(Font.BOLD, 12f)); + tagLabel.setFont(tagLabel.getFont().deriveFont(Font.BOLD, 12f)); } private void initSplit() { split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftPanel, right); split.setResizeWeight(0.28); + split.setDividerSize(4); split.setOpaque(false); + split.setBorder(null); add(split, BorderLayout.CENTER); } @@ -193,129 +293,106 @@ public final class MemoPanel extends JPanel { } private void initListeners() { - themeCombo.addActionListener( - e -> { - if (themeComboSuppress) { - return; - } - MemoUiTheme t = (MemoUiTheme) themeCombo.getSelectedItem(); - if (t != null) { - MemoProjectService.getInstance(project).setMemoTheme(t); - applyTheme(t); - } - }); + themeCombo.addActionListener(e -> { + if (themeComboSuppress) return; + MemoUiTheme t = (MemoUiTheme) themeCombo.getSelectedItem(); + if (t != null) { + MemoProjectService.getInstance(project).setMemoTheme(t); + applyTheme(t); + } + }); - list.addListSelectionListener( - new ListSelectionListener() { - @Override - public void valueChanged(ListSelectionEvent e) { - if (e.getValueIsAdjusting()) { - return; - } - loadSelectedIntoEditor(); - } - }); + list.addListSelectionListener((ListSelectionEvent e) -> { + if (e.getValueIsAdjusting()) return; + loadSelectedIntoEditor(); + }); newBtn.addActionListener(e -> createMemo()); saveBtn.addActionListener(e -> saveCurrent()); deleteBtn.addActionListener(e -> deleteCurrent()); + pinBtn.addActionListener(e -> togglePin()); + exportBtn.addActionListener(e -> exportMemos()); + importBtn.addActionListener(e -> importMemos()); + sortCombo.addActionListener(e -> applySortAndFilter()); - titleField - .getDocument() - .addDocumentListener( - new javax.swing.event.DocumentListener() { - @Override - public void insertUpdate(javax.swing.event.DocumentEvent e) { - refreshSelectedTitleInList(); - } + tagField.addActionListener(e -> addTagFromField()); - @Override - public void removeUpdate(javax.swing.event.DocumentEvent e) { - refreshSelectedTitleInList(); - } + titleField.getDocument().addDocumentListener(new DocumentListener() { + @Override public void insertUpdate(DocumentEvent e) { refreshSelectedTitleInList(); updateWordCount(); } + @Override public void removeUpdate(DocumentEvent e) { refreshSelectedTitleInList(); updateWordCount(); } + @Override public void changedUpdate(DocumentEvent e) { refreshSelectedTitleInList(); updateWordCount(); } + }); - @Override - public void changedUpdate(javax.swing.event.DocumentEvent e) { - refreshSelectedTitleInList(); - } - }); + contentArea.getDocument().addDocumentListener(new DocumentListener() { + @Override public void insertUpdate(DocumentEvent e) { markModified(); updateWordCount(); } + @Override public void removeUpdate(DocumentEvent e) { markModified(); updateWordCount(); } + @Override public void changedUpdate(DocumentEvent e) { markModified(); updateWordCount(); } + }); - searchField - .getDocument() - .addDocumentListener( - new javax.swing.event.DocumentListener() { - @Override - public void insertUpdate(javax.swing.event.DocumentEvent e) { - filterList(); - } - - @Override - public void removeUpdate(javax.swing.event.DocumentEvent e) { - filterList(); - } - - @Override - public void changedUpdate(javax.swing.event.DocumentEvent e) { - filterList(); - } - }); + searchField.getDocument().addDocumentListener(new DocumentListener() { + @Override public void insertUpdate(DocumentEvent e) { filterList(); } + @Override public void removeUpdate(DocumentEvent e) { filterList(); } + @Override public void changedUpdate(DocumentEvent e) { filterList(); } + }); } private void installListRenderer() { - list.setCellRenderer( - new DefaultListCellRenderer() { - @Override - public Component getListCellRendererComponent( - JList jList, - Object value, - int index, - boolean isSelected, - boolean cellHasFocus) { - JLabel label = (JLabel) super.getListCellRendererComponent( - jList, value, index, isSelected, cellHasFocus); - if (value instanceof MemoItem) { - MemoItem item = (MemoItem) value; - String title = escapeHtml(item.displayTitle()); - String date = item.displayDate(); - String preview = item.contentPreview(); + list.setCellRenderer(new DefaultListCellRenderer() { + @Override + public Component getListCellRendererComponent(JList jList, Object value, int index, + boolean isSelected, boolean cellHasFocus) { + JLabel label = (JLabel) super.getListCellRendererComponent( + jList, value, index, isSelected, cellHasFocus); + if (value instanceof MemoItem) { + MemoItem item = (MemoItem) value; + String title = escapeHtml(item.displayTitle()); + String date = item.displayDate(); + String preview = item.contentPreview(); + String tagsStr = item.tagsDisplay(); - StringBuilder html = new StringBuilder(128); - html.append(""); - html.append("").append(title).append(""); - if (!date.isEmpty()) { - html.append("
    ").append(date).append(""); - } - if (!preview.isEmpty()) { - html.append("
    ").append(escapeHtml(preview)).append(""); - } - html.append(""); - label.setText(html.toString()); - } - if (currentTheme != MemoUiTheme.IDE_DEFAULT) { - MemoThemeSpec spec = MemoThemeSpec.forTheme(currentTheme); - label.setOpaque(true); - if (isSelected) { - label.setBackground(spec.listSelectedBg); - label.setForeground(spec.listSelectedFg); - } else { - label.setBackground(spec.listBg); - label.setForeground(spec.text); - } - } - label.setBorder(JBUI.Borders.empty(4, 8, 4, 8)); - return label; + StringBuilder html = new StringBuilder(160); + html.append(""); + if (item.pinned) { + html.append(""); } - }); + html.append("").append(title).append(""); + if (!date.isEmpty()) { + html.append("
    ").append(date).append(""); + } + if (!tagsStr.isEmpty()) { + html.append("
    [").append(escapeHtml(tagsStr)).append("]"); + } + if (!preview.isEmpty()) { + html.append("
    ").append(escapeHtml(preview)).append(""); + } + html.append(""); + label.setText(html.toString()); + } + if (currentTheme != MemoUiTheme.IDE_DEFAULT) { + MemoThemeSpec spec = MemoThemeSpec.forTheme(currentTheme); + label.setOpaque(true); + if (isSelected) { + label.setBackground(spec.listSelectedBg); + label.setForeground(spec.listSelectedFg); + } else { + label.setBackground(spec.listBg); + label.setForeground(spec.text); + } + } + label.setBorder(JBUI.Borders.empty(5, 10, 5, 10)); + return label; + } + }); } private static String escapeHtml(String text) { - if (text == null) { - return ""; - } + if (text == null) return ""; return text.replace("&", "&") .replace("<", "<") .replace(">", ">") @@ -333,24 +410,14 @@ public final class MemoPanel extends JPanel { } MemoThemeSpec spec = MemoThemeSpec.forTheme(theme); - Border softField = - JBUI.Borders.compound( - new LineBorder(spec.fieldBorder, 1, true), - JBUI.Borders.empty(4, 6)); Color panelBg = spec.panelBg; setBackground(panelBg); topStack.setOpaque(true); topStack.setBackground(panelBg); - toolbar.setBackground(panelBg); toolbar.setOpaque(true); - leftButtonBar.setOpaque(true); - leftButtonBar.setBackground(panelBg); - themeChooserBar.setOpaque(true); - themeChooserBar.setBackground(panelBg); + toolbar.setBackground(panelBg); themeChooserLabel.setForeground(spec.text); - flairLabel.setBackground(panelBg); - flairLabel.setForeground(spec.text); split.setBackground(panelBg); leftPanel.setBackground(panelBg); searchPanel.setBackground(panelBg); @@ -362,39 +429,25 @@ public final class MemoPanel extends JPanel { right.setBackground(panelBg); titleBlock.setBackground(panelBg); contentBlock.setBackground(panelBg); + tagBlock.setBackground(panelBg); + tagDisplayPanel.setBackground(panelBg); titleLabel.setForeground(spec.text); contentLabel.setForeground(spec.text); + tagLabel.setForeground(spec.text); countLabel.setForeground(spec.text); modifiedLabel.setForeground(spec.text); + wordCountLabel.setForeground(spec.text); editorStatusBar.setBackground(panelBg); - if (theme == MemoUiTheme.CUTE_CANDY) { - flairLabel.setVisible(true); - flairLabel.setText("\u2728 今天也要甜甜哒\uFF5E 选一条备忘,把想法留下来吧 \uD83D\uDC95"); - titleLabel.setText("\uD83D\uDCDD 标题"); - contentLabel.setText("\uD83D\uDC97 正文"); - titleLabel.setFont(defaultLabelFont.deriveFont(Font.BOLD, defaultLabelFont.getSize() + 1f)); - contentLabel.setFont( - defaultLabelFont.deriveFont(Font.BOLD, defaultLabelFont.getSize() + 1f)); - } else { - flairLabel.setVisible(false); - flairLabel.setText(" "); - titleLabel.setText("标题"); - contentLabel.setText("内容"); - titleLabel.setFont(defaultLabelFont); - contentLabel.setFont(defaultLabelFont); - } - - styleTextField(titleField, spec, softField); - styleTextField(searchField, spec, softField); - styleTextArea(contentArea, contentScroll, spec, softField); - - styleAccentButton(newBtn, spec); - styleAccentButton(saveBtn, spec); - styleAccentButton(deleteBtn, spec); + styleTextField(titleField, spec); + styleTextField(searchField, spec); + styleTextField(tagField, spec); + styleTextArea(contentArea, contentScroll, spec); themeCombo.setBackground(spec.editorBg); themeCombo.setForeground(spec.text); + sortCombo.setBackground(spec.editorBg); + sortCombo.setForeground(spec.text); revalidate(); repaint(); @@ -409,16 +462,8 @@ public final class MemoPanel extends JPanel { setBackground(panelBg); topStack.setOpaque(false); - topStack.setBackground(panelBg); - toolbar.setBackground(panelBg); toolbar.setOpaque(false); - leftButtonBar.setOpaque(false); - themeChooserBar.setOpaque(false); themeChooserLabel.setForeground(labelFg); - flairLabel.setVisible(false); - flairLabel.setText(" "); - flairLabel.setBackground(panelBg); - flairLabel.setForeground(labelFg); split.setBackground(panelBg); leftPanel.setBackground(panelBg); searchPanel.setBackground(panelBg); @@ -430,29 +475,34 @@ public final class MemoPanel extends JPanel { right.setBackground(panelBg); titleBlock.setBackground(panelBg); contentBlock.setBackground(panelBg); + tagBlock.setBackground(panelBg); + tagDisplayPanel.setBackground(panelBg); titleLabel.setText("标题"); contentLabel.setText("内容"); + tagLabel.setText("标签"); titleLabel.setFont(defaultLabelFont); contentLabel.setFont(defaultLabelFont); + tagLabel.setFont(defaultLabelFont); titleLabel.setForeground(labelFg); contentLabel.setForeground(labelFg); + tagLabel.setForeground(labelFg); countLabel.setForeground(JBColor.GRAY); modifiedLabel.setForeground(JBColor.GRAY); + wordCountLabel.setForeground(JBColor.GRAY); editorStatusBar.setBackground(panelBg); resetFieldLook(titleField, editorBg, fg); resetFieldLook(searchField, editorBg, fg); + resetFieldLook(tagField, editorBg, fg); resetFieldLook(contentArea, editorBg, fg); contentScroll.getViewport().setBackground(editorBg); contentArea.setCaretColor(fg); titleField.setCaretColor(fg); searchField.setCaretColor(fg); - - resetButton(newBtn); - resetButton(saveBtn); - resetButton(deleteBtn); + tagField.setCaretColor(fg); resetCombo(themeCombo); + resetCombo(sortCombo); revalidate(); repaint(); @@ -462,47 +512,36 @@ public final class MemoPanel extends JPanel { c.setOpaque(true); c.setBackground(bg); c.setForeground(fg); - c.setBorder( - JBUI.Borders.compound( - JBUI.Borders.customLine(UIUtil.getBoundsColor(), 1), - JBUI.Borders.empty(4, 6))); + c.setBorder(JBUI.Borders.compound( + JBUI.Borders.customLine(UIUtil.getBoundsColor(), 1), + JBUI.Borders.empty(4, 8))); Color selBg = UIManager.getColor("TextArea.selectionBackground"); - if (selBg == null) { - selBg = UIUtil.getListSelectionBackground(); - } + if (selBg == null) selBg = UIUtil.getListSelectionBackground(); Color selFg = UIManager.getColor("TextArea.selectionForeground"); - if (selFg == null) { - selFg = UIUtil.getListSelectionForeground(); - } + if (selFg == null) selFg = UIUtil.getListSelectionForeground(); c.setSelectionColor(selBg); c.setSelectedTextColor(selFg); } - private static void resetButton(JButton b) { - b.setOpaque(false); - b.setBackground(null); - b.setForeground(null); - b.setBorder(JBUI.Borders.empty(4, 12)); - } - private static void resetCombo(JComboBox combo) { combo.setOpaque(true); combo.setBackground(null); combo.setForeground(null); } - private static void styleTextField(JBTextField field, MemoThemeSpec spec, Border border) { + private static void styleTextField(JBTextField field, MemoThemeSpec spec) { field.setOpaque(true); field.setBackground(spec.editorBg); field.setForeground(spec.text); - field.setBorder(border); + field.setBorder(JBUI.Borders.compound( + JBUI.Borders.customLine(spec.fieldBorder, 1), + JBUI.Borders.empty(4, 8))); field.setCaretColor(spec.text); field.setSelectedTextColor(spec.listSelectedFg); field.setSelectionColor(spec.listSelectedBg); } - private static void styleTextArea( - JBTextArea area, JScrollPane scroll, MemoThemeSpec spec, Border border) { + private static void styleTextArea(JBTextArea area, JScrollPane scroll, MemoThemeSpec spec) { area.setOpaque(true); area.setBackground(spec.editorBg); area.setForeground(spec.text); @@ -510,163 +549,489 @@ public final class MemoPanel extends JPanel { area.setCaretColor(spec.text); area.setSelectedTextColor(spec.listSelectedFg); area.setSelectionColor(spec.listSelectedBg); - scroll.setBorder(border); scroll.getViewport().setBackground(spec.editorBg); + scroll.setBorder(JBUI.Borders.compound( + JBUI.Borders.customLine(spec.fieldBorder, 1), + JBUI.Borders.empty(0))); } - private static void styleAccentButton(JButton b, MemoThemeSpec spec) { - b.setOpaque(true); - b.setBackground(spec.buttonBg); - b.setForeground(spec.buttonFg); - b.setBorder( - JBUI.Borders.compound( - new LineBorder(spec.accent, 1, true), JBUI.Borders.empty(5, 14))); - b.setFocusPainted(false); + private void markModified() { + if (currentEditingItem != null) { + modifiedLabel.setText("已修改"); + } + } + + private void updateWordCount() { + String text = contentArea.getText(); + int charCount = text.length(); + int wordCount = text.isEmpty() ? 0 : text.split("[\\s,;.?!\\u3002\\uff1b\\uff0c\\uff01\\uff1f]+").length; + int lineCount = text.isEmpty() ? 0 : text.split("\n", -1).length; + wordCountLabel.setText(String.format("\uD83D\uDCC4 %d 字 | %d 词 | %d 行", charCount, wordCount, lineCount)); } private void reloadListModel() { - allItems = MemoProjectService.getInstance(project).getItems(); - filterList(); + MemoProjectService svc = MemoProjectService.getInstance(project); + allItems = new ArrayList<>(svc.getItems()); + applySortAndFilter(); + } + + private void applySortAndFilter() { + String query = searchField.getText().toLowerCase().trim(); + int sortMode = sortCombo.getSelectedIndex(); + + List filtered = allItems.stream() + .filter(item -> query.isEmpty() + || item.title.toLowerCase().contains(query) + || item.content.toLowerCase().contains(query) + || item.tagsDisplay().toLowerCase().contains(query)) + .sorted(getComparator(sortMode)) + .collect(Collectors.toList()); + + listModel.clear(); + for (MemoItem item : filtered) { + listModel.addElement(item); + } + countLabel.setText("\uD83D\uDCCB 共 " + filtered.size() + " 条"); + } + + private Comparator getComparator(int sortMode) { + switch (sortMode) { + case 0: + return (a, b) -> { + if (a.pinned != b.pinned) return a.pinned ? -1 : 1; + return Long.compare(b.updated, a.updated); + }; + case 1: + return (a, b) -> { + if (a.pinned != b.pinned) return a.pinned ? -1 : 1; + return a.displayTitle().compareToIgnoreCase(b.displayTitle()); + }; + case 2: + return (a, b) -> { + if (a.pinned != b.pinned) return a.pinned ? -1 : 1; + return Long.compare(b.updated, a.updated); + }; + default: + return (a, b) -> Long.compare(b.updated, a.updated); + } } private void filterList() { - String query = searchField.getText().trim().toLowerCase(); - listModel.clear(); - - List filtered; - if (query.isEmpty()) { - filtered = allItems; - } else { - filtered = allItems.stream() - .filter(m -> matchesQuery(m, query)) - .collect(Collectors.toList()); - } - - for (MemoItem m : filtered) { - listModel.addElement(m); - } - - updateCountLabel(filtered.size()); - - if (!filtered.isEmpty() && list.getSelectedIndex() < 0) { - list.setSelectedIndex(0); - } - - if (filtered.isEmpty() && !allItems.isEmpty() && !query.isEmpty()) { - clearEditor(); - modifiedLabel.setText("无匹配结果"); - } else if (filtered.isEmpty() && allItems.isEmpty()) { - clearEditor(); - modifiedLabel.setText(" "); - } - } - - private static boolean matchesQuery(MemoItem m, String query) { - if (m.title != null && m.title.toLowerCase().contains(query)) { - return true; - } - if (m.content != null && m.content.toLowerCase().contains(query)) { - return true; - } - return false; - } - - private void updateCountLabel(int visibleCount) { - int total = allItems.size(); - if (total == 0) { - countLabel.setText(" "); - } else if (visibleCount == total) { - countLabel.setText("共 " + total + " 条"); - } else { - countLabel.setText("显示 " + visibleCount + " / " + total + " 条"); - } - } - - private void createMemo() { - MemoItem m = MemoProjectService.getInstance(project).createMemo(); - allItems = MemoProjectService.getInstance(project).getItems(); - searchField.setText(""); - filterList(); - list.setSelectedValue(m, true); - titleField.requestFocusInWindow(); - } - - private void saveCurrent() { - MemoItem m = list.getSelectedValue(); - if (m == null) { - return; - } - m.title = titleField.getText(); - m.content = contentArea.getText(); - m.updated = System.currentTimeMillis(); - allItems = MemoProjectService.getInstance(project).getItems(); - list.repaint(); - updateModifiedLabel(m); - } - - private void deleteCurrent() { - MemoItem m = list.getSelectedValue(); - if (m == null) { - return; - } - String itemTitle = m.displayTitle(); - int result = JOptionPane.showConfirmDialog( - this, - "确定要删除「" + itemTitle + "」吗?\n此操作不可撤销。", - "确认删除", - JOptionPane.YES_NO_OPTION, - JOptionPane.WARNING_MESSAGE); - if (result != JOptionPane.YES_OPTION) { - return; - } - int idx = list.getSelectedIndex(); - MemoProjectService.getInstance(project).removeById(m.id); - allItems = MemoProjectService.getInstance(project).getItems(); - listModel.removeElement(m); - updateCountLabel(listModel.getSize()); - if (!listModel.isEmpty()) { - int next = Math.min(idx, listModel.getSize() - 1); - list.setSelectedIndex(Math.max(0, next)); - } else { - clearEditor(); - } + applySortAndFilter(); } private void loadSelectedIntoEditor() { - MemoItem m = list.getSelectedValue(); + if (loadingSelection) return; + + MemoItem sel = list.getSelectedValue(); + if (sel == null) { + saveCurrentEditingItem(); + titleField.setText(""); + contentArea.setText(""); + tagField.setText(""); + tagDisplayPanel.removeAll(); + tagDisplayPanel.revalidate(); + tagDisplayPanel.repaint(); + modifiedLabel.setText(""); + wordCountLabel.setText(" "); + currentEditingItem = null; + return; + } + + saveCurrentEditingItem(); + loadingSelection = true; - try { - if (m == null) { - clearEditor(); - } else { - titleField.setText(m.title != null ? m.title : ""); - contentArea.setText(m.content != null ? m.content : ""); - updateModifiedLabel(m); + currentEditingItem = sel; + titleField.setText(sel.title); + contentArea.setText(sel.content); + modifiedLabel.setText(sel.updated > 0 ? "\uD83D\uDCC5 " + sel.displayDate() : ""); + updateWordCount(); + refreshTagDisplay(sel); + pinBtn.setText(sel.pinned ? "\u2B50 已置顶" : "\uD83D\uDCCC 置顶"); + loadingSelection = false; + } + + private void saveCurrentEditingItem() { + if (currentEditingItem == null) return; + currentEditingItem.title = titleField.getText(); + currentEditingItem.content = contentArea.getText(); + currentEditingItem.updated = System.currentTimeMillis(); + list.repaint(); + } + + private void refreshTagDisplay(MemoItem item) { + tagDisplayPanel.removeAll(); + if (item.tags != null) { + for (String tag : item.tags) { + JLabel tagLabel = new JLabel(tag); + tagLabel.setOpaque(true); + tagLabel.setBackground(new JBColor(new Color(0xE3F2FD), new Color(0x1A3A5C))); + tagLabel.setForeground(new JBColor(new Color(0x1565C0), new Color(0x90CAF9))); + tagLabel.setBorder(JBUI.Borders.compound( + JBUI.Borders.customLine(new JBColor(new Color(0xBBDEFB), new Color(0x2A4A6C)), 1), + JBUI.Borders.empty(2, 8))); + tagLabel.setFont(tagLabel.getFont().deriveFont(Font.PLAIN, 11f)); + tagDisplayPanel.add(tagLabel); } - } finally { + } + tagDisplayPanel.revalidate(); + tagDisplayPanel.repaint(); + } + + private void addTagFromField() { + String tag = tagField.getText().trim(); + if (tag.isEmpty()) return; + + if (currentEditingItem == null) { + JOptionPane.showMessageDialog(this, "请先选择一条备忘录"); + return; + } + + if (currentEditingItem.tags == null) { + currentEditingItem.tags = new ArrayList<>(); + } + if (!currentEditingItem.tags.contains(tag)) { + currentEditingItem.tags.add(tag); + tagField.setText(""); + refreshTagDisplay(currentEditingItem); + list.repaint(); + } + } + + private void togglePin() { + MemoItem sel = list.getSelectedValue(); + if (sel == null) return; + + saveCurrentEditingItem(); + + sel.pinned = !sel.pinned; + pinBtn.setText(sel.pinned ? "\u2B50 已置顶" : "\uD83D\uDCCC 置顶"); + + loadingSelection = true; + applySortAndFilter(); + + for (int i = 0; i < listModel.size(); i++) { + if (listModel.get(i).id.equals(sel.id)) { + list.setSelectedIndex(i); + break; + } + } + loadingSelection = false; + } + + private void createMemo() { + saveCurrentEditingItem(); + + MemoProjectService svc = MemoProjectService.getInstance(project); + MemoItem m = svc.createMemo(); + m.title = "新备忘录"; + m.content = ""; + m.updated = System.currentTimeMillis(); + allItems.add(m); + + loadingSelection = true; + applySortAndFilter(); + + for (int i = 0; i < listModel.size(); i++) { + if (listModel.get(i).id.equals(m.id)) { + list.setSelectedIndex(i); + list.ensureIndexIsVisible(i); + break; + } + } + loadingSelection = false; + + currentEditingItem = m; + titleField.setText(m.title); + contentArea.setText(m.content); + modifiedLabel.setText(""); + updateWordCount(); + refreshTagDisplay(m); + pinBtn.setText(m.pinned ? "\u2B50 已置顶" : "\uD83D\uDCCC 置顶"); + titleField.requestFocus(); + titleField.selectAll(); + } + + private void saveCurrent() { + if (currentEditingItem == null) return; + + currentEditingItem.title = titleField.getText(); + currentEditingItem.content = contentArea.getText(); + currentEditingItem.updated = System.currentTimeMillis(); + modifiedLabel.setText("\u2705 已保存 " + currentEditingItem.displayDate()); + list.repaint(); + } + + private void saveCurrentSilent() { + if (currentEditingItem == null) return; + + currentEditingItem.title = titleField.getText(); + currentEditingItem.content = contentArea.getText(); + currentEditingItem.updated = System.currentTimeMillis(); + list.repaint(); + } + + private void deleteCurrent() { + MemoItem sel = list.getSelectedValue(); + if (sel == null) return; + + int confirm = JOptionPane.showConfirmDialog(this, + "确定要删除\u300C" + sel.displayTitle() + "\u300D吗?\n此操作不可撤销。", + "删除确认", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE); + if (confirm != JOptionPane.YES_OPTION) return; + + saveCurrentEditingItem(); + + if (currentEditingItem != null && currentEditingItem.id.equals(sel.id)) { + currentEditingItem = null; + } + + MemoProjectService svc = MemoProjectService.getInstance(project); + svc.removeById(sel.id); + allItems.remove(sel); + + loadingSelection = true; + applySortAndFilter(); + + if (listModel.size() > 0) { + list.setSelectedIndex(0); loadingSelection = false; - } - } - - private void updateModifiedLabel(MemoItem m) { - String date = m.displayDate(); - if (!date.isEmpty()) { - modifiedLabel.setText("最后修改: " + date); + MemoItem first = list.getSelectedValue(); + if (first != null) { + currentEditingItem = first; + titleField.setText(first.title); + contentArea.setText(first.content); + modifiedLabel.setText(first.updated > 0 ? "\uD83D\uDCC5 " + first.displayDate() : ""); + updateWordCount(); + refreshTagDisplay(first); + pinBtn.setText(first.pinned ? "\u2B50 已置顶" : "\uD83D\uDCCC 置顶"); + } } else { - modifiedLabel.setText(" "); + loadingSelection = false; + currentEditingItem = null; + titleField.setText(""); + contentArea.setText(""); + tagField.setText(""); + tagDisplayPanel.removeAll(); + tagDisplayPanel.revalidate(); + tagDisplayPanel.repaint(); + modifiedLabel.setText(""); + wordCountLabel.setText(" "); } } - private void clearEditor() { - titleField.setText(""); - contentArea.setText(""); - modifiedLabel.setText(" "); - } - private void refreshSelectedTitleInList() { - if (loadingSelection) { - return; + if (currentEditingItem == null || loadingSelection) return; + currentEditingItem.title = titleField.getText(); + list.repaint(); + } + + private void exportMemos() { + saveCurrentEditingItem(); + + JFileChooser chooser = new JFileChooser(); + chooser.setDialogTitle("导出备忘录"); + chooser.setSelectedFile(new java.io.File("memos_export.json")); + chooser.setFileFilter(new FileNameExtensionFilter("JSON 文件 (*.json)", "json")); + + if (chooser.showSaveDialog(this) != JFileChooser.APPROVE_OPTION) return; + + java.io.File file = chooser.getSelectedFile(); + if (!file.getName().endsWith(".json")) { + file = new java.io.File(file.getAbsolutePath() + ".json"); } - javax.swing.SwingUtilities.invokeLater(() -> list.repaint()); + + try { + StringBuilder sb = new StringBuilder(); + sb.append("[\n"); + for (int i = 0; i < allItems.size(); i++) { + MemoItem item = allItems.get(i); + sb.append(" {\n"); + sb.append(" \"id\": \"").append(escapeJson(item.id)).append("\",\n"); + sb.append(" \"title\": \"").append(escapeJson(item.title)).append("\",\n"); + sb.append(" \"content\": \"").append(escapeJson(item.content)).append("\",\n"); + sb.append(" \"updated\": ").append(item.updated).append(",\n"); + sb.append(" \"pinned\": ").append(item.pinned).append(",\n"); + sb.append(" \"tags\": ["); + if (item.tags != null) { + for (int j = 0; j < item.tags.size(); j++) { + if (j > 0) sb.append(", "); + sb.append("\"").append(escapeJson(item.tags.get(j))).append("\""); + } + } + sb.append("]\n"); + sb.append(" }"); + if (i < allItems.size() - 1) sb.append(","); + sb.append("\n"); + } + sb.append("]\n"); + java.nio.file.Files.writeString(file.toPath(), sb.toString(), StandardCharsets.UTF_8); + JOptionPane.showMessageDialog(this, "已导出 " + allItems.size() + " 条备忘录到:\n" + file); + } catch (Exception ex) { + JOptionPane.showMessageDialog(this, "导出失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); + } + } + + private void importMemos() { + saveCurrentEditingItem(); + + JFileChooser chooser = new JFileChooser(); + chooser.setDialogTitle("导入备忘录"); + chooser.setFileFilter(new FileNameExtensionFilter("JSON 文件 (*.json)", "json")); + + if (chooser.showOpenDialog(this) != JFileChooser.APPROVE_OPTION) return; + + try { + String json = java.nio.file.Files.readString(chooser.getSelectedFile().toPath(), StandardCharsets.UTF_8); + List imported = parseMemoJson(json); + if (imported.isEmpty()) { + JOptionPane.showMessageDialog(this, "未找到有效的备忘录数据"); + return; + } + + MemoProjectService svc = MemoProjectService.getInstance(project); + int added = 0; + for (MemoItem item : imported) { + if (svc.findById(item.id) == null) { + svc.getItems().add(item); + allItems.add(item); + added++; + } + } + if (added > 0) { + applySortAndFilter(); + JOptionPane.showMessageDialog(this, "成功导入 " + added + " 条备忘录"); + } else { + JOptionPane.showMessageDialog(this, "所有备忘录已存在,未导入新数据"); + } + } catch (Exception ex) { + JOptionPane.showMessageDialog(this, "导入失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); + } + } + + private static String escapeJson(String s) { + if (s == null) return ""; + return s.replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("\t", "\\t"); + } + + private static List parseMemoJson(String json) { + List result = new ArrayList<>(); + try { + com.google.gson.Gson gson = new com.google.gson.Gson(); + MemoItem[] items = gson.fromJson(json, MemoItem[].class); + if (items != null) { + for (MemoItem item : items) { + if (item.id != null && !item.id.isEmpty()) { + if (item.tags == null) item.tags = new ArrayList<>(); + result.add(item); + } + } + } + } catch (Exception ignored) { + try { + json = json.trim(); + if (json.startsWith("[")) { + int braceCount = 0; + int start = -1; + for (int i = 0; i < json.length(); i++) { + char c = json.charAt(i); + if (c == '{') { + if (braceCount == 0) start = i; + braceCount++; + } else if (c == '}') { + braceCount--; + if (braceCount == 0 && start >= 0) { + String itemJson = json.substring(start, i + 1); + MemoItem item = parseSingleMemo(itemJson); + if (item != null) result.add(item); + start = -1; + } + } + } + } + } catch (Exception ignored2) {} + } + return result; + } + + private static MemoItem parseSingleMemo(String json) { + try { + MemoItem item = new MemoItem(); + String idField = extractJsonField(json, "id"); + if (idField != null) item.id = idField; + item.title = extractJsonField(json, "title"); + item.content = extractJsonField(json, "content"); + String updatedStr = extractJsonField(json, "updated"); + if (updatedStr != null) { + try { item.updated = Long.parseLong(updatedStr); } catch (NumberFormatException ignored) {} + } + String pinnedStr = extractJsonField(json, "pinned"); + item.pinned = "true".equals(pinnedStr); + String tagsStr = extractJsonArray(json, "tags"); + if (tagsStr != null && !tagsStr.isEmpty()) { + item.tags = new ArrayList<>(); + for (String t : tagsStr.split(",")) { + String clean = t.replace("\"", "").trim(); + if (!clean.isEmpty()) item.tags.add(clean); + } + } + if (item.tags == null) item.tags = new ArrayList<>(); + return item.id.isEmpty() ? null : item; + } catch (Exception e) { + return null; + } + } + + private static String extractJsonField(String json, String field) { + String key = "\"" + field + "\""; + int idx = json.indexOf(key); + if (idx < 0) return null; + idx = json.indexOf(':', idx + key.length()); + if (idx < 0) return null; + idx++; + while (idx < json.length() && json.charAt(idx) == ' ') idx++; + if (idx >= json.length()) return null; + if (json.charAt(idx) == '"') { + StringBuilder sb = new StringBuilder(); + idx++; + while (idx < json.length()) { + char c = json.charAt(idx); + if (c == '\\') { + idx++; + if (idx < json.length()) sb.append(json.charAt(idx)); + } else if (c == '"') { + break; + } else { + sb.append(c); + } + idx++; + } + return sb.toString(); + } else { + StringBuilder sb = new StringBuilder(); + while (idx < json.length() && (Character.isDigit(json.charAt(idx)) || json.charAt(idx) == '-' + || json.charAt(idx) == '.' || json.charAt(idx) == 'e' || json.charAt(idx) == 'E' + || json.charAt(idx) == '+' || json.charAt(idx) == 't' || json.charAt(idx) == 'f')) { + sb.append(json.charAt(idx)); + idx++; + } + return sb.toString().trim(); + } + } + + private static String extractJsonArray(String json, String field) { + String key = "\"" + field + "\""; + int idx = json.indexOf(key); + if (idx < 0) return null; + idx = json.indexOf('[', idx + key.length()); + if (idx < 0) return null; + idx++; + int end = json.indexOf(']', idx); + if (end < 0) return null; + return json.substring(idx, end).trim(); } } \ No newline at end of file diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 964afd1..f8a9bbb 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -22,6 +22,11 @@ anchor="right" secondary="true" canCloseContents="false"/> + JAVA com.huangzj.protoplugin.json.ClassToJsonIntention