吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 1925|回复: 20
收起左侧

[Python 转载] AI生成的话术管理助手

[复制链接]
提拉米苏子冉 发表于 2026-6-25 16:24
本帖最后由 提拉米苏子冉 于 2026-6-25 16:33 编辑

话术管理助手 是一款基于 Python + Tkinter 开发的桌面工具,专为客服、销售、运营等需要频繁使用固定话术的职场人士设计。
它采用分组管理一键复制拖拽排序等高效交互方式,帮助您告别重复输入,大幅提升回复效率。
所有数据完全本地存储,无需联网,保护隐私;
支持窗口置顶主题颜色自定义分组显隐等便捷功能;
话术内容支持多行文本,并在列表中用 ↵ 符号清晰显示换行位置。
功能特点
1. 分组管理
  • 自由创建、重命名、删除分组(至少保留一个)。
  • 分组支持拖拽排序,常用分组置顶。
  • 分组面板可临时隐藏,专注于话术浏览。
2. 话术管理
  • 每个分组下可添加任意条话术,内容支持多行文本。
  • 话术列表实时搜索(内容关键词),支持中文输入防抖。
  • 双击话术自动复制到剪贴板,并弹出“已复制”提示。
  • 话术支持拖拽排序,高频话术放在前列。
  • 话术中的换行符在列表中显示为 ↵,清晰区分段落。
3. 个性化外观
  • 可调节字体大小(8~20pt)。
  • 可切换字体颜色(8种)和列表背景色(8种)。
  • 所有外观设置自动保存,重启后恢复。
4. 窗口管理
  • 窗口置顶功能,让工具始终悬浮于其他应用之上。
  • 窗口大小和位置自动保存,下次启动恢复。
  • 分组面板宽度可拖动调节,宽度值也会保存。
5. 数据安全
  • 每次保存数据时自动备份旧文件(groups.json.backup),防止数据损坏。
  • 加载数据失败时自动尝试恢复备份,若失败则创建默认数据。
  • 配置采用原子写入(先写临时文件再替换),避免配置损坏。

