当前位置:首页>python>PythonEnglish Typing Tutor - 英语指法打字练习系统

PythonEnglish Typing Tutor - 英语指法打字练习系统

  • 2026-08-21 04:02:28
PythonEnglish Typing Tutor - 英语指法打字练习系统

English Typing Tutor - 英语指法打字练习系统

项目简介

English Typing Tutor 是一款面向中小学生和英语初学者的英语指法打字练习系统。本项目将标准盲打指法教学与单词背诵相结合,帮助用户在练习打字的同时记忆英语单词。系统采用纯前端技术栈实现,无需后端服务器支持,所有数据存储在浏览器本地,完全离线可用,不收集任何用户隐私数据。

本系统主打轻量化、无广告、教育向设计风格,适合课堂教学辅助和家庭自学使用。


技术架构

技术栈

层级
技术选型
说明
结构层
HTML5
语义化页面结构
样式层
CSS3
纯 CSS 实现,无第三方 UI 框架依赖
逻辑层
Vanilla JavaScript (ES6+)
模块化设计,零依赖
存储层
localStorage
浏览器本地持久化
OCR支持
pdf.js (可选 CDN)
PDF 文本提取

项目结构

english-typing-tutor/
├── index.html              # 主页面入口
├── README.md               # 项目说明文档
├── css/
│   └── style.css           # 全局样式表
└── js/
    ├── app.js              # 主应用入口,事件绑定与初始化
    ├── keyboard.js         # 虚拟键盘渲染与指法映射
    ├── practice.js         # 练习流程控制与输入判定逻辑
    ├── wordbank.js         # 词库管理(导入/编辑/存储/统计)
    └── stats.js            # 实时数据统计与面板更新

模块职责划分

  • wordbank.js:词库的增删改查、JSON/PDF 导入、易错词管理、练习记录、localStorage 持久化
  • keyboard.js:QWERTY 虚拟键盘渲染、手指分区配色、按键高亮、指法提示文本生成
  • practice.js:练习模式状态机、输入字符判定、单词进度推进、组完成判定、音效播放
  • stats.js:正确率计算、WPM 速度统计、进度数据汇总、面板 DOM 更新
  • app.js:各模块初始化调度、DOM 事件监听绑定、文件导入处理、弹窗控制

功能模块详解

一、词库管理模块

词库是整个系统的数据基础,支持多种方式构建和维护:

1. 内置默认词库

系统预置 30 个基础英语单词(含中文释义),覆盖 A-Z 字母开头的常用词,确保用户开箱即用无需额外导入。

2. PDF 课文导入

支持批量上传 PDF 格式的英语课文文件,系统自动从中提取所有英文单词(2个字母及以上),自动去重后加入词库。如浏览器引入了 pdf.js 库,则使用专业 PDF 解析能力;否则回退为基础文本提取模式。

3. JSON 词库导入

支持导入标准 JSON 格式的自定义词库文件,格式为:

[
  {"en""apple""zh""苹果"},
  {"en""banana""zh""香蕉"}
]

导入时自动与已有词库合并去重。

4. 手动编辑

通过弹窗编辑器,用户可以逐行编辑词库内容,格式为 英文单词|中文释义,支持批量增删改操作。

5. 数据统计

侧边栏实时展示三类核心数据:总词数、已练习词数、错过词数,帮助用户直观了解学习进度。


二、练习模式模块

系统提供四种差异化的练习标签,覆盖不同学习场景:

模式
说明
适用场景
课文词模式
使用导入的课文词库顺序练习
配合教材同步学习
基准键指法
使用 home row 字母组成的单词练习
新手指法入门训练
易错词专项
仅练习之前输错过的单词
针对性巩固薄弱词汇
中文默写
隐藏英文,仅显示中文释义要求默写
深度记忆检测

用户可通过滑块自定义每组练习的单词数量(5-30个),灵活控制单次练习强度。


三、实时数据统计面板

顶部统计栏实时展示五项核心指标:

  1. 打字正确率:正确按键次数 / 总按键次数 × 100%
  2. 打字速度 WPM:每分钟完成的单词数(Words Per Minute)
  3. 本组进度:当前组内已完成单词数 / 本组总单词数
  4. 总词库进度:本次会话已练习总数 / 词库总词数
  5. 默写完成进度:默写模式下的完成情况

数据在每次按键后即时更新,为用户提供实时的学习反馈。


四、打字交互主界面

核心交互流程:

  1. 界面上方显示当前单词的中文释义(默写模式仅显示中文)
  2. 居中放大展示待输入的英文单词,每个字母独立着色显示输入状态
  3. 下方单行输入框实时接收键盘输入
  4. 输入正确的字母标记为绿色,自动推进到下一个字母
  5. 输入错误的字母触发红色闪烁提示,不推进位置,该词记入易错词库
  6. 整个单词输入完成后自动跳转下一词
  7. 一组单词全部完成后展示正确率和速度汇总

输入规则

  • 仅接受英文字母输入,自动屏蔽中文输入法干扰(通过 composition 事件拦截)
  • 大小写开关开启后严格匹配,关闭则忽略大小写差异
  • 支持开启/关闭按键音效反馈

五、可视化指法教学虚拟键盘

系统底部渲染完整的三行 QWERTY 标准键盘加空格键,采用手指分区马卡龙低饱和度配色方案:

颜色
对应手指
负责按键区域
粉色 (fce4ec)
小指
Q A Z / P
黄色 (fff9c4)
无名指
W S X / O L
青绿色 (e0f7fa)
中指
E D C / I K
绿色 (e8f5e9)
食指
R F V T G B / Y H N U J M
灰色 (f5f5f5)
拇指
空格键

实时高亮机制:每当用户需要输入下一个字母时,虚拟键盘上对应的按键会放大并添加阴影效果高亮显示,同时文字区域提示该字母应使用哪只手的哪个手指按下,强制规范盲打指法习惯。


六、辅助功能

  • 练习记录清空:一键清除所有练习进度和易错词记录,重新开始
  • 词库批量编辑:弹窗式文本编辑器,支持快速修改所有单词和释义
  • 数据本地持久化:词库、易错词、已练习词均自动保存到浏览器 localStorage
  • 键盘快捷键:按 Enter 键可快速开始练习或切换下一组

UI 设计规范

