简介:一份基于Flet框架的桌面登录验证系统模板,适合Python初学者以及需要快速搭建桌面端身份验证功能的中小型应用开发者。系统完整覆盖用户注册、登录验证、SQLite持久化存储三大模块,并采用统一的页面切换机制,实现注册页与登录页灵活互转,用户输入口令时以隐藏形式显示,注册和登录操作会动态给出成功或失败的彩色提示,交互直观。压缩包共5个文件,以Python主脚本为核心,另含SQLite数据库文件、界面截图、GIF效果演示和说明文档,整体大小仅243KB,代码结构清晰便于二次开发。已有115人学习使用。借助这份模板,可掌握Flet中密码输入控件的安全配置、数据库连接与关闭的资源管理、页面状态切换等关键实现,也可作为个人桌面工具或教务管理软件的登录模块参考。
1. 基于Flet框架的桌面程序登录验证:为什么SQLite和自定义组件模板把开发成本压下来
做内部工具时,我越来越倾向把单机桌面程序作为第一选择:不需要部署服务端,不需要配置环境,双击就能用。Flet 框架恰好把这类开发的体验拉到了一个新高度,它写起来像前端,跑起来却是本地桌面窗口,登录验证这类页面逻辑用 Python 直接描述,SQLite 承担本地数据存储,再把高频复用的登录表单抽成自定义组件模板,一套组合拳下来项目结构和维护成本都非常可控。这套方案的受众其实很明确:想用 Python 交付桌面小工具、又不想在 PySide6 的复杂度和 Tkinter 的简陋布局之间反复纠结的开发者。下面我按“骨架 → 登录验证 → 数据库 → 组件模板 → 避坑”的顺序,把每一层的做法和参数讲透。
2. 用Flet搭建桌面登录验证骨架:页面切换、会话状态和回调写法
先解决一个基本问题:Flet 的桌面程序到底是怎么组织页面和事件的。很多人第一次上手会把它当传统 GUI 框架用,结果在布局和事件绑定上反复翻车。其实 Flet 的模型非常接近 Web 前端的组件思维,理解了这一点,后面接 SQLite 和自定义组件模板就顺了。
2.1 最小可运行骨架:ft.app 入口、窗口尺寸和热重载
Flet 的桌面入口是ft.app(target=main),函数接收一个page: ft.Page对象,这个对象就是整个窗口的根。所有的控件都挂在page下面,通过page.add()加入控件树,改完属性后调page.update()让界面刷新。
import flet as ft def main(page: ft.Page): page.title = "桌面登录验证 Demo" page.window.width = 960 page.window.height = 640 page.padding = 40 page.add(ft.Text("应用骨架已启动,控件树挂载到这里")) ft.app(target=main)这段代码的逻辑很直白:page.window.width和page.window.height决定了窗口初始尺寸,单位是逻辑像素;page.padding控制整个页面的内边距,能避免控件贴边,看起来没那么局促。ft.app(target=main)在本地以桌面窗口模式启动,不额外指定view参数时就是默认窗口模式。开发时用flet run或直接跑脚本,保存代码后窗口会自动热重载,这是比 PySide6 顺手很多的地方,改完布局不用重启进程。
参数方面需要注意:ft.app(main)和ft.app(target=main)是等价写法,但显式写target=可读性更好;窗口尺寸只是初始值,运行时也能通过page.window.width = 1200调整。第一次跑通这个骨架后,你会意识到 Flet 的页面操作本质上就是一棵动态控件树,后续所有登录验证逻辑都建立在“改树、刷新”这两个动作之上。
2.2 登录到主界面的页面切换:page.clean 与 page.go 的选择
登录验证的核心动作是:填账号密码,点登录,校验通过后从登录页切到主页面。Flet 里做页面切换有两种常见路径——page.go()配合page.views做页面栈管理,以及page.clean()清空后重加控件树。
先说登录页面的构造。用户名输入框、密码输入框、登录按钮、错误提示文本,这四个控件构成一个典型登录表单。密码框要设置password=True让输入变成掩码,新版本 Flet 还支持can_reveal_password=True,输入框右侧会出现一个小眼睛图标,点击后可以明文查看密码,体验比传统桌面程序好不少。
import flet as ft def main(page: ft.Page): page.title = "登录验证" page.window.width = 960 page.window.height = 640 username = ft.TextField(label="用户名", width=300) password = ft.TextField(label="密码", width=300, password=True, can_reveal_password=True) tip = ft.Text("", color=ft.Colors.RED_400, size=13) def do_login(e): uname = username.value.strip() pwd = password.value or "" if uname == "admin" and pwd == "123456": page.clean() page.add(ft.Column([ ft.Text(f"欢迎回来:{uname}", size=24), ft.Text("这里是主界面占位内容", size=14) ], alignment=ft.MainAxisAlignment.CENTER)) else: tip.value = "账号或密码错误,请重试" password.value = "" page.update() password.on_submit = do_login login_btn = ft.FilledButton("登录", on_click=do_login) form = ft.Column( [username, password, tip, login_btn], alignment=ft.MainAxisAlignment.CENTER, horizontal_alignment=ft.CrossAxisAlignment.CENTER, spacing=16 ) page.add(form) ft.app(target=main)这里有个参数值得展开:do_login同时绑定了密码框的on_submit和按钮的on_click,这样用户在密码框里按回车也能触发登录,不用非得去点鼠标,这是桌面软件的基本交互习惯。校验通过后我用page.clean()把登录页控件全部移除,再重新page.add()主界面内容。
那什么时候用page.go()?当你的软件有“返回上一页”的诉求时——比如主界面里点“修改密码”跳到子页面,子页面能退回主界面——才需要页面栈。登录场景用page.clean()更干净,因为登录页的历史状态不该被保留。如果你用page.go("/main")实现跳转,用户按返回键还有可能回到登录页,这反而是安全隐患。所以我的建议是:登录验证后的跳转,用clean()清栈;后续功能页面间的导航,再考虑go()。
2.3 登录失败与接口防抖:错误提示、输入重置和重复提交拦截
登录失败的处理看似简单,但细节决定体验。上面代码里已经展示了三个要点:错误提示用独立的tip文本控件,失败时把值设为提示文案并page.update();密码框清空、用户名保留,这样用户改正密码时不用重新输账号;调用password.value or ""避免用户输入 None 导致拼接报错。
还有一个容易踩的坑:重复提交。Flet 的事件循环是单线程的,但快速连点登录按钮会造成同一个登录函数被连续多次触发,尤其在后面接入 SQLite 之后,重复提交会给数据库带来无意义的写入压力。常见做法是在登录函数里加一个状态锁:
is_submitting = False def do_login(e): nonlocal is_submitting if is_submitting: return is_submitting = True try: # 原有的登录校验逻辑 pass finally: is_submitting = False我一般把is_submitting放在外层函数作用域,用nonlocal声明,这样登录期间按钮点击只认第一次。更完整的方案是把按钮设为禁用状态,等校验完成再恢复——不过要注意,btn.disabled = True之后必须调用page.update(),否则界面上的按钮仍然可点。这个防抖逻辑后面接入数据库后同样适用,登录查询耗时通常在一两毫秒到几十毫秒之间,连点几下的压力虽然不至于打挂 SQLite,但会制造出一堆没必要的查询日志。这个细节在做自定义组件模板时还会用到,先留个印象。
3. 把SQLite接进登录验证:用户表、密码哈希和十万条数据的查询参数
登录验证不接数据库就只能当 Demo,接上 SQLite 才算能用的桌面程序。SQLite 和 Flet 的组合有一个天然优势:两边都是轻量级,整个软件不用装任何外部服务。接下来要解决四件事:数据库文件放哪、怎么建用户表、密码怎么存、以及真实数据量下查询怎么保证速度。
3.1 连接管理与建表:sqlite3 初始化、DB 路径和 WAL 设置
SQLite 的接入不用 ORM,Python 自带的sqlite3模块就够了,核心就是一套连接管理函数。我习惯把所有数据库操作收拢到一个db.py模块里,页面层只负责调用,不直接持有连接对象。
import sqlite3 import os APP_DIR = os.path.join(os.path.expanduser("~"), ".login_demo") os.makedirs(APP_DIR, exist_ok=True) DB_PATH = os.path.join(APP_DIR, "app.db") def get_conn(): conn = sqlite3.connect(DB_PATH, timeout=5) conn.row_factory = sqlite3.Row conn.execute("PRAGMA journal_mode=WAL") conn.execute("PRAGMA busy_timeout=3000") return conn def init_db(): with get_conn() as conn: conn.execute(""" CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, created_at TEXT DEFAULT (datetime('now', 'localtime')) ) """)这里有几个参数值得逐一说清楚。数据库文件放在用户主目录下的.login_demo文件夹,而不是和脚本同目录。原因是桌面程序将来要用 PyInstaller 打包,打包后脚本所在的__file__路径会指向一个临时解压目录,往里写文件重装或者系统清理后数据就丢了。放用户目录才符合“本地单机数据”的预期。timeout=5是sqlite3.connect的默认等待锁的秒数,配合PRAGMA busy_timeout=3000(毫秒)双保险,能让写入冲突时的等待窗口更可控。PRAGMA journal_mode=WAL把日志模式切成 WAL,读写不互相阻塞,对 Flet 这种事件循环里偶尔穿插查询的场景收益明显。
conn.row_factory = sqlite3.Row这行也很关键,它让查询结果按列名访问而不是只给数字下标,后面写row["username"]可读性好太多。init_db()里用CREATE TABLE IF NOT EXISTS保证幂等,每次启动程序都可以执行一次。用户表的设计遵循最小原则:自增主键、用户名唯一、密码哈希、创建时间,后续要扩展字段就用ALTER TABLE加,后面 3.3 节会讲到 SQLite 对改字段类型的限制。
3.2 注册与登录 SQL 实操:参数绑定、PBKDF2 哈希和唯一约束
密码存储是一条红线:绝对不能明文入库。常见做法是加盐 PBKDF2 哈希,虽然主流密码学更推荐 bcrypt 或 argon2,但 Python 内置的hashlib就能实现 PBKDF2,少一个第三方依赖。注册函数负责把用户名和密码哈希写入users表,查询函数负责按用户名取出行记录用于登录验证。
import hashlib import secrets def hash_password(password: str, salt: bytes = None) -> str: if salt is None: salt = secrets.token_bytes(16) dk = hashlib.pbkdf2_hmac("sha256", password.encode(), salt, 100_000) return salt.hex() + ":" + dk.hex() def verify_password(password: str, stored: str) -> bool: salt_hex, hash_hex = stored.split(":") dk = hashlib.pbkdf2_hmac( "sha256", password.encode(), bytes.fromhex(salt_hex), 100_000 ) return dk.hex() == hash_hex def create_user(username: str, password: str): with get_conn() as conn: conn.execute( "INSERT INTO users(username, password_hash) VALUES(?, ?)", (username, hash_password(password)) ) def find_user(username: str): with get_conn() as conn: row = conn.execute( "SELECT * FROM users WHERE username = ?", (username,) ).fetchone() return dict(row) if row else None两个 SQL 语句都用了?参数绑定,而不是把变量直接拼进 SQL 字符串,这是防注入的基本底线。桌面程序虽然不像 Web 端那样暴露在公网下,但输入框里传入'; DROP TABLE users;--这种内容是真实可能发生的,参数绑定从构造上杜绝了这类问题。哈希函数里secrets.token_bytes(16)生成 16 字节随机盐,PBKDF2 迭代十万次,对桌面程序的登录频率来说开销可接受(每次验证大约几十毫秒),安全性远高于简单hashlib.sha256(password.encode()).hexdigest()。
create_user里没有显式检查用户名是否存在,依赖的是users表上username TEXT NOT NULL UNIQUE这个唯一约束,插入重复用户名时sqlite3会抛IntegrityError。调用方捕获这个异常后转成“用户名已存在,请更换”的提示文案。登录验证则是组合逻辑:先find_user查出用户,不存在就直接返回账号错误;存在则调verify_password比对密码哈希,比对失败返回密码错误。写实际操作时,你会发现把“用户不存在”和“密码错误”分开提示会有安全问题,但它能让使用者明确知道该改哪个字段,桌面内部工具这点取舍可以接受,看你需求。
3.3 sqlite修改字段类型和查询性能:索引计划与连接参数
接入数据库后,两个高频问题立刻会暴露:表结构想改字段类型怎么办,以及数据量涨到十万条后查询还快不快。
先说改字段类型。SQLite 的ALTER TABLE只支持重命名表、重命名列、添加新列,不支持直接修改已有列的类型。比如你想把created_at从TEXT改成INTEGER存时间戳,直接跑ALTER TABLE users MODIFY COLUMN会报语法错误。标准做法是重建表:
def migrate_users(): with get_conn() as conn: conn.execute(""" CREATE TABLE users_new ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, created_at INTEGER ) """) conn.execute(""" INSERT INTO users_new(id, username, password_hash, created_at) SELECT id, username, password_hash, CAST(strftime('%s', created_at) AS INTEGER) FROM users """) conn.execute("DROP TABLE users") conn.execute("ALTER TABLE users_new RENAME TO users")这种迁移方式在 SQLite 的运维场景里已经是共识:新表造一个 → 数据拷过去 → 删旧表 → 改名。注意INSERT INTO ... SELECT里的类型转换要自己写,别指望数据库帮你转。写迁移脚本时建议放在init_db()之前执行,并且用PRAGMA user_version记录当前表结构版本号,避免重复迁移。每次改动结构都把版本号加一,程序启动时对比旧版本再决定跑哪些迁移脚本。
再说查询性能。“十万条数据 sqlite 查询需要多久”——这不是一个固定答案,完全看查询条件有没有走索引。users表的username列有UNIQUE约束,SQLite 会自动为它建唯一索引,所以WHERE username = ?这种等值查询在十万条数据下一般是亚毫秒到几毫秒级别。如果你写的是WHERE username LIKE '%admin%',前方通配符会让索引失效,变成全表扫描,十万条数据就可能要几十毫秒以上,数据量再涨一个量级就肉眼可见地卡。验证是否走了索引可以用:
EXPLAIN QUERY PLAN SELECT * FROM users WHERE username = 'admin';结果里出现SEARCH users USING INDEX就说明走对了。日常开发我还习惯开 WAL 后把synchronous从默认的 FULL 降为 NORMAL,对桌面单机应用来说可以显著减少写入时的磁盘同步开销,断电丢失一小段事务的风险在登录验证这种场景下可以接受。
4. 自定义组件模板:把登录表单封装成可复用控件的参数化设计
登录表单写两遍就会烦:登录页一遍、注册页一遍、管理员重置密码页面又要一遍。Flet 提供了ft.UserControl基类让我们做自定义组件模板,本质是把一组控件和交互逻辑包成一个整体,外部通过参数控制外观和行为。这一节回答三个问题:模板怎么写得像官方控件一样顺手、参数和回调怎么设计才符合实际需求、以及模板和业务逻辑的边界在哪。
4.1 LoginForm模板的最小实现:继承 UserControl 与 build 挂载
Flet 自定义组件的最基本的写法是继承ft.UserControl,重写build()方法,返回一棵控件树。build()返回的内容就是整个控件渲染到页面上的样子,外部使用page.add(LoginForm(...))就能把模板挂上去。
import flet as ft class LoginForm(ft.UserControl): def __init__( self, title: str = "登录", submit_text: str = "登 录", on_submit=None ): super().__init__() self.title = title self.submit_text = submit_text self.on_submit = on_submit self.username_field = ft.TextField( label="用户名", width=320, text_size=14, border_radius=8, autofocus=True ) self.password_field = ft.TextField( label="密码", width=320, password=True, can_reveal_password=True, text_size=14 ) self.error_text = ft.Text( "", size=12, color=ft.Colors.RED_400 ) self.submit_btn = ft.FilledButton( submit_text, on_click=self._handle_submit ) self.is_submitting = False def _handle_submit(self, e): if self.is_submitting: return username = (self.username_field.value or "").strip() password = self.password_field.value or "" if not username or not password: self.error_text.value = "请输入账号和密码" self.update() return if self.on_submit is not None: self.on_submit(username, password) elif username and password: self.error_text.value = "未绑定登录回调" self.update() def build(self): return ft.Column( [ ft.Text(self.title, size=22), self.username_field, self.password_field, self.error_text, self.submit_btn, ], spacing=16, horizontal_alignment=ft.CrossAxisAlignment.CENTER, )这个模板里有几个设计点值得强调。__init__中显示调用super().__init__()之后再把挂到模板上的成员属性建好,顺序不能反过来——控件的字段初始化依赖父类的基础状态,经验上先调父类再建子类属性最稳。_handle_submit里的self.is_submitting防抖逻辑和 2.3 节一致,说明模板内部也承载了交互保护。空值校验放在模板内,因为“能不能为空”是 UI 层就能断言的规则,不该下放到业务层。真正的登录业务通过on_submit回调抛出去,模板内只做两件事:收集输入、做前置校验、把合规的数据交出去。
使用时非常简单:
def handle_login(username, password): # 在这里调 find_user 和 verify_password pass form = LoginForm(title="用户登录", submit_text="登录", on_submit=handle_login) page.add(form)模板拿到了用户名和密码之后,业务层是查 SQLite 还是查远端接口,模板一概不关心。这是组件模板复用的前提:UI 层和业务层之间只有一个明确的回调契约。
4.2 模板参数与回调边界:验证规则注入、防抖状态和复用场景
把模板参数设计想清楚,复用时才不别扭。上面LoginForm的参数分为两类:展示参数(title、submit_text)和行为参数(on_submit)。展示参数决定形态,行为参数决定逻辑。如果你还想让模板支持更细的校验规则,比如用户名必须包含字母,可以再加一个validate_username回调参数,模板内部的_handle_submit先调它,返回错误文本就填入error_text,返回空字符串则继续:
def __init__(self, validate_username=None, validate_password=None): super().__init__() self.validate_username = validate_username or (lambda v: "") self.validate_password = validate_password or (lambda v: "")这样模板成了一个可配置的验证容器,传入不同的验证函数就得到不同的表单,但外观和提示结构完全一致。我一般建议控制在三四个参数以内,太多模板就会退化成“什么都想做”的上帝类,反而难维护。
复用边界要清醒:模板内部不要直接 importdb.py的find_user,也不要自己去查数据库。一旦模板持有数据库连接或查询逻辑,你换一个项目、换一张数据表时,整个模板就得重写。正确的边界是模板只处理“控件树 + 事件 + 状态”,数据库和业务全在外部回调里。这也是自定义组件模板最值钱的地方——它让你的登录框在任何页面出现时都长一个样、错误提示风格统一、防抖机制不用重新写。扩展到注册表单时,可以在模板上加一个确认密码的文本框,或者干脆做一个继承LoginForm的RegisterForm,改动量都很小。
5. Flet与SQLite结合的登录验证避坑清单:现象、原因和排查路径
这套组合自己在实际项目里踩过不少坑,集中在事件状态、数据库并发、路径与数据验证三个大方向。下面每条都按“现象 → 原因 → 解决”的顺序写,方便你直接对照排查。
5.1 回调与页面状态的坑:重复触发、self.page 未挂载
现象一:在密码框按回车,登录动作被执行了两次,日志里同一秒出现两条登录记录。这个现象在登录按钮获得焦点时更容易复现。
原因:按钮绑定了on_click、密码框绑定了on_submit,两个控件在同一轮事件循环里都接到了回车动作。Flet 底层的事件派发依赖控件树的焦点状态,当焦点落在按钮上时按回车,按钮的点击事件和密码框的提交事件同时被触发,两份处理逻辑都跑了一遍。
解决:最简单的防御就是 2.3 节的is_submitting状态锁:事件入口处判断,已经正在处理的就直接返回。如果表单里不只有登录一个主操作,再加一层通用事件锁也不为过,因为 Flet 的事件模型里,这类“看起来只绑了一次、实际触发两次”的问题,最彻底的办法就是统一用一个提交函数加状态锁兜底。
现象二:在LoginForm的build()方法里访问self.page并调用page.go(),运行时报AttributeError: 'NoneType' object has no attribute 'go'。
原因:UserControl的self.page属性只有在该控件被真正挂载到页面之后才有值。build()方法在控件树构建阶段就会执行,这个阶段控件尚未与page关联,所以拿不到页面对象。
解决:把self.page的访问推迟到事件回调里。事件触发时控件已经挂载完成,self.page是有效的。如果确实需要在构建阶段拿到页面引用,就在构造函数里把page作为参数传进来,但这样模板耦合了具体页面,复用性会下降。我习惯是模板尽量不主动访问page,需要跳转时把跳转逻辑放进on_submit回调里由外部处理。
5.2 SQLite锁与打包路径:database is locked、数据库文件写不进去
现象三:登录成功后快速点“注册”,或者连续快速提交两次写操作,控制台抛出sqlite3.OperationalError: database is locked。
原因:Flet 是事件循环模型,所有回调在同一线程里跑,但你写代码时不可能保证每个回调里的连接都及时关闭。常见情况是某个回调开了连接由于异常中断没提交,连接对象被垃圾回收时持有的写锁还悬着;另一个回调紧接着打开新连接写入,两个连接在同一时间竞争同一个数据库文件的写锁,默认超时不够,就直接报错。
解决:两个层面。第一,连接的获取和释放一定用with get_conn() as conn:,with块结束必然关闭连接,绝不在函数里裸调conn = get_conn()然后忘了conn.close()。第二,连接参数里把timeout设到 5 秒以上,再补一条PRAGMA busy_timeout=3000,给并发撞锁留足等待窗口。另外,WAL 模式也必须开,它让读写可以并行,减少“一个读卡住所有写”的尴尬局面。这条经验是真金白银换来的,项目刚上线时就因为这个被用户在注册页卡了三次。
现象四:PyInstaller 打包成 exe 后,第一次注册失败,打开日志发现数据库目录不存在或者没有写入权限。
原因:打包后的程序里,__file__指向的是 PyInstaller 临时解压目录,路径每次启动都不同,而且通常是只读的。如果沿用在开发时“脚本同目录放 app.db”的思路,打包后自然写不进去。
解决:数据库路径在 3.1 节里已经处理了——放在os.path.expanduser("~")下的应用专属目录。打包前还要确认目录创建逻辑os.makedirs(APP_DIR, exist_ok=True)在拿db.py模块时就会执行,而不是等到真正写库时才执行。调试时如果发现库文件没生成,八成是路径拼接错了,先用print(DB_PATH)确认实际指向,再决定改哪。
5.3 数据验证与迁移:db browser for sqlite 检查表和字段类型修改方法
现象五:登录验证逻辑明明是对的,但程序里查不到用户,或者看到的数据和预期不一致。
原因:SQLite 数据文件是独立的,开发时热重载会重启进程,如果两个进程用的DB_PATH指向不同位置(比如一个是相对路径 “app.db”,一个是绝对路径 “~/.login_demo/app.db”),就会发生“程序查不到刚才注册的用户”这种诡异情况。
解决:全局统一数据库路径,并且建议用 db browser for sqlite 这类图形工具直接打开.db文件看数据。它能直观浏览表结构、索引和数据行,三步就能定位问题:打开文件 → 看 users 表是否有数据 → 看 username 列值是否包含多余空格。桌面程序的 SQLite 排错,第一步永远是确认文件路径和真实数据,不要上来就怀疑代码逻辑。
现象六:想给已有表加一个字段,或者修改某个字段类型,发现 SQLite 的 ALTER 支持有限,报错后数据还在但结构改不了。
原因:如前所述,SQLite 对 ALTER TABLE 的支持很克制,能做的操作只有重命名和加列。修改字段类型不在支持范围内,需要重建表。
解决:用 3.3 节的“新表 + 拷贝 + 改名”三步式迁移。有条件的话,在开发早期就把datetime('now','localtime')这类文本格式换掉,统一存整数时间戳,避免后期频繁走迁移流程。建表时字段类型也尽量一步到位,TEXT 就是 TEXT,INTEGER 就是 INTEGER,别为了省事用 TEXT 存数字,后面要排序时才知道多难受。
6. 收拢成自己的项目启动模板:验证三步走与一次性调试的小技巧
这一节把前三章的骨架和逻辑合并成一个可以直接复制的项目模板,并给出验证方法和一个实用的调试技巧。这套结构我已经在多个内部工具里重复使用了,收益在于每开一个新项目,只需要复制过去改表名和业务回调。
项目文件组织我习惯这样拆:
app/ ├── main.py # Flet 入口,创建页面和路由 ├── db.py # SQLite 连接、DB 路径、建表与迁移 ├── auth.py # 密码哈希、注册、登录查询 └── components/ └── login_form.py # LoginForm 自定义组件模板main.py里创建一个全局的current_user会话变量,登录成功后赋值,主界面用它来显示当前用户名,登出时置空。整个流程是:main.py初始化数据库 → 生成LoginForm并绑定回调 → 回调调用auth.py的find_user和verify_password→ 验证通过后page.clean()切入主界面。
验证这套模板是否可用,我一般走三步。第一步,本地跑一个最小热重载调试:注册一个新账号,重启程序,再用该账号登录,确认用户数据在重启后依然存在。第二步,打开生成的app.db文件,用 db browser for sqlite 确认 users 表里有对应记录。第三步,如果需要验证查询性能,写一个临时脚本往表里插入十万条随机用户数据,然后执行EXPLAIN QUERY PLAN SELECT ... WHERE username = ?确认走了索引,再实际查询一个用户名看耗时——这一步能让你对“sqlite 查询需要多久”有个体感,而不是听别人说。
一个值得养成的调试技巧是给main()入口加一个环境变量开关,区分“免登录调试模式”和“正式登录模式”:
import os DEV_AUTH = os.getenv("DEV_AUTH") == "1" def main(page: ft.Page): if DEV_AUTH: page.clean() page.add(ft.Text("调试模式:跳过登录")) else: page.add(LoginForm(title="用户登录", on_submit=handle_login))开发时在命令行里DEV_AUTH=1 python main.py就能直接跳到主界面,省去反复输入账号密码的时间,功能逻辑却不会和登录验证搅在一起。这套习惯帮我省了大量重复劳动——现在的新项目第一天就能跑通登录、建表和页面切换,剩下的精力全放在业务本身。希望帮到你。
本文还有配套的精品资源,点击获取