适用场景
  • 客服/在线支持:快速回复常见问题,保证话术一致性与专业度。[md][/md]
  • 销售/商务:保存产品介绍、报价模板、跟进话术,提升成单效率。
  • 运营/内容编辑:管理常见回复、审核意见、协作模板。
  • 个人知识管理:储存常用代码片段、邮件模板、学习笔记等。

    第一次发帖,如有发错,请版主帮忙修改或删除,谢谢!






    [Python] 纯文本查看 复制代码
    # -*- coding: utf-8 -*-
    """
    话术管理助手 - 单文件版
    """
    
    import tkinter as tk
    from tkinter import ttk, messagebox, font
    import pyperclip
    import json
    import os
    import threading
    import time
    import shutil
    from typing import Dict, List, Any, Optional, Callable, Tuple
    from dataclasses import dataclass, asdict, field
    
    
    # ==================== 常量 ====================
    WINDOW_TITLE = "话术管理"
    WINDOW_GEOMETRY = "1100x700"
    ICON_FILE = "icon.ico"
    DEFAULT_ALWAYS_ON_TOP = False
    DEFAULT_SHOW_SEARCH = True
    SEARCH_PLACEHOLDER = "全局搜索内容..."
    DEFAULT_FONT_FAMILY = "Consolas"
    DEFAULT_FONT_SIZE = 10
    DEFAULT_FONT_COLOR = "黑色"
    DEFAULT_BG_COLOR = "白色"
    DEFAULT_UI_FONT = "微软雅黑"
    DISPLAY_MAX_LEN = 80
    SAVE_DELAY = 1.0
    SEARCH_DEBOUNCE_MS = 300
    INPUT_DIALOG_SIZE = (400, 180)
    TEXT_DIALOG_SIZE = (500, 350)
    TEXT_DIALOG_MIN_SIZE = (450, 300)
    BACKUP_SUFFIX = ".backup"
    
    
    # ==================== UI文本 ====================
    BTN_OK = "确定"
    BTN_CANCEL = "取消"
    BTN_ADD_GROUP = "添加分组"
    BTN_RENAME_GROUP = "重命名分组"
    BTN_DELETE_GROUP = "删除分组"
    BTN_ADD_SCRIPT = "添加话术"
    BTN_EDIT_SCRIPT = "编辑话术"
    BTN_DELETE_SCRIPT = "删除话术"
    
    MENU_ADD_GROUP = "添加分组"
    MENU_RENAME_GROUP = "重命名分组"
    MENU_DELETE_GROUP = "删除分组"
    MENU_ADD_SCRIPT = "添加话术"
    MENU_EDIT_SCRIPT = "编辑话术"
    MENU_DELETE_SCRIPT = "删除话术"
    
    DLG_TITLE_ADD_GROUP = "新建分组"
    DLG_TITLE_RENAME_GROUP = "重命名分组"
    DLG_TITLE_ADD_SCRIPT = "添加话术"
    DLG_TITLE_EDIT_SCRIPT = "编辑话术"
    
    DLG_PROMPT_GROUP_NAME = "请输入分组名称:"
    DLG_PROMPT_NEW_GROUP_NAME = "请输入新分组名称:"
    DLG_PROMPT_SCRIPT_CONTENT = "请输入话术内容:"
    DLG_PROMPT_EDIT_SCRIPT = "请编辑话术内容:"
    
    WARNING_TITLE = "警告"
    CONFIRM_TITLE = "确认"
    INFO_TITLE = "提示"
    
    WARN_GROUP_NAME_EMPTY = "分组名称不能为空!"
    WARN_GROUP_NAME_EXISTS = "分组名称已存在!"
    WARN_GROUP_NOT_SELECTED = "请先选择一个分组!"
    WARN_SCRIPT_CONTENT_EMPTY = "话术内容不能为空!"
    WARN_SCRIPT_NOT_SELECTED = "请先选择一个话术!"
    WARN_AT_LEAST_ONE_GROUP = "至少需要保留一个分组!"
    WARN_GROUP_NOT_FOUND = "分组不存在!"
    WARN_SCRIPT_INDEX_ERROR = "话术索引错误!"
    WARN_LOAD_DATA_FAILED = "加载数据失败,已使用备份或创建默认数据。"
    
    CONFIRM_DELETE_GROUP = "确定要删除分组 '{group_name}' 吗?"
    CONFIRM_DELETE_SCRIPT = "确定要删除选中的话术吗?"
    
    TOOLTIP_FONT_SIZE = "字体大小"
    TOOLTIP_FONT_COLOR = "字体颜色"
    TOOLTIP_BG_COLOR = "背景颜色"
    TOOLTIP_ALWAYS_ON_TOP = "窗口置顶"
    
    DEFAULT_GROUP_NAME = "默认分组"
    
    TOGGLE_BTN_HIDE = "▶"
    TOGGLE_BTN_SHOW = "◀"
    COPY_SUCCESS = "已复制到剪贴板"
    
    
    # ==================== 颜色映射 ====================
    COLOR_MAP = {
        "黑色": "black", "白色": "white", "红色": "red", "绿色": "green",
        "蓝色": "blue", "黄色": "yellow", "紫色": "purple", "橙色": "orange",
        "粉色": "pink", "棕色": "brown", "青色": "cyan", "洋红色": "magenta",
        "深红色": "darkred", "深绿色": "darkgreen", "深蓝色": "darkblue",
        "深紫色": "darkviolet", "深橙色": "darkorange", "浅灰色": "lightgray",
        "浅黄色": "lightyellow", "浅蓝色": "lightblue", "浅绿色": "lightgreen",
        "浅粉色": "lightpink", "浅紫色": "lavender", "灰色": "gray",
        "深灰色": "darkgray", "银灰色": "silver", "金色": "gold", "海军蓝": "navy",
        "橄榄色": "olive", "绿松石": "turquoise", "珊瑚色": "coral",
        "紫罗兰": "violet", "米色": "beige", "象牙色": "ivory",
        "薄荷色": "mintcream", "天蓝色": "skyblue", "巧克力色": "chocolate",
        "番茄色": "tomato"
    }
    
    SHORT_TO_FULL_COLOR = {
        "黑": "黑色", "红": "红色", "绿": "绿色", "蓝": "蓝色",
        "紫": "紫色", "橙": "橙色", "棕": "棕色", "灰": "灰色"
    }
    SHORT_TO_FULL_BG = {
        "白": "白色", "浅灰": "浅灰色", "浅黄": "浅黄色", "浅蓝": "浅蓝色",
        "浅绿": "浅绿色", "浅粉": "浅粉色", "米色": "米色", "象牙": "象牙色"
    }
    FULL_TO_SHORT_COLOR = {v: k for k, v in SHORT_TO_FULL_COLOR.items()}
    FULL_TO_SHORT_BG = {v: k for k, v in SHORT_TO_FULL_BG.items()}
    FONT_COLOR_OPTIONS = list(SHORT_TO_FULL_COLOR.keys())
    BG_COLOR_OPTIONS = list(SHORT_TO_FULL_BG.keys())
    
    
    # ==================== 数据模型 ====================
    @dataclass
    class ScriptItem:
        content: str
        created_at: float = field(default_factory=time.time)
        updated_at: float = field(default_factory=time.time)
    
        def to_dict(self) -> Dict[str, Any]:
            return asdict(self)
    
        @classmethod
        def from_dict(cls, data: Dict[str, Any]) -> 'ScriptItem':
            return cls(**data)
    
    
    @dataclass
    class Group:
        name: str
        script_items: List[ScriptItem] = field(default_factory=list)
    
        def to_dict(self) -> Dict[str, Any]:
            return {
                "name": self.name,
                "scripts": [item.to_dict() for item in self.script_items]
            }
    
        @classmethod
        def from_dict(cls, data: Dict[str, Any]) -> 'Group':
            scripts_data = data.get("scripts", [])
            if scripts_data and isinstance(scripts_data[0], str):
                script_items = [ScriptItem(content=content) for content in scripts_data]
            else:
                script_items = [ScriptItem.from_dict(item) for item in scripts_data]
            return cls(name=data["name"], script_items=script_items)
    
    
    # ==================== 管理器 ====================
    class ConfigManager:
        def __init__(self, config_file: str = "settings.json"):
            self.config_file = config_file
            self._cache = {}
            self._load_config()
    
        def _load_config(self):
            default_config = {
                "window_title": WINDOW_TITLE,
                "window_geometry": WINDOW_GEOMETRY,
                "list_font_size": DEFAULT_FONT_SIZE,
                "list_font_family": DEFAULT_FONT_FAMILY,
                "list_font_color": DEFAULT_FONT_COLOR,
                "list_bg_color": DEFAULT_BG_COLOR,
                "always_on_top": DEFAULT_ALWAYS_ON_TOP,
                "show_search": DEFAULT_SHOW_SEARCH,
                "group_panel_width": 150
            }
            if os.path.exists(self.config_file):
                try:
                    with open(self.config_file, 'r', encoding='utf-8') as f:
                        loaded = json.load(f)
                    self._cache = {**default_config, **loaded}
                except Exception:
                    self._cache = default_config
            else:
                self._cache = default_config
    
        def get(self, key: str, default: Any = None) -> Any:
            return self._cache.get(key, default)
    
        def set(self, key: str, value: Any, save: bool = True):
            self._cache[key] = value
            if save:
                self.save()
    
        def save(self):
            try:
                temp_file = self.config_file + ".tmp"
                with open(temp_file, 'w', encoding='utf-8') as f:
                    json.dump(self._cache, f, ensure_ascii=False, indent=2)
                os.replace(temp_file, self.config_file)
            except Exception as e:
                print(f"保存配置失败: {e}")
    
    
    class DataStorage:
        def __init__(self, data_file: str = "groups.json"):
            self.data_file = data_file
            self._groups: List[Group] = []
            self._cache: Dict[str, Group] = {}
            self._dirty = False
            self._lock = threading.RLock()
            self._save_timer = None
            self.load()
    
        def load(self) -> bool:
            with self._lock:
                if os.path.exists(self.data_file):
                    try:
                        with open(self.data_file, 'r', encoding='utf-8') as f:
                            data = json.load(f)
                        self._groups = [Group.from_dict(item) for item in data]
                        self._build_cache()
                        return True
                    except Exception as e:
                        print(f"加载数据失败: {e}")
                        backup_file = self.data_file + BACKUP_SUFFIX
                        if os.path.exists(backup_file):
                            try:
                                with open(backup_file, 'r', encoding='utf-8') as f:
                                    data = json.load(f)
                                self._groups = [Group.from_dict(item) for item in data]
                                self._build_cache()
                                print("已从备份恢复数据")
                                return True
                            except Exception:
                                print("备份恢复也失败")
                        self._create_default_data()
                        return False
                else:
                    self._create_default_data()
                    return True
    
        def _create_default_data(self):
            self._groups = [Group(name=DEFAULT_GROUP_NAME)]
            self._build_cache()
    
        def _build_cache(self):
            self._cache = {group.name: group for group in self._groups}
    
        def _schedule_save(self):
            if self._save_timer is not None:
                self._save_timer.cancel()
                self._save_timer = None
            self._save_timer = threading.Timer(SAVE_DELAY, self._save_if_dirty)
            self._save_timer.daemon = True
            self._save_timer.start()
    
        def _save_if_dirty(self):
            if self._dirty:
                self.save(async_mode=False)
    
        def save(self, async_mode: bool = True):
            def _do_save():
                with self._lock:
                    if not self._dirty:
                        return
                    try:
                        if os.path.exists(self.data_file):
                            backup = self.data_file + BACKUP_SUFFIX
                            shutil.copy2(self.data_file, backup)
                        data = [group.to_dict() for group in self._groups]
                        temp_file = self.data_file + ".tmp"
                        with open(temp_file, 'w', encoding='utf-8') as f:
                            json.dump(data, f, ensure_ascii=False, indent=2)
                        os.replace(temp_file, self.data_file)
                        self._dirty = False
                    except Exception as e:
                        print(f"保存数据失败: {e}")
    
            if async_mode:
                thread = threading.Thread(target=_do_save, daemon=True)
                thread.start()
            else:
                _do_save()
    
        def mark_dirty(self):
            with self._lock:
                self._dirty = True
            self._schedule_save()
    
        # ---------- 分组操作 ----------
        def get_groups(self) -> List[Group]:
            with self._lock:
                return self._groups.copy()
    
        def get_group(self, name: str) -> Optional[Group]:
            with self._lock:
                return self._cache.get(name)
    
        def add_group(self, name: str) -> bool:
            with self._lock:
                if name in self._cache:
                    return False
                new_group = Group(name=name)
                self._groups.append(new_group)
                self._cache[name] = new_group
                self.mark_dirty()
                return True
    
        def rename_group(self, old_name: str, new_name: str) -> bool:
            with self._lock:
                if new_name in self._cache or old_name not in self._cache:
                    return False
                group = self._cache[old_name]
                group.name = new_name
                del self._cache[old_name]
                self._cache[new_name] = group
                self.mark_dirty()
                return True
    
        def delete_group(self, name: str) -> bool:
            with self._lock:
                if name not in self._cache or len(self._groups) <= 1:
                    return False
                self._groups = [g for g in self._groups if g.name != name]
                del self._cache[name]
                self.mark_dirty()
                return True
    
        # ---------- 话术操作 ----------
        def add_script(self, group_name: str, content: str) -> bool:
            with self._lock:
                group = self._cache.get(group_name)
                if not group:
                    return False
                group.script_items.append(ScriptItem(content=content))
                self.mark_dirty()
                return True
    
        def update_script(self, group_name: str, index: int, content: str) -> bool:
            with self._lock:
                group = self._cache.get(group_name)
                if not group or index >= len(group.script_items):
                    return False
                group.script_items[index].content = content
                group.script_items[index].updated_at = time.time()
                self.mark_dirty()
                return True
    
        def delete_script(self, group_name: str, index: int) -> bool:
            with self._lock:
                group = self._cache.get(group_name)
                if not group or index >= len(group.script_items):
                    return False
                group.script_items.pop(index)
                self.mark_dirty()
                return True
    
        def get_script_content(self, group_name: str) -> List[str]:
            with self._lock:
                group = self._cache.get(group_name)
                return [item.content for item in group.script_items] if group else []
    
        def search_script(self, query: str) -> Dict[str, List[str]]:
            results = {}
            query = query.lower()
            with self._lock:
                for group in self._groups:
                    matches = [item.content for item in group.script_items if query in item.content.lower()]
                    if matches:
                        results[group.name] = matches
            return results
    
    
    class ColorManager:
        FULL_TO_SHORT_COLOR = FULL_TO_SHORT_COLOR
        FULL_TO_SHORT_BG = FULL_TO_SHORT_BG
        SHORT_TO_FULL_COLOR = SHORT_TO_FULL_COLOR
        SHORT_TO_FULL_BG = SHORT_TO_FULL_BG
    
        @classmethod
        def get_full_color(cls, short_name: str, is_background: bool = False) -> str:
            if is_background:
                return cls.SHORT_TO_FULL_BG.get(short_name, "白色")
            return cls.SHORT_TO_FULL_COLOR.get(short_name, "黑色")
    
        @classmethod
        def get_short_color(cls, full_name: str, is_background: bool = False) -> str:
            if is_background:
                return cls.FULL_TO_SHORT_BG.get(full_name, "白")
            return cls.FULL_TO_SHORT_COLOR.get(full_name, "黑")
    
        @classmethod
        def get_tk_color(cls, color_name: str) -> str:
            return COLOR_MAP.get(color_name, "black")
    
        @classmethod
        def get_font_color_options(cls) -> List[str]:
            return FONT_COLOR_OPTIONS
    
        @classmethod
        def get_bg_color_options(cls) -> List[str]:
            return BG_COLOR_OPTIONS
    
    
    # ==================== 自定义组件 ====================
    class WidgetFactory:
        @staticmethod
        def create_tooltip(parent, widget, text: str, delay: int = 2000):
            def show_tooltip(event):
                tooltip = tk.Toplevel(parent)
                tooltip.wm_overrideredirect(True)
                tooltip.wm_geometry(f"+{event.x_root+10}+{event.y_root+10}")
                label = tk.Label(tooltip, text=text, background="#ffffe0",
                                 relief="solid", borderwidth=1, font=("微软雅黑", 9))
                label.pack()
                widget.tooltip_window = tooltip
    
                def hide_tooltip():
                    if hasattr(widget, 'tooltip_window'):
                        widget.tooltip_window.destroy()
                        delattr(widget, 'tooltip_window')
                widget.bind("<Leave>", lambda e: hide_tooltip())
                widget.tooltip_id = parent.after(delay, hide_tooltip)
    
            widget.bind("<Enter>", show_tooltip)
    
    
    class DraggableListbox(tk.Listbox):
        def __init__(self, parent, on_swap: Callable[[int, int], None],
                     drag_enabled: bool = True, **kwargs):
            super().__init__(parent, **kwargs)
            self.on_swap = on_swap
            self.drag_enabled = drag_enabled
            self.drag_start_index: Optional[int] = None
            self._last_swap_time = 0
    
            if self.drag_enabled:
                self.bind("<ButtonPress-1>", self._start_drag)
                self.bind("<B1-Motion>", self._do_drag)
                self.bind("<ButtonRelease-1>", self._stop_drag)
    
        def _get_valid_index(self, y: int) -> Optional[int]:
            index = self.nearest(y)
            bbox = self.bbox(index)
            if bbox and bbox[1] <= y <= bbox[1] + bbox[3]:
                return index
            return None
    
        def _start_drag(self, event):
            index = self._get_valid_index(event.y)
            if index is not None:
                self.drag_start_index = index
                self.config(cursor="fleur")
    
        def _do_drag(self, event):
            if self.drag_start_index is None:
                return
            current_index = self.nearest(event.y)
            if current_index != self.drag_start_index:
                now = time.time()
                if now - self._last_swap_time < 0.05:
                    return
                self._last_swap_time = now
                self.on_swap(self.drag_start_index, current_index)
                self.drag_start_index = current_index
    
        def _stop_drag(self, event):
            if self.drag_start_index is not None:
                self.config(cursor="")
                self.drag_start_index = None
                self._last_swap_time = 0
    
    
    # ==================== 对话框 ====================
    class InputDialog:
        def __init__(self, parent, title: str, prompt: str,
                     default_text: str = "", callback: Optional[Callable] = None):
            self.parent = parent
            self.callback = callback
            self.dialog = tk.Toplevel(parent)
            self.dialog.title(title)
            w, h = INPUT_DIALOG_SIZE
            self.dialog.geometry(f"{w}x{h}")
            self.dialog.resizable(False, False)
            self.dialog.transient(parent)
            self.dialog.grab_set()
            self._center()
            self._build_ui(prompt, default_text)
    
        def _center(self):
            self.dialog.update_idletasks()
            x = self.parent.winfo_rootx() + (self.parent.winfo_width() - self.dialog.winfo_width()) // 2
            y = self.parent.winfo_rooty() + (self.parent.winfo_height() - self.dialog.winfo_height()) // 2
            self.dialog.geometry(f"+{x}+{y}")
    
        def _build_ui(self, prompt: str, default_text: str):
            main_frame = ttk.Frame(self.dialog)
            main_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=20)
            main_frame.grid_rowconfigure(1, weight=1)
            main_frame.grid_columnconfigure(0, weight=1)
    
            ttk.Label(main_frame, text=prompt, font=(DEFAULT_UI_FONT, 10)) \
                .grid(row=0, column=0, sticky="w", pady=(0, 10))
    
            self.entry_var = tk.StringVar(value=default_text)
            entry = ttk.Entry(main_frame, textvariable=self.entry_var, font=(DEFAULT_UI_FONT, 10))
            entry.grid(row=1, column=0, sticky="ew", pady=(0, 20))
            entry.focus_set()
            if default_text:
                entry.select_range(0, tk.END)
    
            btn_frame = ttk.Frame(main_frame)
            btn_frame.grid(row=2, column=0, sticky="e")
    
            def on_ok():
                value = self.entry_var.get().strip()
                if value and self.callback:
                    self.callback(value)
                self.dialog.destroy()
    
            def on_cancel():
                self.dialog.destroy()
    
            ttk.Button(btn_frame, text=BTN_OK, command=on_ok, width=10) \
                .pack(side=tk.LEFT, padx=(0, 10))
            ttk.Button(btn_frame, text=BTN_CANCEL, command=on_cancel, width=10) \
                .pack(side=tk.LEFT)
    
            self.dialog.bind("<Return>", lambda e: on_ok())
            self.dialog.bind("<Escape>", lambda e: on_cancel())
    
    
    class TextDialog:
        def __init__(self, parent, title: str, prompt: str,
                     content: str = "", callback: Optional[Callable] = None):
            self.parent = parent
            self.callback = callback
            self.dialog = tk.Toplevel(parent)
            self.dialog.title(title)
            w, h = TEXT_DIALOG_SIZE
            self.dialog.geometry(f"{w}x{h}")
            min_w, min_h = TEXT_DIALOG_MIN_SIZE
            self.dialog.minsize(min_w, min_h)
            self.dialog.transient(parent)
            self.dialog.grab_set()
            self._center()
            self._build_ui(prompt, content)
    
        def _center(self):
            self.dialog.update_idletasks()
            x = self.parent.winfo_rootx() + (self.parent.winfo_width() - self.dialog.winfo_width()) // 2
            y = self.parent.winfo_rooty() + (self.parent.winfo_height() - self.dialog.winfo_height()) // 2
            self.dialog.geometry(f"+{x}+{y}")
    
        def _build_ui(self, prompt: str, content: str):
            main_frame = ttk.Frame(self.dialog)
            main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
            main_frame.grid_rowconfigure(0, weight=0)
            main_frame.grid_rowconfigure(1, weight=1)
            main_frame.grid_rowconfigure(2, weight=0)
            main_frame.grid_columnconfigure(0, weight=1)
    
            ttk.Label(main_frame, text=prompt, font=(DEFAULT_UI_FONT, 10)) \
                .grid(row=0, column=0, sticky="w", pady=(0, 5))
    
            text_container = ttk.Frame(main_frame)
            text_container.grid(row=1, column=0, sticky="nsew", pady=(0, 10))
            text_container.grid_rowconfigure(0, weight=1)
            text_container.grid_columnconfigure(0, weight=1)
    
            self.text_widget = tk.Text(text_container, font=("Consolas", 10), wrap=tk.WORD)
            self.text_widget.grid(row=0, column=0, sticky="nsew")
            if content:
                self.text_widget.insert("1.0", content)
                self.text_widget.tag_add(tk.SEL, "1.0", tk.END)
                self.text_widget.mark_set(tk.INSERT, "1.0")
                self.text_widget.see(tk.INSERT)
    
            scrollbar = ttk.Scrollbar(text_container, command=self.text_widget.yview)
            scrollbar.grid(row=0, column=1, sticky="ns")
            self.text_widget.config(yscrollcommand=scrollbar.set)
    
            btn_frame = ttk.Frame(main_frame)
            btn_frame.grid(row=2, column=0, sticky="e", pady=(5, 0))
    
            def on_ok():
                value = self.text_widget.get("1.0", tk.END).strip()
                if value and self.callback:
                    self.callback(value)
                self.dialog.destroy()
    
            def on_cancel():
                self.dialog.destroy()
    
            ttk.Button(btn_frame, text=BTN_OK, command=on_ok, width=8) \
                .pack(side=tk.LEFT, padx=(0, 10))
            ttk.Button(btn_frame, text=BTN_CANCEL, command=on_cancel, width=8) \
                .pack(side=tk.LEFT)
    
            self.dialog.bind("<Escape>", lambda e: on_cancel())
            self.text_widget.bind("<Control-Return>", lambda e: on_ok())
            self.text_widget.focus_set()
    
    
    # ==================== 分组面板 ====================
    class GroupPanel:
        def __init__(self, parent, storage: DataStorage, on_group_selected: Callable):
            self.storage = storage
            self.on_group_selected = on_group_selected
            self.current_group: Optional[str] = None
    
            self.frame = tk.Frame(parent, relief="flat", borderwidth=0, bg="white")
    
            self.listbox = DraggableListbox(
                self.frame,
                on_swap=self._swap_groups,
                font=(DEFAULT_UI_FONT, 10),
                selectmode=tk.SINGLE,
                relief="flat",
                borderwidth=0,
                highlightthickness=0,
                drag_enabled=True
            )
            self.listbox.pack(fill=tk.BOTH, expand=True)
    
            scrollbar = tk.Scrollbar(self.frame, command=self.listbox.yview)
            scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
            self.listbox.config(yscrollcommand=scrollbar.set)
    
            self.menu = tk.Menu(self.frame, tearoff=0)
            self.menu.add_command(label=MENU_ADD_GROUP, command=self._add_group_dialog)
            self.menu.add_separator()
            self.menu.add_command(label=MENU_RENAME_GROUP, command=self._rename_group_dialog)
            self.menu.add_command(label=MENU_DELETE_GROUP, command=self._delete_group_dialog)
    
            self.listbox.bind("<<ListboxSelect>>", self._on_select)
            self.listbox.bind("<Button-3>", self._show_menu)
    
            self.frame.config(bg=self.listbox.cget("bg"))
    
        def refresh(self):
            self.listbox.delete(0, tk.END)
            for group in self.storage.get_groups():
                self.listbox.insert(tk.END, group.name)
    
            if self.current_group:
                for i in range(self.listbox.size()):
                    if self.listbox.get(i) == self.current_group:
                        self.listbox.selection_set(i)
                        break
    
        def set_current_group(self, name: str):
            self.current_group = name
            self.refresh()
    
        def get_current_group(self) -> Optional[str]:
            return self.current_group
    
        def refresh_appearance(self, bg_color: str):
            self.listbox.config(background=bg_color)
            self.frame.config(bg=bg_color)
    
        def _on_select(self, event):
            selection = self.listbox.curselection()
            if selection:
                self.current_group = self.listbox.get(selection[0])
                self.on_group_selected(self.current_group)
    
        def _swap_groups(self, idx1: int, idx2: int):
            groups = self.storage.get_groups()
            if 0 <= idx1 < len(groups) and 0 <= idx2 < len(groups):
                groups[idx1], groups[idx2] = groups[idx2], groups[idx1]
                self.storage.mark_dirty()
    
                item1 = self.listbox.get(idx1)
                item2 = self.listbox.get(idx2)
                self.listbox.delete(idx1)
                self.listbox.insert(idx1, item2)
                self.listbox.delete(idx2)
                self.listbox.insert(idx2, item1)
    
                self.listbox.selection_clear(0, tk.END)
                self.listbox.selection_set(idx2)
                self.current_group = groups[idx2].name
    
        def _add_group_dialog(self):
            def add_group(name: str):
                if not name:
                    messagebox.showwarning(WARNING_TITLE, WARN_GROUP_NAME_EMPTY)
                    return
                if self.storage.add_group(name):
                    self.refresh()
                    self.current_group = name
                    self.on_group_selected(name)
                else:
                    messagebox.showwarning(WARNING_TITLE, WARN_GROUP_NAME_EXISTS)
    
            InputDialog(self.frame.winfo_toplevel(), DLG_TITLE_ADD_GROUP,
                        DLG_PROMPT_GROUP_NAME, "", add_group)
    
        def _rename_group_dialog(self):
            selection = self.listbox.curselection()
            if not selection:
                messagebox.showwarning(WARNING_TITLE, WARN_GROUP_NOT_SELECTED)
                return
            old_name = self.listbox.get(selection[0])
    
            def rename_group(new_name: str):
                if not new_name:
                    messagebox.showwarning(WARNING_TITLE, WARN_GROUP_NAME_EMPTY)
                    return
                if old_name == new_name:
                    return
                if self.storage.rename_group(old_name, new_name):
                    if self.current_group == old_name:
                        self.current_group = new_name
                    self.refresh()
                    self.on_group_selected(self.current_group)
                else:
                    messagebox.showwarning(WARNING_TITLE, WARN_GROUP_NAME_EXISTS)
    
            InputDialog(self.frame.winfo_toplevel(), DLG_TITLE_RENAME_GROUP,
                        DLG_PROMPT_NEW_GROUP_NAME, old_name, rename_group)
    
        def _delete_group_dialog(self):
            selection = self.listbox.curselection()
            if not selection:
                messagebox.showwarning(WARNING_TITLE, WARN_GROUP_NOT_SELECTED)
                return
            group_name = self.listbox.get(selection[0])
    
            if messagebox.askyesno(CONFIRM_TITLE, CONFIRM_DELETE_GROUP.format(group_name=group_name)):
                if self.storage.delete_group(group_name):
                    groups = self.storage.get_groups()
                    new_current = groups[0].name if groups else None
                    self.current_group = new_current
                    self.refresh()
                    self.on_group_selected(new_current)
                else:
                    messagebox.showwarning(WARNING_TITLE, WARN_AT_LEAST_ONE_GROUP)
    
        def _show_menu(self, event):
            try:
                index = self.listbox.nearest(event.y)
                bbox = self.listbox.bbox(index)
                if bbox and bbox[1] <= event.y <= bbox[1] + bbox[3]:
                    self.listbox.selection_clear(0, tk.END)
                    self.listbox.selection_set(index)
                    self.listbox.activate(index)
                self.menu.tk_popup(event.x_root, event.y_root)
            finally:
                self.menu.grab_release()
    
    
    # ==================== 话术面板 ====================
    class ScriptPanel:
        def __init__(self, parent, storage: DataStorage, config: ConfigManager):
            self.storage = storage
            self.config = config
            self.color_mgr = ColorManager()
            self.current_group: Optional[str] = None
    
            self.frame = tk.Frame(parent, relief="flat", borderwidth=0, bg="white")
    
            self.list_font = font.Font(
                family=self.config.get("list_font_family", DEFAULT_FONT_FAMILY),
                size=self.config.get("list_font_size", DEFAULT_FONT_SIZE)
            )
    
            self._cached_scripts = []
            self._cached_display = []
            self._current_filter = ""
            self._display_to_cache = []
    
            self.listbox = DraggableListbox(
                self.frame,
                on_swap=self._swap_scripts,
                font=self.list_font,
                selectmode=tk.SINGLE,
                relief="flat",
                borderwidth=0,
                highlightthickness=0,
                drag_enabled=True
            )
            self.listbox.pack(fill=tk.BOTH, expand=True)
    
            scrollbar = tk.Scrollbar(self.frame, command=self.listbox.yview)
            scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
            self.listbox.config(yscrollcommand=scrollbar.set)
    
            self.menu = tk.Menu(self.frame, tearoff=0)
            self.menu.add_command(label=MENU_ADD_SCRIPT, command=self._add_script_dialog)
            self.menu.add_separator()
            self.menu.add_command(label=MENU_EDIT_SCRIPT, command=self._edit_script)
            self.menu.add_command(label=MENU_DELETE_SCRIPT, command=self._delete_script)
    
            self.listbox.bind("<<ListboxSelect>>", self._on_select)
            self.listbox.bind("<Double-Button-1>", self._use_script)
            self.listbox.bind("<Button-3>", self._show_menu)
    
            self._search_after_id = None
    
            self._apply_appearance()
            self.frame.config(bg=self.listbox.cget("bg"))
    
        def _format_display(self, content: str, index: int) -> str:
            display = content.replace('\n', ' &#8629; ')
            display = f"{index}. {display}"
            if len(display) > DISPLAY_MAX_LEN:
                display = display[:DISPLAY_MAX_LEN-3] + "..."
            return display
    
        def _load_cache(self):
            if self.current_group:
                raw = self.storage.get_script_content(self.current_group)
                self._cached_scripts = raw
                self._cached_display = []
                for i, content in enumerate(raw, 1):
                    self._cached_display.append(self._format_display(content, i))
            else:
                self._cached_scripts = []
                self._cached_display = []
    
        def set_group(self, group_name: str):
            self.current_group = group_name
            self._load_cache()
            self.refresh()
    
        def refresh(self, filter_keyword: str = ""):
            self.listbox.delete(0, tk.END)
            if not self.current_group or not self._cached_scripts:
                self._display_to_cache = []
                return
    
            self._current_filter = filter_keyword
            self._display_to_cache = []
            if filter_keyword:
                lfilter = filter_keyword.lower()
                for i, content in enumerate(self._cached_scripts):
                    if lfilter in content.lower():
                        self.listbox.insert(tk.END, self._cached_display[i])
                        self._display_to_cache.append(i)
            else:
                for i, display in enumerate(self._cached_display):
                    self.listbox.insert(tk.END, display)
                    self._display_to_cache.append(i)
    
        def refresh_appearance(self):
            self.list_font.configure(size=self.config.get("list_font_size", DEFAULT_FONT_SIZE))
            font_color_name = self.config.get("list_font_color", "黑色")
            font_color = self.color_mgr.get_tk_color(font_color_name)
            bg_color_name = self.config.get("list_bg_color", DEFAULT_BG_COLOR)
            bg_color = self.color_mgr.get_tk_color(bg_color_name)
            self.listbox.config(font=self.list_font, foreground=font_color, background=bg_color)
            self.frame.config(bg=bg_color)
    
        def _apply_appearance(self):
            self.refresh_appearance()
    
        def _on_select(self, event):
            pass
    
        def _swap_scripts(self, idx1: int, idx2: int):
            if not self.current_group:
                return
            group = self.storage.get_group(self.current_group)
            if not group or idx1 >= len(group.script_items) or idx2 >= len(group.script_items):
                return
    
            group.script_items[idx1], group.script_items[idx2] = \
                group.script_items[idx2], group.script_items[idx1]
            self.storage.mark_dirty()
    
            self._cached_scripts[idx1], self._cached_scripts[idx2] = \
                self._cached_scripts[idx2], self._cached_scripts[idx1]
            self._cached_display[idx1], self._cached_display[idx2] = \
                self._cached_display[idx2], self._cached_display[idx1]
    
            item1 = self.listbox.get(idx1)
            item2 = self.listbox.get(idx2)
            self.listbox.delete(idx1)
            self.listbox.insert(idx1, item2)
            self.listbox.delete(idx2)
            self.listbox.insert(idx2, item1)
    
            self.listbox.selection_clear(0, tk.END)
            self.listbox.selection_set(idx2)
    
        def _add_script_dialog(self):
            if not self.current_group:
                messagebox.showwarning(WARNING_TITLE, WARN_GROUP_NOT_SELECTED)
                return
    
            def add(content: str):
                if not content:
                    messagebox.showwarning(WARNING_TITLE, WARN_SCRIPT_CONTENT_EMPTY)
                    return
                if self.storage.add_script(self.current_group, content):
                    self._load_cache()
                    self.refresh(self._current_filter)
    
            TextDialog(self.frame.winfo_toplevel(), DLG_TITLE_ADD_SCRIPT,
                       DLG_PROMPT_SCRIPT_CONTENT, "", add)
    
        def _edit_script(self):
            if not self.current_group:
                messagebox.showwarning(WARNING_TITLE, WARN_GROUP_NOT_SELECTED)
                return
            selection = self.listbox.curselection()
            if not selection:
                messagebox.showwarning(WARNING_TITLE, WARN_SCRIPT_NOT_SELECTED)
                return
            idx = selection[0]
            if idx < len(self._display_to_cache):
                real_idx = self._display_to_cache[idx]
                if real_idx < len(self._cached_scripts):
                    content = self._cached_scripts[real_idx]
    
                    def update(new_content: str):
                        if not new_content:
                            messagebox.showwarning(WARNING_TITLE, WARN_SCRIPT_CONTENT_EMPTY)
                            return
                        if self.storage.update_script(self.current_group, real_idx, new_content):
                            self._load_cache()
                            self.refresh(self._current_filter)
    
                    TextDialog(self.frame.winfo_toplevel(), DLG_TITLE_EDIT_SCRIPT,
                               DLG_PROMPT_EDIT_SCRIPT, content, update)
                else:
                    messagebox.showwarning(WARNING_TITLE, WARN_SCRIPT_INDEX_ERROR)
            else:
                messagebox.showwarning(WARNING_TITLE, WARN_SCRIPT_INDEX_ERROR)
    
        def _delete_script(self):
            if not self.current_group:
                messagebox.showwarning(WARNING_TITLE, WARN_GROUP_NOT_SELECTED)
                return
            selection = self.listbox.curselection()
            if not selection:
                messagebox.showwarning(WARNING_TITLE, WARN_SCRIPT_NOT_SELECTED)
                return
            idx = selection[0]
            if idx < len(self._display_to_cache):
                real_idx = self._display_to_cache[idx]
    
                if messagebox.askyesno(CONFIRM_TITLE, CONFIRM_DELETE_SCRIPT):
                    if self.storage.delete_script(self.current_group, real_idx):
                        self._load_cache()
                        self.refresh(self._current_filter)
            else:
                messagebox.showwarning(WARNING_TITLE, WARN_SCRIPT_INDEX_ERROR)
    
        def _use_script(self, event=None):
            if not self.current_group:
                return
            selection = self.listbox.curselection()
            if not selection:
                return
            idx = selection[0]
            if idx < len(self._display_to_cache):
                real_idx = self._display_to_cache[idx]
                if real_idx < len(self._cached_scripts):
                    content = self._cached_scripts[real_idx]
                    pyperclip.copy(content)
                    messagebox.showinfo(INFO_TITLE, COPY_SUCCESS)
    
        def _show_menu(self, event):
            try:
                index = self.listbox.nearest(event.y)
                bbox = self.listbox.bbox(index)
                if bbox and bbox[1] <= event.y <= bbox[1] + bbox[3]:
                    self.listbox.selection_clear(0, tk.END)
                    self.listbox.selection_set(index)
                    self.listbox.activate(index)
                self.menu.tk_popup(event.x_root, event.y_root)
            finally:
                self.menu.grab_release()
    
    
    # ==================== 设置栏 ====================
    class SettingsBar(ttk.Frame):
        def __init__(self, parent, config: ConfigManager,
                     on_font_changed: Callable,
                     on_search: Callable,
                     on_toggle_group: Callable,
                     on_toggle_top: Callable):
            super().__init__(parent)
            self.config = config
            self.color_mgr = ColorManager()
            self.on_font_changed = on_font_changed
            self.on_search = on_search
            self.on_toggle_group = on_toggle_group
            self.on_toggle_top = on_toggle_top
            self.widget_factory = WidgetFactory()
            self._search_debounce_id = None
    
            self._create_widgets()
            self._apply_initial_values()
    
        def _create_widgets(self):
            self.columnconfigure(0, weight=1)
            self.columnconfigure(1, weight=0)
            self.columnconfigure(2, weight=0)
            self.columnconfigure(3, weight=0)
            self.columnconfigure(4, weight=0)
            self.columnconfigure(5, weight=0)
    
            row = 0
            # 搜索框
            self.search_var = tk.StringVar()
            self.search_entry = ttk.Entry(self, textvariable=self.search_var,
                                          font=(DEFAULT_UI_FONT, 10))
            self.search_entry.insert(0, SEARCH_PLACEHOLDER)
            self.search_entry.grid(row=row, column=0, sticky="ew", padx=(5, 0))
            self.search_entry.bind("<FocusIn>", self._clear_placeholder)
            self.search_entry.bind("<FocusOut>", self._set_placeholder)
            # 使用 trace_add 替代 trace
            self.search_var.trace_add("write", self._on_search_input)
    
            # 字体大小
            font_frame = ttk.Frame(self)
            font_frame.grid(row=row, column=1, padx=(5, 0))
            self.font_size_var = tk.IntVar(value=self.config.get("list_font_size", DEFAULT_FONT_SIZE))
            font_spinbox = tk.Spinbox(font_frame, from_=8, to=20, width=4,
                                      textvariable=self.font_size_var,
                                      command=self._on_font_size_changed)
            font_spinbox.pack(side=tk.LEFT)
            font_spinbox.bind("<Return>", lambda e: self._on_font_size_changed())
            font_spinbox.bind("<FocusOut>", lambda e: self._on_font_size_changed())
            self.widget_factory.create_tooltip(self, font_spinbox, TOOLTIP_FONT_SIZE)
    
            # 字体颜色
            font_color_frame = ttk.Frame(self)
            font_color_frame.grid(row=row, column=2, padx=2)
            self.font_color_var = tk.StringVar(value=self._get_short_font_color())
            font_color_combo = ttk.Combobox(font_color_frame, textvariable=self.font_color_var,
                                            values=FONT_COLOR_OPTIONS,
                                            width=4, state="readonly")
            font_color_combo.pack(side=tk.LEFT)
            font_color_combo.bind("<<ComboboxSelected>>", lambda e: self._on_font_color_changed())
            self.widget_factory.create_tooltip(self, font_color_combo, TOOLTIP_FONT_COLOR)
    
            # 背景颜色
            bg_color_frame = ttk.Frame(self)
            bg_color_frame.grid(row=row, column=3, padx=2)
            self.bg_color_var = tk.StringVar(value=self._get_short_bg_color())
            bg_color_combo = ttk.Combobox(bg_color_frame, textvariable=self.bg_color_var,
                                          values=BG_COLOR_OPTIONS,
                                          width=4, state="readonly")
            bg_color_combo.pack(side=tk.LEFT)
            bg_color_combo.bind("<<ComboboxSelected>>", lambda e: self._on_bg_color_changed())
            self.widget_factory.create_tooltip(self, bg_color_combo, TOOLTIP_BG_COLOR)
    
            # 置顶
            self.always_on_top_var = tk.BooleanVar(value=self.config.get("always_on_top", False))
            top_check = ttk.Checkbutton(self, text="", variable=self.always_on_top_var)
            top_check.grid(row=row, column=4, padx=2)
            top_check.configure(command=self._toggle_always_on_top)
            self.widget_factory.create_tooltip(self, top_check, TOOLTIP_ALWAYS_ON_TOP)
    
            # 分组显隐按钮
            self.toggle_btn = tk.Button(self, text=TOGGLE_BTN_HIDE, font=("Arial", 8),
                                        bg="gray90", relief="raised", width=1, height=1,
                                        command=self._toggle_group_visibility)
            self.toggle_btn.grid(row=row, column=5, padx=2)
    
        def _apply_initial_values(self):
            self.font_size_var.set(self.config.get("list_font_size", DEFAULT_FONT_SIZE))
            self.font_color_var.set(self._get_short_font_color())
            self.bg_color_var.set(self._get_short_bg_color())
            self.always_on_top_var.set(self.config.get("always_on_top", False))
    
        def _get_short_font_color(self):
            full = self.config.get("list_font_color", DEFAULT_FONT_COLOR)
            return self.color_mgr.get_short_color(full, is_background=False)
    
        def _get_short_bg_color(self):
            full = self.config.get("list_bg_color", DEFAULT_BG_COLOR)
            return self.color_mgr.get_short_color(full, is_background=True)
    
        def _clear_placeholder(self, event):
            if self.search_entry.get() == SEARCH_PLACEHOLDER:
                self.search_entry.delete(0, tk.END)
                self.search_entry.config(foreground="black")
    
        def _set_placeholder(self, event):
            if not self.search_entry.get():
                self.search_entry.insert(0, SEARCH_PLACEHOLDER)
                self.search_entry.config(foreground="gray")
    
        def _on_search_input(self, *args):
            if self._search_debounce_id:
                self.after_cancel(self._search_debounce_id)
                self._search_debounce_id = None
            self._search_debounce_id = self.after(SEARCH_DEBOUNCE_MS, self._perform_search)
    
        def _perform_search(self):
            self._search_debounce_id = None
            keyword = self.get_search_keyword()
            self.on_search(keyword)
    
        def _on_font_size_changed(self):
            size = self.font_size_var.get()
            self.config.set("list_font_size", size)
            self.on_font_changed()
    
        def _on_font_color_changed(self):
            short = self.font_color_var.get()
            full = self.color_mgr.get_full_color(short, is_background=False)
            self.config.set("list_font_color", full)
            self.on_font_changed()
    
        def _on_bg_color_changed(self):
            short = self.bg_color_var.get()
            full = self.color_mgr.get_full_color(short, is_background=True)
            self.config.set("list_bg_color", full)
            self.on_font_changed()
    
        def _toggle_always_on_top(self):
            self.on_toggle_top(self.always_on_top_var.get())
    
        def _toggle_group_visibility(self):
            self.on_toggle_group()
    
        def set_toggle_button_visible(self, visible: bool):
            if visible:
                self.toggle_btn.grid(row=0, column=5, padx=2)
            else:
                self.toggle_btn.grid_remove()
    
        def update_toggle_button_text(self, group_visible: bool):
            if group_visible:
                self.toggle_btn.config(text=TOGGLE_BTN_HIDE)
            else:
                self.toggle_btn.config(text=TOGGLE_BTN_SHOW)
    
        def get_search_keyword(self) -> str:
            keyword = self.search_var.get()
            return "" if keyword == SEARCH_PLACEHOLDER else keyword
    
    
    # ==================== 主窗口 ====================
    class ScriptManager:
        def __init__(self, root: tk.Tk):
            self.root = root
            self.config = ConfigManager()
            self.storage = DataStorage()
            self.group_visible = True
            self.color_mgr = ColorManager()
            self.bg_color = self.color_mgr.get_tk_color(
                self.config.get("list_bg_color", DEFAULT_BG_COLOR)
            )
    
            self._setup_window()
            self._create_ui()
            self._initialize_display()
    
        def _setup_window(self):
            geometry = self.config.get("window_geometry", WINDOW_GEOMETRY)
            self.root.title(self.config.get("window_title", WINDOW_TITLE))
            self.root.geometry(geometry)
            try:
                self.root.iconbitmap(default=ICON_FILE)
            except Exception:
                pass
            if self.config.get("always_on_top", False):
                self.root.attributes('-topmost', True)
            self.root.protocol("WM_DELETE_WINDOW", self._on_closing)
    
        def _on_closing(self):
            self.config.set("window_geometry", self.root.geometry(), save=True)
            # 保存分组宽度(仅在分组可见时)
            if hasattr(self, 'paned') and self.group_visible:
                try:
                    if self.paned.panes():
                        coord = self.paned.sash_coord(0)
                        if coord and isinstance(coord, tuple) and len(coord) > 0:
                            width = int(coord[0])
                            if width > 0:
                                self.config.set("group_panel_width", width, save=True)
                except Exception:
                    pass
            self.storage.save(async_mode=False)
            self.config.save()
            self.root.destroy()
    
        def _create_ui(self):
            main_frame = ttk.Frame(self.root)
            main_frame.pack(fill=tk.BOTH, expand=True)
    
            self.settings_bar = SettingsBar(
                main_frame, self.config,
                on_font_changed=self._on_settings_changed,
                on_search=self._on_search,
                on_toggle_group=self._toggle_group_visibility,
                on_toggle_top=self._toggle_always_on_top
            )
            self.settings_bar.pack(fill=tk.X, pady=0)
    
            self.paned = tk.PanedWindow(
                main_frame, orient=tk.HORIZONTAL,
                sashrelief="raised",
                sashwidth=6,
                bd=0,
                relief="flat"
            )
            self.paned.pack(fill=tk.BOTH, expand=True)
            self.paned.config(bg=self.bg_color)
    
            group_width = self.config.get("group_panel_width", 150)
    
            self.group_panel = GroupPanel(self.paned, self.storage, self._on_group_selected)
            self.paned.add(self.group_panel.frame, width=group_width)
    
            self.script_panel = ScriptPanel(self.paned, self.storage, self.config)
            self.paned.add(self.script_panel.frame, width=600)
    
            self.settings_bar.set_toggle_button_visible(True)
    
        def _initialize_display(self):
            groups = self.storage.get_groups()
            if groups:
                self.group_panel.set_current_group(groups[0].name)
                self.script_panel.set_group(groups[0].name)
                self.script_panel.refresh()
    
        def _on_settings_changed(self):
            self.bg_color = self.color_mgr.get_tk_color(
                self.config.get("list_bg_color", DEFAULT_BG_COLOR)
            )
            self.paned.config(bg=self.bg_color)
            self.group_panel.refresh_appearance(self.bg_color)
            self.script_panel.refresh_appearance()
    
        def _on_group_selected(self, group_name: str):
            self.script_panel.set_group(group_name)
            self.script_panel.refresh(self.settings_bar.get_search_keyword())
    
        def _on_search(self, keyword: str):
            self.script_panel.refresh(keyword)
    
        def _toggle_group_visibility(self):
            if self.group_visible:
                try:
                    if self.paned.panes():
                        coord = self.paned.sash_coord(0)
                        if coord and isinstance(coord, tuple) and len(coord) > 0:
                            width = int(coord[0])
                            if width > 0:
                                self.config.set("group_panel_width", width, save=False)
                except Exception:
                    pass
                self.paned.forget(self.group_panel.frame)
                self.settings_bar.update_toggle_button_text(False)
                self.group_visible = False
            else:
                children = self.paned.panes()
                for child in children:
                    self.paned.forget(child)
                width = self.config.get("group_panel_width", 150)
                self.paned.add(self.group_panel.frame, width=width)
                self.paned.add(self.script_panel.frame, width=600)
                self.settings_bar.update_toggle_button_text(True)
                self.group_visible = True
    
        def _toggle_always_on_top(self, is_top: bool):
            self.root.attributes('-topmost', is_top)
            self.config.set("always_on_top", is_top)
    
    
    # ==================== 入口 ====================
    def main():
        print("常用语管理")
        print("=" * 50)
        print("启动中...")
    
        try:
            root = tk.Tk()
            # 强制窗口显示
            root.deiconify()
            root.lift()
            root.update()
            app = ScriptManager(root)
            root.mainloop()
        except Exception as e:
            print(f"启动失败: {e}")
            import traceback
            traceback.print_exc()
            try:
                root = tk.Tk()
                root.withdraw()
                tk.messagebox.showerror("严重错误", f"程序启动失败:\n{e}")
                root.destroy()
            except:
                pass
            input("按Enter键退出...")
    
    
    if __name__ == "__main__":
        main()