本项目严格遵循以下视觉约束:

  • 配色方案:纯白底色 + 浅灰分割线 + 浅蓝渐变主按钮 + 马卡龙键盘配色
  • 布局结构:左右分栏,左侧 280px 配置面板,右侧弹性练习主区域
  • 卡片设计:12px 圆角 + 轻微阴影(0 2px 8px rgba(0,0,0,0.04))
  • 字体方案:系统字体栈,优先使用苹方/微软雅黑,确保中英文清晰显示
  • 交互反馈:按钮悬停微上浮、输入错误抖动动画、正确输入绿色渐变
  • 响应式适配:900px 以下断点自动切换为上下布局

使用指南

快速开始

  1. 直接在浏览器中打开 index.html 文件即可运行
  2. 点击左侧「开始练习」按钮,系统加载默认词库开始练习
  3. 在输入框中输入显示的英文单词,观察虚拟键盘的指法提示
  4. 一组完成后查看正确率和速度,点击「下一组」继续练习

导入自定义词库

方法一:JSON 导入

准备 JSON 文件(数组格式,每项包含 en 和 zh 字段),点击「导入 JSON 词库」按钮选择文件即可。

方法二:PDF 导入

直接上传英语课文 PDF 文件,系统自动提取其中所有英文单词。提取后的单词释义默认为「待补充释义」,可通过编辑器手动补充。

如需完整 PDF 解析能力,请在 index.html 的 <head> 标签中加入:

<scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>

方法三:手动编辑

点击「编辑词库」按钮,在弹窗中按 单词|释义 格式逐行编辑,保存即生效。

模式选择建议

学习阶段
推荐模式
建议每组词数
零基础入门
基准键指法
5-8 个
日常练习
课文词模式
10-15 个
考前复习
中文默写
10-20 个
薄弱巩固
易错词专项
全部

桌面客户端打包

本项目为纯前端应用,可通过以下方式打包为 Windows 桌面客户端:

方案一:Electron 打包

npm init -y
npm install electron --save-dev
# 配置 main.js 加载 index.html
npx electron-packager . EnglishTypingTutor --platform=win32 --arch=x64

方案二:Tauri 打包(推荐,体积更小)

npm create tauri-app
# 将项目文件放入 src 目录
npm run tauri build

方案三:PWA 模式

添加 manifest.json 和 Service Worker 即可支持浏览器安装为桌面应用。


数据存储说明

所有数据均存储在浏览器 localStorage 中,键名如下:

存储键
内容
格式
typing_tutor_wordbank
完整词库
JSON 数组
typing_tutor_errors
易错词列表
JSON 数组(单词字符串)
typing_tutor_practiced
已练习词列表
JSON 数组(单词字符串)

清空浏览器缓存或点击「清空练习记录」按钮可重置练习数据(词库保留)。


浏览器兼容性

浏览器
最低版本
支持状态
Chrome
60+
✅ 完全支持
Firefox
55+
✅ 完全支持
Edge
79+
✅ 完全支持
Safari
12+
✅ 完全支持
IE
-
❌ 不支持

后续扩展方向

  • [ ] 添加 Service Worker 实现完整 PWA 离线缓存
  • [ ] 集成 Tesseract.js 实现浏览器端 OCR 识别图片型 PDF
  • [ ] 添加学习日历和每日打卡统计
  • [ ] 支持多用户切换(基于 localStorage 命名空间隔离)
  • [ ] 添加单词发音功能(Web Speech API)
  • [ ] 支持自定义键盘配色主题
  • [ ] 添加打字速度历史趋势图表(Canvas/SVG)
  • [ ] 支持导出学习报告为 PDF

开源协议

本项目仅供学习和教育用途,代码完全开源,可自由修改和分发。


联系与反馈

如有功能建议或使用问题,欢迎提交 Issue 或 Pull Request。

