This commit is contained in:
liubiren 2026-07-20 20:16:11 +08:00
parent ef9c607e6b
commit 1e4da027ea
4 changed files with 194 additions and 104 deletions

View File

@ -6,59 +6,66 @@ import reflex as rx
from application.pages.index import render_index from application.pages.index import render_index
app = rx.App( app = rx.App(
theme=rx.theme(),
style={ style={
"root": { "--prismui-background-color-1": "#ffffff", # 背景颜色:白色
"font-family": "HuaweiFont,Helvetica,Arial,PingFangSC-Regular,Hiragino Sans GB,Microsoft YaHei,微软雅黑,Microsoft JhengHei",
},
"*": {
"margin": "0",
"padding": "0",
"box_sizing": "border-box",
"background-color": "transparent",
"gap": "0",
},
# 覆盖伪元素 ::before 和 ::after
"*::before, *::after": {
"box_sizing": "border-box",
},
"--prismui-background-1": "linear-gradient(to bottom, #D0C9FF 0%, #E6D6F0 8%, #F1DBEA 12%, #C8DCFB 40%, #ABC6F6 60%, #87AEFE 90%)",
"--prismui-background-color-1": "#ffffff", # 纯白背景色
"--prismui-background-color-2": "#ebebeb", "--prismui-background-color-2": "#ebebeb",
"--prismui-background-color-3": "#f6f6f8", "--prismui-background-color-3": "#f6f6f8",
"--prismui-background-color-4": "#babbc0", "--prismui-background-color-4": "#babbc0",
"--prismui-background-1": "linear-gradient(to bottom, #D0C9FF 0%, #E6D6F0 8%, #F1DBEA 12%, #C8DCFB 40%, #ABC6F6 60%, #87AEFE 90%)", # 背景:用于主页面渐变背景
"--prismui-font-family": "HuaweiFont,Helvetica,Arial,PingFangSC-Regular,Hiragino Sans GB,Microsoft YaHei,微软雅黑,Microsoft JhengHei", # 字体
"--prismui-font-size-1": "12px",
"--prismui-font-size-2": "14px", # 字体大小:常用于正文
"--prismui-font-size-3": "20px",
"--prismui-font-weight-1": "normal", # 字体粗细:常用于正文
"--prismui-font-weight-2": "500",
"--prismui-font-weight-3": "700",
"--prismui-color-1": "#252b3a", # 颜色:深灰色,常用于标题和正文颜色
"--prismui-color-2": "#575d6c", # 颜色:中灰色,常用于副标题
"--prismui-color-3": "#babbc0", # 颜色:浅灰色,常用于占位符
"--prismui-color-4": "rgba(6, 10, 38, 0.24)", # 颜色:浅灰色,常用于不可点击元素
"--prismui-color-5": "#5e7ce0", # 颜色:蓝色,常用于可点击元素
"--prismui-color-6": "#f66f6a", # 颜色:红色,常用于错误提示
"--prismui-border-1": "1px solid rgba(6, 10, 38, 0.06)", # 边框样式
"--mc-float-block-shadow": "#d5d5d5", "--mc-float-block-shadow": "#d5d5d5",
"--devui-global-bg": "#f6f6f8", "--devui-global-bg": "#f6f6f8",
"--mc-global-bg": "linear-gradient(to bottom, #D0C9FF 0%, #E6D6F0 8%, #F1DBEA 12%, #C8DCFB 40%, #ABC6F6 60%, #87AEFE 90%)", "--mc-global-bg": "linear-gradient(to bottom, #D0C9FF 0%, #E6D6F0 8%, #F1DBEA 12%, #C8DCFB 40%, #ABC6F6 60%, #87AEFE 90%)",
"--mc-box-shadow": "#191919", "--mc-box-shadow": "#191919",
"--devui-shadow-length-connected-overlay": "0px 0px 12px 0px #000000", "--devui-shadow-length-connected-overlay": "0px 0px 12px 0px #000000",
"--prismui-box-shadow-1": "0 4px 12px #0000001a", # 导航按钮阴影 "--prismui-box-shadow-1": "0 4px 64px 0 #0000001a", # 登录卡片阴影
"--prismui-box-shadow-2": "0 4px 12px #0000001a", # 导航按钮阴影
"--devui-icon-fill-weak": "#babbc0", "--devui-icon-fill-weak": "#babbc0",
"--prismui-background-color-weak": "#babbc0", "--prismui-background-color-weak": "#babbc0",
"--devui-border-radius-feedback": "4px", "--devui-border-radius-feedback": "4px",
"--prismui-border-1": "1px solid rgba(6, 10, 38, 0.06)",
"--prismui-border-radius": "8px", "--prismui-border-radius": "8px",
"--prismui-border-radius-1": "8px", "--prismui-border-radius-1": "8px",
"--prismui-border-radius-2": "12px",
"--prismui-border-radius-3": "20px",
"--devui-border-radius-full": "100px", "--devui-border-radius-full": "100px",
"--font-family": "HuaweiFont,Helvetica,Arial,PingFangSC-Regular,Hiragino Sans GB,Microsoft YaHei,微软雅黑,Microsoft JhengHei", "box-shadow": "none",
"--prismui-font-size": "14px", "background-color": "var(--prismui-background-color-1)", # 默认背景颜色
"--prismui-font-size-1": "14px", "font_family": "var(--prismui-font-family)",
"--prismui-font-size-2": "12px", "font_size": "var(--prismui-font-size-2)",
"--prismui-font-size-sm": "12px", "font_weight": "var(--prismui-font-weight-1)",
"--prismui-font-weight-1": "700", "color": "var(--prismui-color-1)",
"--devui-font-size": "14px", "*": {
"--devui-font-size-lg": "14px", "margin": "0",
"--prismui-color-text": "#252b3a", "padding": "0",
"--prismui-color-text-light": "#babbc0", "gap": "0",
"--prismui-color-1": "#252b3a", # 文本颜色 },
"--prismui-color-2": "#575d6c", "*::before, *::after": {
"--prismui-color-3": "#babbc0", "box-sizing": "border-box",
"--devui-text-weak": "#575d6c", },
"--devui-light-text": "#ffffff", "* .rt-TextFieldRoot, * textarea": {
"--placeholder": "#babbc0", "border": "var(--prismui-border-1)",
"--devui-aide-text": "#71757f", "outline": "none",
"--devui-primary": "#5e7ce0", "box-shadow": "none",
"--devui-brand": "#5e7ce0", },
"--devui-brand-hover": "#7693f5", "*::placeholder": {
"--devui-dividing-line": "#f2f2f3", "color": "var(--prismui-color-3)",
"opacity": 1,
}, # 覆盖占位符样式
}, # 自定义主题颜色 }, # 自定义主题颜色
) # 此处变量名需使用 app ,具体原因目前尚不清楚 ) # 此处变量名需使用 app ,具体原因目前尚不清楚
# 注册首页路由 # 注册首页路由

View File

@ -44,7 +44,7 @@ def render_nav_button(
is_actived, "var(--prismui-background-color-1)", "transparent" is_actived, "var(--prismui-background-color-1)", "transparent"
), ),
border_radius="var(--prismui-border-radius-1)", border_radius="var(--prismui-border-radius-1)",
box_shadow=rx.cond(is_actived, "var(--prismui-box-shadow-1)", "none"), box_shadow=rx.cond(is_actived, "var(--prismui-box-shadow-2)", "none"),
), ),
# 渲染标签 # 渲染标签
rx.text( rx.text(
@ -126,39 +126,95 @@ def render_login_window() -> rx.Component:
rx.hstack( rx.hstack(
rx.vstack( rx.vstack(
rx.text( rx.text(
"登录", "登录解锁更多功能",
font_size="20px", line_height="32px",
font_weight="600", font_size="var(--prismui-font-size-3)",
font_weight="var(--prismui-font-weight-2)",
margin_bottom="24px", margin_bottom="24px",
), ),
rx.input( rx.input(
placeholder="请输入邮箱地址", placeholder="请输入邮箱地址",
value=AuthState.email, value=AuthState.email,
on_change=AuthState.set_email,
width="100%", width="100%",
height="45px",
margin_bottom="24px",
padding="4px",
border_radius="var(--prismui-border-radius-2)",
line_height="16px",
# 值变化事件:设置邮箱
on_change=AuthState.set_email,
# 获取焦点事件:清除提示信息
on_focus=AuthState.reset_notification,
), ),
# 自定义验证码输入框
rx.hstack( rx.hstack(
rx.input( rx.input(
placeholder="请输入验证码", placeholder="请输入验证码",
max_length=6,
value=AuthState.captcha, value=AuthState.captcha,
on_change=AuthState.set_captcha,
flex=1, flex=1,
height="100%",
padding="4px",
line_height="16px",
border="none",
# 值变化事件:设置验证码
on_change=AuthState.set_captcha,
style={
"&": {
"border": "none !important",
}, # 清除父元素边框
},
), ),
rx.button( rx.text(
# 若验证码发送倒计时大于0则显示倒计时否则显示获取验证码
rx.cond( rx.cond(
AuthState.captcha_sending_countdown > 0, AuthState.captcha_sending_countdown,
f"{AuthState.captcha_sending_countdown} s111111", f"{AuthState.captcha_sending_countdown} s",
"获取验证码", "获取验证码",
), ),
variant="outline", margin="0 8px",
width="120px", line_height="16px",
color=rx.cond(
AuthState.is_captcha_sending_disabled,
"var(--prismui-color-4)",
"var(--prismui-color-5)",
),
cursor="pointer",
on_click=AuthState.send_captcha, on_click=AuthState.send_captcha,
disabled=AuthState.captcha_sending_countdown > 0,
), ),
gap="8px",
width="100%", width="100%",
height="45px",
align_items="center",
border="var(--prismui-border-1)",
border_radius="var(--prismui-border-radius-2)",
),
rx.cond(
AuthState.captcha_sending_countdown,
rx.hstack(
rx.icon(
"circle-check",
width="14px",
height="14px",
color="var(--prismui-color-2)",
),
rx.text(
"验证码已发送,请耐心等待",
line_height="14px",
font_size="var(--prismui-font-size-1)",
color="var(--prismui-color-2)",
),
margin="10px 0",
gap="4px",
),
rx.text(
AuthState.notification,
width="100%",
text_align="left",
line_height="20px",
font_size="12px",
color="var(--prismui-color-6)",
),
), ),
rx.text(AuthState.notification, color="red", font_size="12px"),
rx.hstack( rx.hstack(
rx.checkbox( rx.checkbox(
checked=AuthState.is_policies_agreed, checked=AuthState.is_policies_agreed,
@ -184,12 +240,11 @@ def render_login_window() -> rx.Component:
color="#666", color="#666",
margin_top="8px", margin_top="8px",
), ),
width="360px", width="396px",
gap="16px", padding="40px 48px 0 48px",
padding="40px 32px", background_color="var(--prismui-background-color-1)",
align_items="start", align_items="center",
), ),
rx.divider(orientation="vertical"),
rx.vstack( rx.vstack(
rx.image( rx.image(
src="/qrcode.png", src="/qrcode.png",
@ -211,15 +266,18 @@ def render_login_window() -> rx.Component:
padding="40px 32px", padding="40px 32px",
style={"background": "#f8f9fc"}, style={"background": "#f8f9fc"},
), ),
border="1px solid #2222220f",
border_radius="var(--prismui-border-radius-3)",
box_shadow="0 4px 64px 0 #0000001a",
overflow="hidden",
), ),
position="fixed", position="absolute",
top="0", top="0",
left="0", left="0",
z_index="9999",
width="100vw", width="100vw",
height="100vh", height="100vh",
bg="rgba(255, 255, 255, 0.85)", backdrop_filter="blur(4px)",
z_index="9999",
overflow="hidden",
) )

View File

@ -9,6 +9,7 @@ import re
from typing import Any, AsyncGenerator, Coroutine, cast from typing import Any, AsyncGenerator, Coroutine, cast
from aiosmtplib import SMTP, SMTPException from aiosmtplib import SMTP, SMTPException
import reflex as rx import reflex as rx
from reflex.event import EventCallback
from application.states.database import DatabaseState from application.states.database import DatabaseState
@ -52,6 +53,14 @@ class AuthState(rx.State):
self.user_id = "" self.user_id = ""
@rx.event
def reset_notification(self) -> None:
"""
清除提示信息
:return: None
"""
self.notification = ""
@rx.event @rx.event
def set_email(self, email: str) -> None: def set_email(self, email: str) -> None:
""" """
@ -61,65 +70,50 @@ class AuthState(rx.State):
""" """
self.email = email.strip() self.email = email.strip()
@rx.event @rx.var
def set_captcha(self, captcha: str) -> None: def is_captcha_sending_disabled(self) -> bool:
""" """
设置验证码 验证码发送禁用状态
:param captcha: 验证码 :return: 禁用状态True 表示禁用False 表示未禁用
:return: None
""" """
self.captcha = captcha.strip()
@rx.event
def toggle_policies_agreed(self) -> None:
""" """
切换协议同意状态 邮箱为空或验证码发送倒计时大于0时禁用
:return: None
""" """
self.is_policies_agreed = not self.is_policies_agreed return not self.email or self.captcha_sending_countdown > 0
def validate_email(self) -> bool: def validate_email(self) -> bool:
""" """
邮箱 验邮箱
:return: 验证是否通过True 表示校验通过False 表示校验不通过 :return: 校验结果True 表示校验通过False 表示校验不通过
""" """
self.notification = ""
# 非空校验 # 非空校验
if not self.email: if not self.email:
self.notification = "请输入邮箱" self.notification = "请输入邮箱"
return False return False
# 格式校验 # 格式校验
"""
邮箱由用户名@域名组成其中
1用户名首字符为字母或数字中间可穿插下划线短横线加号或点符号后为字母或数字
2域名由若干子域名段和顶级域名段组成各段之间用点分隔其中
-- 子域名段首字符为字母或数字中间可穿插下划线或短横线符号后为字母或数字
-- 顶级域名段至少包含两个字母或数字
"""
if not re.fullmatch( if not re.fullmatch(
r"^[a-zA-Z0-9]+([_\-+.][a-zA-Z0-9_\-+]+)*@[a-zA-Z0-9]+(\-[a-zA-Z0-9\-]+)*(\.[a-zA-Z0-9]+(\-[a-zA-Z0-9\-]+)*)*\.[a-zA-Z0-9]{2,}$", r"^[a-zA-Z0-9]+([_\-+.][a-zA-Z0-9_\-+]+)*@[a-zA-Z0-9]+(\-[a-zA-Z0-9\-]+)*(\.[a-zA-Z0-9]+(\-[a-zA-Z0-9\-]+)*)*\.[a-zA-Z0-9]{2,}$",
self.email, self.email,
): ):
self.notification = "邮箱格式不正确,请重新输入" self.notification = "邮箱格式不正确,请重新输入"
return False return False
return True
def validate_captcha(self) -> bool:
"""
验证验证码
:return: 验证是否通过True 表示校验通过False 表示校验不通过
"""
self.notification = "" self.notification = ""
# 非空校验
if not self.captcha:
self.notification = "请输入验证码"
return False
# 格式校验
if not re.fullmatch(r"^\d{6}$", self.captcha):
self.notification = "验证码格式不正确,请重新输入"
return False
return True return True
@rx.event(background=True) @rx.event(background=True)
async def resend_captcha_countdown(self) -> None: async def resend_captcha_countdown_(self) -> None:
""" """
重新发送验证码倒计时 再次发送验证码倒计时后台任务
:return: None :return: None
""" """
async with self: async with self:
self.captcha_sending_countdown = 59 self.captcha_sending_countdown = 59
@ -131,7 +125,7 @@ class AuthState(rx.State):
@rx.event(background=True) @rx.event(background=True)
async def send_captcha_(self) -> None: async def send_captcha_(self) -> None:
""" """
发送验证码 发送验证码后台任务
:return: None :return: None
""" """
async with self: async with self:
@ -140,7 +134,7 @@ class AuthState(rx.State):
database_state = await self.get_state(DatabaseState) database_state = await self.get_state(DatabaseState)
captcha = await database_state.create_captchas_record(email=self.email) captcha = await database_state.create_captchas_record(email=self.email)
# 构建邮件 # 构建验证码邮件
message = MIMEText( message = MIMEText(
CAPTCHA_HTML_TEMPLATE.format(captcha=captcha), CAPTCHA_HTML_TEMPLATE.format(captcha=captcha),
"html", "html",
@ -166,21 +160,52 @@ class AuthState(rx.State):
self.captcha_sending_countdown = 0 self.captcha_sending_countdown = 0
@rx.event @rx.event
async def send_captcha(self) -> AsyncGenerator: async def send_captcha(self) -> AsyncGenerator[EventCallback, None]:
""" """
发送验证码 发送验证码
:return: None :return: AsyncGenerator[EventCallback, None]
""" """
self.notification = "" # 校验邮箱
# 验证邮箱
if not self.validate_email(): if not self.validate_email():
return return
# 将重新发送验证码倒计时和发送验证码事件添加至后台任务队列 # 将再次发送验证码倒计时和发送验证码事件添加至后台任务队列
yield type(self).resend_captcha_countdown yield type(self).resend_captcha_countdown_
yield type(self).send_captcha_ yield type(self).send_captcha_
@rx.event
def set_captcha(self, captcha: str) -> None:
"""
设置验证码
:param captcha: 验证码
:return: None
"""
self.captcha = captcha.strip()
def validate_captcha(self) -> bool:
"""
校验验证码
:return: 验证是否通过True 表示校验通过False 表示校验不通过
"""
self.notification = ""
# 非空校验
if not self.captcha:
self.notification = "请输入验证码"
return False
# 格式校验
if not re.fullmatch(r"^\d{6}$", self.captcha):
self.notification = "验证码格式不正确,请重新输入"
return False
return True
@rx.event
def toggle_policies_agreed(self) -> None:
"""
切换协议同意状态
:return: None
"""
self.is_policies_agreed = not self.is_policies_agreed
@rx.event @rx.event
async def login(self) -> None: async def login(self) -> None:
""" """
@ -189,7 +214,7 @@ class AuthState(rx.State):
""" """
self.is_logging_in = True self.is_logging_in = True
# 验证邮箱 # 验证邮箱格式
if not self.validate_email(): if not self.validate_email():
return return

Binary file not shown.