免费评分

参与人数 1吾爱币 +3 热心值 +1 收起 理由
苏紫方璇 + 3 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

zhengxiurong 发表于 2026-6-26 01:22
大佬很牛!我也想跟大佬们一起学习,但pycarm我自有体验的,各位大佬有pycharm2026的激活工具不,或者激活码都可以,大佬们能分享给我一下不,十分的需要。感谢大佬们!
kmoui 发表于 2026-6-26 00:21
这一类的软件好像没有能实现点击某一条,然后自动插入到光标,或者是可以拖拽插入到光标处的。
pinghost 发表于 2026-6-25 18:23
这个挺好玩,我现在都是用搜狗输入法的快捷短语来实现快速话术回复,又多了一个选择,感谢!
webshow 发表于 2026-6-25 19:04
现在到处都是AI。
chlong 发表于 2026-6-25 22:53
支持起来吧
wyxnyj 发表于 2026-6-26 07:20
支持一下,跑销售的朋友可以重点关注,销售就是靠话术。
lbdcyy 发表于 2026-6-26 08:26
可以试下
 楼主| 提拉米苏子冉 发表于 2026-6-26 08:38
zhengxiurong 发表于 2026-6-26 01:22
大佬很牛!我也想跟大佬们一起学习,但pycarm我自有体验的,各位大佬有pycharm2026的激活工具不,或者激活 ...

我也是小白,代码全部是DS生成的
飘浮 发表于 2026-6-26 09:04
看起来不错。。
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-7-10 18:50

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表