"""英语指法打字练习系统 - Python GUI 版本基于 tkinter 实现,完整离线桌面客户端"""import tkinter as tkfrom tkinter import ttk, filedialog, messagebox, simpledialogimport jsonimport osimport timeimport reimport threading===== 数据存储路径 =====DATA_DIR = os.path.join(os.path.expanduser("~"), ".english_typing_tutor")WORDBANK_FILE = os.path.join(DATA_DIR, "wordbank.json")ERRORS_FILE = os.path.join(DATA_DIR, "errors.json")PRACTICED_FILE = os.path.join(DATA_DIR, "practiced.json")os.makedirs(DATA_DIR, exist_ok=True)===== 默认词库 =====DEFAULT_WORDS = [{"en""apple""zh""苹果"}, {"en""banana""zh""香蕉"},{"en""cat""zh""猫"}, {"en""dog""zh""狗"},{"en""elephant""zh""大象"}, {"en""flower""zh""花"},{"en""green""zh""绿色"}, {"en""happy""zh""快乐的"},{"en""ice""zh""冰"}, {"en""jump""zh""跳"},{"en""king""zh""国王"}, {"en""lion""zh""狮子"},{"en""mother""zh""母亲"}, {"en""night""zh""夜晚"},{"en""orange""zh""橙子"}, {"en""pencil""zh""铅笔"},{"en""queen""zh""女王"}, {"en""rabbit""zh""兔子"},{"en""school""zh""学校"}, {"en""teacher""zh""老师"},{"en""umbrella""zh""雨伞"}, {"en""violet""zh""紫罗兰"},{"en""water""zh""水"}, {"en""yellow""zh""黄色"},{"en""zoo""zh""动物园"}, {"en""book""zh""书"},{"en""chair""zh""椅子"}, {"en""door""zh""门"},{"en""friend""zh""朋友"}, {"en""garden""zh""花园"},]HOMEROW_WORDS = [{"en""sad""zh""悲伤的"}, {"en""had""zh""有(过去式)"},{"en""ask""zh""问"}, {"en""fall""zh""落下"},{"en""half""zh""一半"}, {"en""hall""zh""大厅"},{"en""shall""zh""将"}, {"en""flash""zh""闪光"},{"en""glass""zh""玻璃"}, {"en""glad""zh""高兴的"},{"en""flag""zh""旗帜"}, {"en""salad""zh""沙拉"},{"en""dad""zh""爸爸"}, {"en""add""zh""添加"},{"en""all""zh""所有"}, {"en""gas""zh""气体"},{"en""lass""zh""少女"}, {"en""lad""zh""男孩"},{"en""gal""zh""女孩"}, {"en""dash""zh""冲刺"},]===== 手指分区映射 =====FINGER_MAP = {'q''pinky''a''pinky''z''pinky','w''ring''s''ring''x''ring','e''middle''d''middle''c''middle','r''index''f''index''v''index','t''index''g''index''b''index','y''index''h''index''n''index','u''index''j''index''m''index','i''middle''k''middle','o''ring''l''ring','p''pinky',' ''thumb'}FINGER_NAMES = {'pinky''小指''ring''无名指''middle''中指','index''食指''thumb''拇指'}FINGER_HAND = {'q''左手''w''左手''e''左手''r''左手''t''左手','a''左手''s''左手''d''左手''f''左手''g''左手','z''左手''x''左手''c''左手''v''左手''b''左手','y''右手''u''右手''i''右手''o''右手''p''右手','h''右手''j''右手''k''右手''l''右手','n''右手''m''右手'' ''任意'}手指分区颜色(马卡龙低饱和度配色)FINGER_COLORS = {'pinky''#fce4ec','ring''#fff9c4','middle''#e0f7fa','index''#e8f5e9','thumb''#f0f0f0'}FINGER_ACTIVE_COLORS = {'pinky''#f48fb1','ring''#fff176','middle''#4dd0e1','index''#66bb6a','thumb''#bdbdbd'}===== 词库管理类 =====class WordBankManager:def init(self):self.words = []self.error_words = set()self.practiced_words = set()self.load()def load(self):    if os.path.exists(WORDBANK_FILE):        with open(WORDBANK_FILE, 'r', encoding='utf-8'as f:            self.words = json.load(f)    else:        self.words = list(DEFAULT_WORDS)        self.save()    if os.path.exists(ERRORS_FILE):        with open(ERRORS_FILE, 'r', encoding='utf-8'as f:            self.error_words = set(json.load(f))    if os.path.exists(PRACTICED_FILE):        with open(PRACTICED_FILE, 'r', encoding='utf-8'as f:            self.practiced_words = set(json.load(f))def save(self):    with open(WORDBANK_FILE, 'w', encoding='utf-8'as f:        json.dump(self.words, f, ensure_ascii=False, indent=2)def save_errors(self):    with open(ERRORS_FILE, 'w', encoding='utf-8'as f:        json.dump(list(self.error_words), f, ensure_ascii=False)def save_practiced(self):    with open(PRACTICED_FILE, 'w', encoding='utf-8'as f:        json.dump(list(self.practiced_words), f, ensure_ascii=False)def get_all(self):    return self.wordsdef get_homerow(self):    return HOMEROW_WORDSdef get_errors(self):    return [w for w in self.words if w['en'in self.error_words]def add_error(self, word):    self.error_words.add(word)    self.save_errors()def mark_practiced(self, word):    self.practiced_words.add(word)    self.save_practiced()def get_stats(self):    return {        'total'len(self.words),        'practiced'len(self.practiced_words),        'errors'len(self.error_words)    }def import_json(self, data):    if isinstance(data, list):        new_words = [w for w in data if 'en' in w and 'zh' in w]        self.words.extend(new_words)        seen = set()        unique = []        for w in self.words:            key = w['en'].lower()            if key not in seen:                seen.add(key)                unique.append(w)        self.words = unique        self.save()def import_from_text(self, text):    english_words = re.findall(r'\b[a-zA-Z]{2,}\b', text)    unique = list(set(w.lower() for w in english_words))    existing = {w['en'].lower() for w in self.words}    added = 0    for w in unique:        if w not in existing:            self.words.append({'en': w, 'zh''待补充释义'})            added += 1    self.save()    return addeddef set_words(self, new_words):    self.words = new_words    self.save()def clear_records(self):    self.error_words.clear()    self.practiced_words.clear()    if os.path.exists(ERRORS_FILE):        os.remove(ERRORS_FILE)    if os.path.exists(PRACTICED_FILE):        os.remove(PRACTICED_FILE)===== 主应用窗口 =====class TypingTutorApp:def init(self, root):self.root = rootself.root.title("英语指法打字练习系统")self.root.geometry("1200x750")self.root.configure(bg='#f8f9fa')self.root.minsize(1000650)    # 数据    self.word_bank = WordBankManager()    self.current_mode = 'textbook'    self.current_words = []    self.current_index = 0    self.current_letter_index = 0    self.group_size = 10    self.is_active = False    self.case_sensitive = False    self.show_translation = True    self.sound_enabled = False    # 统计    self.correct_count = 0    self.total_keystrokes = 0    self.words_completed = 0    self.start_time = None    self.total_practiced_session = 0    self.dictation_completed = 0    # 键盘按键引用    self.key_labels = {}    self.build_ui()    self.update_word_stats()def build_ui(self):    # 顶部统计栏    self.build_stats_bar()    # 主区域    main_frame = tk.Frame(self.root, bg='#f8f9fa')    main_frame.pack(fill=tk.BOTH, expand=True, padx=12, pady=(012))    # 左侧面板    self.build_sidebar(main_frame)    # 右侧练习区    self.build_practice_area(main_frame)def build_stats_bar(self):    bar = tk.Frame(self.root, bg='white', relief='flat', bd=0)    bar.pack(fill=tk.X, padx=12, pady=(128))    # 外部圆角效果通过 padding 模拟    inner = tk.Frame(bar, bg='white', padx=20, pady=12)    inner.pack(fill=tk.X)    stats_data = [        ("正确率""accuracy""0%"),        ("速度 WPM""wpm""0"),        ("本组进度""group_prog""0/0"),        ("总词库进度""total_prog""0/0"),        ("默写完成""dict_prog""0/0"),    ]    self.stat_vars = {}    for i, (label, key, default) in enumerate(stats_data):        frame = tk.Frame(inner, bg='white')        frame.pack(side=tk.LEFT, expand=True)        tk.Label(frame, text=label, font=('Microsoft YaHei'9),                 fg='#888', bg='white').pack()        var = tk.StringVar(value=default)        self.stat_vars[key] = var        tk.Label(frame, textvariable=var, font=('Microsoft YaHei'16'bold'),                 fg='#4a9eff', bg='white').pack()def build_sidebar(self, parent):    sidebar = tk.Frame(parent, bg='#f8f9fa', width=260)    sidebar.pack(side=tk.LEFT, fill=tk.Y, padx=(012))    sidebar.pack_propagate(False)    # 词库管理卡片    card1 = self.create_card(sidebar, "词库管理")    tk.Button(card1, text="导入 PDF 课文", command=self.import_pdf,              bg='#74b9ff', fg='white', relief='flat', font=('Microsoft YaHei'10),              cursor='hand2').pack(fill=tk.X, pady=(04))    tk.Button(card1, text="导入 JSON 词库", command=self.import_json,              bg='#f0f0f0', fg='#555', relief='flat', font=('Microsoft YaHei'10),              cursor='hand2').pack(fill=tk.X, pady=(08))    stats_frame = tk.Frame(card1, bg='#f9f9f9', padx=8, pady=6)    stats_frame.pack(fill=tk.X, pady=(06))    self.lbl_total = tk.Label(stats_frame, text="总词: 0", font=('Microsoft YaHei'9),                              bg='#f9f9f9', fg='#666')    self.lbl_total.pack(side=tk.LEFT, expand=True)    self.lbl_practiced = tk.Label(stats_frame, text="已练: 0", font=('Microsoft YaHei'9),                                  bg='#f9f9f9', fg='#666')    self.lbl_practiced.pack(side=tk.LEFT, expand=True)    self.lbl_errors = tk.Label(stats_frame, text="错过: 0", font=('Microsoft YaHei'9),                               bg='#f9f9f9', fg='#666')    self.lbl_errors.pack(side=tk.LEFT, expand=True)    tk.Button(card1, text="编辑词库", command=self.edit_words,              bg='#f5f5f5', fg='#666', relief='flat', font=('Microsoft YaHei'9),              cursor='hand2').pack(fill=tk.X)    # 练习模式卡片    card2 = self.create_card(sidebar, "练习模式")    modes_frame = tk.Frame(card2, bg='white')    modes_frame.pack(fill=tk.X)    self.mode_buttons = {}    modes = [("课文词""textbook"), ("基准键""homerow"),             ("易错词""errors"), ("默写""dictation")]    for i, (text, mode) in enumerate(modes):        btn = tk.Button(modes_frame, text=text, font=('Microsoft YaHei'9),                        relief='flat', cursor='hand2',                        command=lambda m=mode: self.set_mode(m))        btn.grid(row=i // 2, column=i % 2, sticky='ew', padx=2, pady=2)        modes_frame.columnconfigure(i % 2, weight=1)        self.mode_buttons[mode] = btn    self.update_mode_buttons()    # 设置卡片    card3 = self.create_card(sidebar, "设置")    # 每组单词数    tk.Label(card3, text="每组单词数", font=('Microsoft YaHei'9),             bg='white', fg='#666').pack(anchor='w')    slider_frame = tk.Frame(card3, bg='white')    slider_frame.pack(fill=tk.X, pady=(08))    self.group_size_var = tk.IntVar(value=10)    self.group_slider = tk.Scale(slider_frame, from_=5, to=30, orient=tk.HORIZONTAL,                                 variable=self.group_size_var, command=self.on_slider_change,                                 bg='white', highlightthickness=0, troughcolor='#e0e0e0')    self.group_slider.pack(fill=tk.X)    # 开关选项    self.sound_var = tk.BooleanVar(value=False)    tk.Checkbutton(card3, text="按键音效", variable=self.sound_var,                   command=self.on_settings_change, bg='white',                   font=('Microsoft YaHei'9)).pack(anchor='w')    self.case_var = tk.BooleanVar(value=False)    tk.Checkbutton(card3, text="区分大小写", variable=self.case_var,                   command=self.on_settings_change, bg='white',                   font=('Microsoft YaHei'9)).pack(anchor='w')    self.trans_var = tk.BooleanVar(value=True)    tk.Checkbutton(card3, text="显示中文翻译", variable=self.trans_var,                   command=self.on_settings_change, bg='white',                   font=('Microsoft YaHei'9)).pack(anchor='w')    # 清空记录    card4 = self.create_card(sidebar, "")    tk.Button(card4, text="清空练习记录", command=self.clear_records,              bg='#ffe0e0', fg='#e74c3c', relief='flat', font=('Microsoft YaHei'10),              cursor='hand2').pack(fill=tk.X)def create_card(self, parent, title):    card = tk.Frame(parent, bg='white', padx=12, pady=10, relief='flat', bd=0)    card.pack(fill=tk.X, pady=(08))    if title:        tk.Label(card, text=title, font=('Microsoft YaHei'11'bold'),                 bg='white', fg='#555').pack(anchor='w', pady=(08))    return carddef build_practice_area(self, parent):    area = tk.Frame(parent, bg='white', padx=24, pady=20)    area.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)    # 中文释义    self.meaning_var = tk.StringVar(value="点击「开始练习」开始")    tk.Label(area, textvariable=self.meaning_var, font=('Microsoft YaHei'14),             fg='#888', bg='white').pack(pady=(108))    # 单词展示区域    self.word_frame = tk.Frame(area, bg='white', height=80)    self.word_frame.pack(fill=tk.X, pady=(016))    self.word_display_var = tk.StringVar(value="Ready")    self.word_label = tk.Label(self.word_frame, textvariable=self.word_display_var,                               font=('Consolas'36'bold'), fg='#ddd', bg='white')    self.word_label.pack()    # 字母逐个显示的 frame    self.letters_frame = tk.Frame(area, bg='white')    self.letters_frame.pack(pady=(016))    self.letter_labels = []    # 输入框    input_frame = tk.Frame(area, bg='white')    input_frame.pack(pady=(012))    self.input_var = tk.StringVar()    self.input_entry = tk.Entry(input_frame, textvariable=self.input_var,                                font=('Consolas'18), width=30, justify='center',                                relief='flat', bd=2, highlightthickness=2,                                highlightcolor='#74b9ff', highlightbackground='#e0e0e0')    self.input_entry.pack()    self.input_entry.bind('<KeyRelease>'self.on_key_release)    self.input_entry.bind('<Key>'self.on_key_press)    # 指法提示    self.hint_var = tk.StringVar(value="等待输入...")    tk.Label(area, textvariable=self.hint_var, font=('Microsoft YaHei'11),             fg='#888', bg='white').pack(pady=(016))    # 操作按钮    btn_frame = tk.Frame(area, bg='white')    btn_frame.pack(pady=(020))    self.btn_start = tk.Button(btn_frame, text="开始练习", command=self.start_practice,                               bg='#74b9ff', fg='white', relief='flat',                               font=('Microsoft YaHei'11), padx=20, pady=6, cursor='hand2')    self.btn_start.pack(side=tk.LEFT, padx=4)    self.btn_next = tk.Button(btn_frame, text="下一组", command=self.next_group,                              bg='#f0f0f0', fg='#555', relief='flat',                              font=('Microsoft YaHei'11), padx=20, pady=6,                              state=tk.DISABLED, cursor='hand2')    self.btn_next.pack(side=tk.LEFT, padx=4)    # 虚拟键盘    self.keyboard_frame = tk.Frame(area, bg='white')    self.keyboard_frame.pack(fill=tk.X, pady=(100))    self.build_keyboard()def build_keyboard(self):    rows = [        ['q''w''e''r''t''y''u''i''o''p'],        ['a''s''d''f''g''h''j''k''l'],        ['z''x''c''v''b''n''m']    ]    for row_keys in rows:        row_frame = tk.Frame(self.keyboard_frame, bg='white')        row_frame.pack(pady=2)        for key in row_keys:            finger = FINGER_MAP.get(key, 'thumb')            color = FINGER_COLORS[finger]            lbl = tk.Label(row_frame, text=key.upper(), width=4, height=2,                           bg=color, font=('Consolas'11'bold'), fg='#555',                           relief='flat', bd=1)            lbl.pack(side=tk.LEFT, padx=2)            self.key_labels[key] = lbl    # 空格键    space_frame = tk.Frame(self.keyboard_frame, bg='white')    space_frame.pack(pady=2)    space_lbl = tk.Label(space_frame, text="SPACE", width=30, height=2,                         bg=FINGER_COLORS['thumb'], font=('Consolas'11),                         fg='#888', relief='flat', bd=1)    space_lbl.pack()    self.key_labels[' '] = space_lbldef highlight_key(self, letter):    """高亮指定按键"""    # 先清除所有高亮    for key, lbl in self.key_labels.items():        finger = FINGER_MAP.get(key, 'thumb')        lbl.configure(bg=FINGER_COLORS[finger], fg='#555',                      font=('Consolas'11'bold'))    # 高亮目标键    key = letter.lower()    if key in self.key_labels:        finger = FINGER_MAP.get(key, 'thumb')        self.key_labels[key].configure(            bg=FINGER_ACTIVE_COLORS[finger], fg='white',            font=('Consolas'12'bold'))def clear_highlight(self):    for key, lbl in self.key_labels.items():        finger = FINGER_MAP.get(key, 'thumb')        lbl.configure(bg=FINGER_COLORS[finger], fg='#555',                      font=('Consolas'11'bold'))def get_finger_hint(self, letter):    key = letter.lower()    finger = FINGER_MAP.get(key, '')    hand = FINGER_HAND.get(key, '')    if not finger:        return ''    finger_name = FINGER_NAMES.get(finger, '')    return f"{hand}{finger_name} → {letter.upper()} | 手腕放松,先准后快"# ===== 练习逻辑 =====def start_practice(self):    self.correct_count = 0    self.total_keystrokes = 0    self.words_completed = 0    self.start_time = None    self.current_index = 0    self.current_letter_index = 0    self.total_practiced_session = 0    self.dictation_completed = 0    # 获取当前模式词库    if self.current_mode == 'textbook':        self.current_words = self.word_bank.get_all()[:self.group_size]    elif self.current_mode == 'homerow':        self.current_words = self.word_bank.get_homerow()[:self.group_size]    elif self.current_mode == 'errors':        self.current_words = self.word_bank.get_errors()[:self.group_size]        if not self.current_words:            messagebox.showinfo("提示""暂无易错词,先练习其他模式吧!")            return    elif self.current_mode == 'dictation':        self.current_words = self.word_bank.get_all()[:self.group_size]    self.is_active = True    self.input_entry.configure(state=tk.NORMAL)    self.input_entry.focus_set()    self.input_var.set('')    self.btn_next.configure(state=tk.DISABLED)    self.show_current_word()    self.update_stats_display()def next_group(self):    self.correct_count = 0    self.total_keystrokes = 0    self.words_completed = 0    self.start_time = None    self.current_index = 0    self.current_letter_index = 0    all_words = self.word_bank.get_homerow() if self.current_mode == 'homerow' \        else self.word_bank.get_all()    offset = self.total_practiced_session    self.current_words = all_words[offset:offset + self.group_size]    if not self.current_words:        self.total_practiced_session = 0        self.current_words = all_words[:self.group_size]    self.is_active = True    self.input_entry.configure(state=tk.NORMAL)    self.input_entry.focus_set()    self.input_var.set('')    self.btn_next.configure(state=tk.DISABLED)    self.show_current_word()    self.update_stats_display()def show_current_word(self):    if self.current_index >= len(self.current_words):        return    word = self.current_words[self.current_index]    # 显示中文释义    if self.current_mode == 'dictation' or self.show_translation:        self.meaning_var.set(word['zh'])    else:        self.meaning_var.set('')    # 显示单词(逐字母)    self.display_word_letters(word['en'])    # 指法提示    first_letter = word['en'][0]    self.hint_var.set(self.get_finger_hint(first_letter))    self.highlight_key(first_letter)def display_word_letters(self, word):    # 清除旧的字母显示    for lbl in self.letter_labels:        lbl.destroy()    self.letter_labels.clear()    # 隐藏 word_label    self.word_label.pack_forget()    # 如果是默写模式,显示提示文字    if self.current_mode == 'dictation':        self.word_display_var.set("请根据中文释义输入英文单词")        self.word_label.configure(font=('Microsoft YaHei'14), fg='#ccc')        self.word_label.pack()        return    # 显示逐字母    for widget in self.letters_frame.winfo_children():        widget.destroy()    self.letter_labels = []    for i, ch in enumerate(word):        if i == 0:            fg = '#0984e3'        else:            fg = '#ccc'        lbl = tk.Label(self.letters_frame, text=ch, font=('Consolas'36'bold'),                       fg=fg, bg='white')        lbl.pack(side=tk.LEFT, padx=1)        self.letter_labels.append(lbl)def update_letter_display(self):    word = self.current_words[self.current_index]['en']    for i, lbl in enumerate(self.letter_labels):        if i < self.current_letter_index:            lbl.configure(fg='#27ae60')  # 正确-绿色        elif i == self.current_letter_index:            lbl.configure(fg='#0984e3')  # 当前-蓝色        else:            lbl.configure(fg='#ccc')  # 待输入-灰色def on_key_press(self, event):    """按键按下事件 - 过滤非英文输入"""    if not self.is_active:        return 'break'    char = event.char    # 允许退格和删除    if event.keysym in ('BackSpace''Delete'):        return 'break'  # 不允许回退    # 只允许英文字母    if char and not char.isalpha():        return 'break'    return Nonedef on_key_release(self, event):    """按键释放事件 - 处理输入逻辑"""    if not self.is_active:        return    char = event.char    if not char or not char.isalpha():        self.input_var.set('')        return    if self.start_time is None:        self.start_time = time.time()    word = self.current_words[self.current_index]    target_word = word['en']    target_char = target_word[self.current_letter_index]    # 判断正确性    if self.case_sensitive:        is_correct = char == target_char    else:        is_correct = char.lower() == target_char.lower()    self.total_keystrokes += 1    if is_correct:        self.correct_count += 1        self.current_letter_index += 1        # 播放正确音效        if self.sound_enabled:            self.play_beep(True)        # 更新字母显示        if self.current_mode != 'dictation':            self.update_letter_display()        # 指法提示 - 下一个字母        if self.current_letter_index < len(target_word):            next_letter = target_word[self.current_letter_index]            self.hint_var.set(self.get_finger_hint(next_letter))            self.highlight_key(next_letter)        # 单词完成        if self.current_letter_index >= len(target_word):            self.word_complete()    else:        # 错误处理        if self.sound_enabled:            self.play_beep(False)        self.word_bank.add_error(word['en'])        self.hint_var.set(            f"✗ 正确应为 \"{target_char.upper()}\" - {self.get_finger_hint(target_char)}")        # 输入框闪红(简单实现)        self.input_entry.configure(highlightbackground='#e74c3c', highlightcolor='#e74c3c')        self.root.after(300lambdaself.input_entry.configure(            highlightbackground='#e0e0e0', highlightcolor='#74b9ff'))    # 清空输入框    self.input_var.set('')    self.update_stats_display()def word_complete(self):    self.words_completed += 1    self.word_bank.mark_practiced(self.current_words[self.current_index]['en'])    self.total_practiced_session += 1    if self.current_mode == 'dictation':        self.dictation_completed += 1    self.current_index += 1    self.current_letter_index = 0    self.input_var.set('')    if self.current_index >= len(self.current_words):        self.group_complete()    else:        self.show_current_word()def group_complete(self):    self.is_active = False    self.input_entry.configure(state=tk.DISABLED)    self.clear_highlight()    # 显示完成信息    accuracy = self.get_accuracy()    wpm = self.get_wpm()    self.meaning_var.set(f"正确率: {accuracy}% | 速度: {wpm} WPM")    # 清除字母显示    for lbl in self.letter_labels:        lbl.destroy()    self.letter_labels.clear()    self.word_display_var.set("🎉 本组完成!")    self.word_label.configure(font=('Microsoft YaHei'24), fg='#27ae60')    self.word_label.pack()    self.hint_var.set("可以重新练习本组或切换下一组")    self.btn_next.configure(state=tk.NORMAL)    self.update_word_stats()def play_beep(self, correct):    """简单的系统提示音"""    try:        import winsound        freq = 880 if correct else 220        winsound.Beep(freq, 80)    except Exception:        pass# ===== 统计相关 =====def get_accuracy(self):    if self.total_keystrokes == 0:        return 0    return round((self.correct_count / self.total_keystrokes) * 100)def get_wpm(self):    if not self.start_time or self.words_completed == 0:        return 0    minutes = (time.time() - self.start_time) / 60    if minutes < 0.01:        return 0    return round(self.words_completed / minutes)def update_stats_display(self):    self.stat_vars['accuracy'].set(f"{self.get_accuracy()}%")    self.stat_vars['wpm'].set(str(self.get_wpm()))    total_in_group = len(self.current_words) if self.current_words else 0    self.stat_vars['group_prog'].set(f"{min(self.current_index + 1, total_in_group)}/{total_in_group}")    all_words = self.word_bank.get_homerow() if self.current_mode == 'homerow' \        else self.word_bank.get_all()    self.stat_vars['total_prog'].set(f"{self.total_practiced_session}/{len(all_words)}")    dict_total = len(self.current_words) if self.current_mode == 'dictation' else 0    self.stat_vars['dict_prog'].set(f"{self.dictation_completed}/{dict_total}")def update_word_stats(self):    stats = self.word_bank.get_stats()    self.lbl_total.configure(text=f"总词: {stats['total']}")    self.lbl_practiced.configure(text=f"已练: {stats['practiced']}")    self.lbl_errors.configure(text=f"错过: {stats['errors']}")# ===== 模式与设置 =====def set_mode(self, mode):    self.current_mode = mode    self.update_mode_buttons()def update_mode_buttons(self):    for m, btn in self.mode_buttons.items():        if m == self.current_mode:            btn.configure(bg='#74b9ff', fg='white')        else:            btn.configure(bg='#f0f0f0', fg='#555')def on_slider_change(self, val):    self.group_size = int(val)def on_settings_change(self):    self.sound_enabled = self.sound_var.get()    self.case_sensitive = self.case_var.get()    self.show_translation = self.trans_var.get()# ===== 词库操作 =====def import_pdf(self):    filepaths = filedialog.askopenfilenames(        title="选择 PDF 文件",        filetypes=[("PDF 文件""*.pdf"), ("文本文件""*.txt")]    )    if not filepaths:        return    total_added = 0    for fp in filepaths:        try:            text = self.extract_text_from_file(fp)            added = self.word_bank.import_from_text(text)            total_added += added        except Exception as e:            messagebox.showerror("错误"f"处理 {os.path.basename(fp)} 时出错:\n{e}")    if total_added > 0:        messagebox.showinfo("导入成功"f"共导入 {total_added} 个新单词")        self.update_word_stats()def extract_text_from_file(self, filepath):    """从文件提取文本"""    ext = os.path.splitext(filepath)[1].lower()    if ext == '.txt':        with open(filepath, 'r', encoding='utf-8', errors='ignore'as f:            return f.read()    if ext == '.pdf':        # 尝试使用 PyPDF2 或 pdfplumber        try:            import PyPDF2            with open(filepath, 'rb'as f:                reader = PyPDF2.PdfReader(f)                text = ''                for page in reader.pages:                    text += page.extract_text() or ''                return text        except ImportError:            pass        try:            import pdfplumber            with pdfplumber.open(filepath) as pdf:                text = ''                for page in pdf.pages:                    text += page.extract_text() or ''                return text        except ImportError:            pass        # 回退:尝试直接读取二进制中的文本        with open(filepath, 'rb'as f:            content = f.read().decode('utf-8', errors='ignore')            words = re.findall(r'\b[a-zA-Z]{2,}\b', content)            if words:                return ' '.join(words)        raise Exception("需要安装 PyPDF2 或 pdfplumber 库来解析 PDF\n"                        "运行: pip install PyPDF2")    # 其他格式尝试直接读取    with open(filepath, 'r', encoding='utf-8', errors='ignore'as f:        return f.read()def import_json(self):    filepath = filedialog.askopenfilename(        title="选择 JSON 词库文件",        filetypes=[("JSON 文件""*.json")]    )    if not filepath:        return    try:        with open(filepath, 'r', encoding='utf-8'as f:            data = json.load(f)        self.word_bank.import_json(data)        self.update_word_stats()        messagebox.showinfo("成功""词库导入成功!")    except Exception as e:        messagebox.showerror("错误"f"导入失败:\n{e}")def edit_words(self):    """弹出词库编辑窗口"""    edit_win = tk.Toplevel(self.root)    edit_win.title("编辑词库")    edit_win.geometry("500x450")    edit_win.configure(bg='white')    edit_win.transient(self.root)    edit_win.grab_set()    tk.Label(edit_win, text="格式:每行一个,英文单词|中文释义",             font=('Microsoft YaHei'10), fg='#888', bg='white').pack(        anchor='w', padx=16, pady=(124))    text_frame = tk.Frame(edit_win, bg='white', padx=16)    text_frame.pack(fill=tk.BOTH, expand=True)    scrollbar = tk.Scrollbar(text_frame)    scrollbar.pack(side=tk.RIGHT, fill=tk.Y)    text_widget = tk.Text(text_frame, font=('Consolas'11), wrap=tk.NONE,                          yscrollcommand=scrollbar.set, relief='flat',                          highlightthickness=1, highlightbackground='#e0e0e0')    text_widget.pack(fill=tk.BOTH, expand=True)    scrollbar.config(command=text_widget.yview)    # 填充内容    words = self.word_bank.get_all()    content = '\n'.join(f"{w['en']}|{w['zh']}" for w in words)    text_widget.insert('1.0', content)    def save_words():        text = text_widget.get('1.0', tk.END).strip()        lines = [l.strip() for l in text.split('\n'if l.strip()]        new_words = []        for line in lines:            parts = line.split('|')            en = parts[0].strip() if parts else ''            zh = parts[1].strip() if len(parts) > 1 else '待补充释义'            if en:                new_words.append({'en': en, 'zh': zh})        self.word_bank.set_words(new_words)        self.update_word_stats()        edit_win.destroy()        messagebox.showinfo("成功""词库已保存!")    btn_frame = tk.Frame(edit_win, bg='white', padx=16, pady=12)    btn_frame.pack(fill=tk.X)    tk.Button(btn_frame, text="保存", command=save_words,              bg='#74b9ff', fg='white', relief='flat',              font=('Microsoft YaHei'11), padx=20, cursor='hand2').pack(side=tk.RIGHT)def clear_records(self):    if messagebox.askyesno("确认""确定要清空所有练习记录吗?\n词库不会被删除。"):        self.word_bank.clear_records()        self.update_word_stats()        messagebox.showinfo("完成""练习记录已清空")===== 程序入口 =====if name == 'main':root = tk.Tk()app = TypingTutorApp(root)root.mainloop()

最新文章

随机文章

基本 文件 流程 错误 SQL 调试
  1. 请求信息 : 2026-08-21 18:19:46 HTTP/2.0 GET : https://f.mffb.com.cn/a/504028.html
  2. 运行时间 : 0.233657s [ 吞吐率:4.28req/s ] 内存消耗:4,917.05kb 文件加载:140
  3. 缓存信息 : 0 reads,0 writes
  4. 会话信息 : SESSION_ID=daf02015ff31cccb4067bc9de5bd29df
  1. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/public/index.php ( 0.79 KB )
  2. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/autoload.php ( 0.17 KB )
  3. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/autoload_real.php ( 2.49 KB )
  4. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/platform_check.php ( 0.90 KB )
  5. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/ClassLoader.php ( 14.03 KB )
  6. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/autoload_static.php ( 4.90 KB )
  7. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper.php ( 8.34 KB )
  8. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-validate/src/helper.php ( 2.19 KB )
  9. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/helper.php ( 1.47 KB )
  10. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/stubs/load_stubs.php ( 0.16 KB )
  11. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Exception.php ( 1.69 KB )
  12. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-container/src/Facade.php ( 2.71 KB )
  13. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/deprecation-contracts/function.php ( 0.99 KB )
  14. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/polyfill-mbstring/bootstrap.php ( 8.26 KB )
  15. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/polyfill-mbstring/bootstrap80.php ( 9.78 KB )
  16. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/var-dumper/Resources/functions/dump.php ( 1.49 KB )
  17. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-dumper/src/helper.php ( 0.18 KB )
  18. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/var-dumper/VarDumper.php ( 4.30 KB )
  19. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/App.php ( 15.30 KB )
  20. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-container/src/Container.php ( 15.76 KB )
  21. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/container/src/ContainerInterface.php ( 1.02 KB )
  22. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/provider.php ( 0.19 KB )
  23. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Http.php ( 6.04 KB )
  24. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper/Str.php ( 7.29 KB )
  25. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Env.php ( 4.68 KB )
  26. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/common.php ( 0.03 KB )
  27. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/helper.php ( 18.78 KB )
  28. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Config.php ( 5.54 KB )
  29. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/app.php ( 0.95 KB )
  30. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/cache.php ( 0.78 KB )
  31. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/console.php ( 0.23 KB )
  32. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/cookie.php ( 0.56 KB )
  33. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/database.php ( 2.48 KB )
  34. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/Env.php ( 1.67 KB )
  35. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/filesystem.php ( 0.61 KB )
  36. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/lang.php ( 0.91 KB )
  37. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/log.php ( 1.35 KB )
  38. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/middleware.php ( 0.19 KB )
  39. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/route.php ( 1.89 KB )
  40. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/session.php ( 0.57 KB )
  41. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/trace.php ( 0.34 KB )
  42. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/view.php ( 0.82 KB )
  43. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/event.php ( 0.25 KB )
  44. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Event.php ( 7.67 KB )
  45. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/service.php ( 0.13 KB )
  46. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/AppService.php ( 0.26 KB )
  47. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Service.php ( 1.64 KB )
  48. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Lang.php ( 7.35 KB )
  49. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/lang/zh-cn.php ( 13.70 KB )
  50. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/Error.php ( 3.31 KB )
  51. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/RegisterService.php ( 1.33 KB )
  52. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/services.php ( 0.14 KB )
  53. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/PaginatorService.php ( 1.52 KB )
  54. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/ValidateService.php ( 0.99 KB )
  55. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/ModelService.php ( 2.04 KB )
  56. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/Service.php ( 0.77 KB )
  57. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Middleware.php ( 6.72 KB )
  58. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/BootService.php ( 0.77 KB )
  59. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/Paginator.php ( 11.86 KB )
  60. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-validate/src/Validate.php ( 63.20 KB )
  61. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/Model.php ( 23.55 KB )
  62. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/Attribute.php ( 21.05 KB )
  63. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/AutoWriteData.php ( 4.21 KB )
  64. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/Conversion.php ( 6.44 KB )
  65. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/DbConnect.php ( 5.16 KB )
  66. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/ModelEvent.php ( 2.33 KB )
  67. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/RelationShip.php ( 28.29 KB )
  68. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/contract/Arrayable.php ( 0.09 KB )
  69. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/contract/Jsonable.php ( 0.13 KB )
  70. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/contract/Modelable.php ( 0.09 KB )
  71. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Db.php ( 2.88 KB )
  72. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/DbManager.php ( 8.52 KB )
  73. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Log.php ( 6.28 KB )
  74. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Manager.php ( 3.92 KB )
  75. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/log/src/LoggerTrait.php ( 2.69 KB )
  76. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/log/src/LoggerInterface.php ( 2.71 KB )
  77. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Cache.php ( 4.92 KB )
  78. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/simple-cache/src/CacheInterface.php ( 4.71 KB )
  79. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper/Arr.php ( 16.63 KB )
  80. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/cache/driver/File.php ( 7.84 KB )
  81. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/cache/Driver.php ( 9.03 KB )
  82. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/CacheHandlerInterface.php ( 1.99 KB )
  83. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/Request.php ( 0.09 KB )
  84. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Request.php ( 55.78 KB )
  85. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/middleware.php ( 0.25 KB )
  86. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Pipeline.php ( 2.61 KB )
  87. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/TraceDebug.php ( 3.40 KB )
  88. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/middleware/SessionInit.php ( 1.94 KB )
  89. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Session.php ( 1.80 KB )
  90. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/session/driver/File.php ( 6.27 KB )
  91. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/SessionHandlerInterface.php ( 0.87 KB )
  92. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/session/Store.php ( 7.12 KB )
  93. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Route.php ( 23.73 KB )
  94. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleName.php ( 5.75 KB )
  95. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Domain.php ( 2.53 KB )
  96. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleGroup.php ( 22.43 KB )
  97. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Rule.php ( 26.95 KB )
  98. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleItem.php ( 9.78 KB )
  99. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/route/app.php ( 1.72 KB )
  100. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/Route.php ( 4.70 KB )
  101. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/dispatch/Controller.php ( 4.74 KB )
  102. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Dispatch.php ( 10.44 KB )
  103. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/controller/Index.php ( 4.81 KB )
  104. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/BaseController.php ( 2.05 KB )
  105. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/facade/Db.php ( 0.93 KB )
  106. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/connector/Mysql.php ( 5.44 KB )
  107. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/PDOConnection.php ( 52.47 KB )
  108. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Connection.php ( 8.39 KB )
  109. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/ConnectionInterface.php ( 4.57 KB )
  110. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/builder/Mysql.php ( 16.58 KB )
  111. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Builder.php ( 24.06 KB )
  112. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/BaseBuilder.php ( 27.50 KB )
  113. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Query.php ( 15.71 KB )
  114. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/BaseQuery.php ( 45.13 KB )
  115. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/TimeFieldQuery.php ( 7.43 KB )
  116. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/AggregateQuery.php ( 3.26 KB )
  117. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ModelRelationQuery.php ( 20.07 KB )
  118. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ParamsBind.php ( 3.66 KB )
  119. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ResultOperation.php ( 7.01 KB )
  120. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/WhereQuery.php ( 19.37 KB )
  121. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/JoinAndViewQuery.php ( 7.11 KB )
  122. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/TableFieldInfo.php ( 2.63 KB )
  123. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/Transaction.php ( 2.77 KB )
  124. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/log/driver/File.php ( 5.96 KB )
  125. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/LogHandlerInterface.php ( 0.86 KB )
  126. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/log/Channel.php ( 3.89 KB )
  127. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/event/LogRecord.php ( 1.02 KB )
  128. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/Collection.php ( 16.47 KB )
  129. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/View.php ( 1.70 KB )
  130. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/View.php ( 4.39 KB )
  131. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Response.php ( 8.81 KB )
  132. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/response/View.php ( 3.29 KB )
  133. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Cookie.php ( 6.06 KB )
  134. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-view/src/Think.php ( 8.38 KB )
  135. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/TemplateHandlerInterface.php ( 1.60 KB )
  136. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/Template.php ( 46.61 KB )
  137. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/template/driver/File.php ( 2.41 KB )
  138. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/template/contract/DriverInterface.php ( 0.86 KB )
  139. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/runtime/temp/067d451b9a0c665040f3f1bdd3293d68.php ( 11.98 KB )
  140. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/Html.php ( 4.42 KB )
  1. CONNECT:[ UseTime:0.001127s ] mysql:host=127.0.0.1;port=3306;dbname=f_mffb;charset=utf8mb4
  2. SHOW FULL COLUMNS FROM `fenlei` [ RunTime:0.001460s ]
  3. SELECT * FROM `fenlei` WHERE `fid` = 0 [ RunTime:0.000712s ]
  4. SELECT * FROM `fenlei` WHERE `fid` = 63 [ RunTime:0.000779s ]
  5. SHOW FULL COLUMNS FROM `set` [ RunTime:0.001305s ]
  6. SELECT * FROM `set` [ RunTime:0.000552s ]
  7. SHOW FULL COLUMNS FROM `article` [ RunTime:0.001324s ]
  8. SELECT * FROM `article` WHERE `id` = 504028 LIMIT 1 [ RunTime:0.001316s ]
  9. UPDATE `article` SET `lasttime` = 1787307586 WHERE `id` = 504028 [ RunTime:0.047971s ]
  10. SELECT * FROM `fenlei` WHERE `id` = 66 LIMIT 1 [ RunTime:0.000795s ]
  11. SELECT * FROM `article` WHERE `id` < 504028 ORDER BY `id` DESC LIMIT 1 [ RunTime:0.001411s ]
  12. SELECT * FROM `article` WHERE `id` > 504028 ORDER BY `id` ASC LIMIT 1 [ RunTime:0.001373s ]
  13. SELECT * FROM `article` WHERE `id` < 504028 ORDER BY `id` DESC LIMIT 10 [ RunTime:0.002415s ]
  14. SELECT * FROM `article` WHERE `id` < 504028 ORDER BY `id` DESC LIMIT 10,10 [ RunTime:0.008247s ]
  15. SELECT * FROM `article` WHERE `id` < 504028 ORDER BY `id` DESC LIMIT 20,10 [ RunTime:0.002291s ]
0.237325s