diff --git a/agent/application/application.py b/agent/application/application.py index 25622c3..a07d20b 100644 --- a/agent/application/application.py +++ b/agent/application/application.py @@ -6,59 +6,66 @@ import reflex as rx from application.pages.index import render_index app = rx.App( + theme=rx.theme(), style={ - "root": { - "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-1": "#ffffff", # 背景颜色:白色 "--prismui-background-color-2": "#ebebeb", "--prismui-background-color-3": "#f6f6f8", "--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", "--devui-global-bg": "#f6f6f8", "--mc-global-bg": "linear-gradient(to bottom, #D0C9FF 0%, #E6D6F0 8%, #F1DBEA 12%, #C8DCFB 40%, #ABC6F6 60%, #87AEFE 90%)", "--mc-box-shadow": "#191919", "--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", "--prismui-background-color-weak": "#babbc0", "--devui-border-radius-feedback": "4px", + "--prismui-border-1": "1px solid rgba(6, 10, 38, 0.06)", "--prismui-border-radius": "8px", "--prismui-border-radius-1": "8px", + "--prismui-border-radius-2": "12px", + "--prismui-border-radius-3": "20px", "--devui-border-radius-full": "100px", - "--font-family": "HuaweiFont,Helvetica,Arial,PingFangSC-Regular,Hiragino Sans GB,Microsoft YaHei,微软雅黑,Microsoft JhengHei", - "--prismui-font-size": "14px", - "--prismui-font-size-1": "14px", - "--prismui-font-size-2": "12px", - "--prismui-font-size-sm": "12px", - "--prismui-font-weight-1": "700", - "--devui-font-size": "14px", - "--devui-font-size-lg": "14px", - "--prismui-color-text": "#252b3a", - "--prismui-color-text-light": "#babbc0", - "--prismui-color-1": "#252b3a", # 文本颜色 - "--prismui-color-2": "#575d6c", - "--prismui-color-3": "#babbc0", - "--devui-text-weak": "#575d6c", - "--devui-light-text": "#ffffff", - "--placeholder": "#babbc0", - "--devui-aide-text": "#71757f", - "--devui-primary": "#5e7ce0", - "--devui-brand": "#5e7ce0", - "--devui-brand-hover": "#7693f5", - "--devui-dividing-line": "#f2f2f3", + "box-shadow": "none", + "background-color": "var(--prismui-background-color-1)", # 默认背景颜色 + "font_family": "var(--prismui-font-family)", + "font_size": "var(--prismui-font-size-2)", + "font_weight": "var(--prismui-font-weight-1)", + "color": "var(--prismui-color-1)", + "*": { + "margin": "0", + "padding": "0", + "gap": "0", + }, + "*::before, *::after": { + "box-sizing": "border-box", + }, + "* .rt-TextFieldRoot, * textarea": { + "border": "var(--prismui-border-1)", + "outline": "none", + "box-shadow": "none", + }, + "*::placeholder": { + "color": "var(--prismui-color-3)", + "opacity": 1, + }, # 覆盖占位符样式 }, # 自定义主题颜色 ) # 此处变量名需使用 app ,具体原因目前尚不清楚 # 注册首页路由 diff --git a/agent/application/pages/index.py b/agent/application/pages/index.py index 1c743fc..9ed0f49 100644 --- a/agent/application/pages/index.py +++ b/agent/application/pages/index.py @@ -44,7 +44,7 @@ def render_nav_button( is_actived, "var(--prismui-background-color-1)", "transparent" ), 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( @@ -126,39 +126,95 @@ def render_login_window() -> rx.Component: rx.hstack( rx.vstack( rx.text( - "登录", - font_size="20px", - font_weight="600", + "登录解锁更多功能", + line_height="32px", + font_size="var(--prismui-font-size-3)", + font_weight="var(--prismui-font-weight-2)", margin_bottom="24px", ), rx.input( placeholder="请输入邮箱地址", value=AuthState.email, - on_change=AuthState.set_email, 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.input( placeholder="请输入验证码", + max_length=6, value=AuthState.captcha, - on_change=AuthState.set_captcha, 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( - AuthState.captcha_sending_countdown > 0, - f"{AuthState.captcha_sending_countdown} s111111", + AuthState.captcha_sending_countdown, + f"{AuthState.captcha_sending_countdown} s", "获取验证码", ), - variant="outline", - width="120px", + margin="0 8px", + 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, - disabled=AuthState.captcha_sending_countdown > 0, ), - gap="8px", 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.checkbox( checked=AuthState.is_policies_agreed, @@ -184,12 +240,11 @@ def render_login_window() -> rx.Component: color="#666", margin_top="8px", ), - width="360px", - gap="16px", - padding="40px 32px", - align_items="start", + width="396px", + padding="40px 48px 0 48px", + background_color="var(--prismui-background-color-1)", + align_items="center", ), - rx.divider(orientation="vertical"), rx.vstack( rx.image( src="/qrcode.png", @@ -211,15 +266,18 @@ def render_login_window() -> rx.Component: padding="40px 32px", 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", left="0", + z_index="9999", width="100vw", height="100vh", - bg="rgba(255, 255, 255, 0.85)", - z_index="9999", - overflow="hidden", + backdrop_filter="blur(4px)", ) diff --git a/agent/application/states/auth.py b/agent/application/states/auth.py index 34e142c..5c48d9b 100644 --- a/agent/application/states/auth.py +++ b/agent/application/states/auth.py @@ -9,6 +9,7 @@ import re from typing import Any, AsyncGenerator, Coroutine, cast from aiosmtplib import SMTP, SMTPException import reflex as rx +from reflex.event import EventCallback from application.states.database import DatabaseState @@ -52,6 +53,14 @@ class AuthState(rx.State): self.user_id = "" + @rx.event + def reset_notification(self) -> None: + """ + 清除提示信息 + :return: None + """ + self.notification = "" + @rx.event def set_email(self, email: str) -> None: """ @@ -61,65 +70,50 @@ class AuthState(rx.State): """ self.email = email.strip() - @rx.event - def set_captcha(self, captcha: str) -> None: + @rx.var + def is_captcha_sending_disabled(self) -> bool: """ - 设置验证码 - :param captcha: 验证码 - :return: None + 验证码发送禁用状态 + :return: 禁用状态,True 表示禁用,False 表示未禁用 """ - self.captcha = captcha.strip() - - @rx.event - def toggle_policies_agreed(self) -> None: """ - 切换协议同意状态 - :return: None + 邮箱为空或验证码发送倒计时大于0时禁用 """ - self.is_policies_agreed = not self.is_policies_agreed + return not self.email or self.captcha_sending_countdown > 0 def validate_email(self) -> bool: """ - 验证邮箱 - :return: 验证是否通过,True 表示校验通过,False 表示校验不通过 + 校验邮箱 + :return: 校验结果,True 表示校验通过,False 表示校验不通过 """ - self.notification = "" # 非空校验 if not self.email: self.notification = "请输入邮箱" return False + # 格式校验 + """ + 邮箱由用户名@域名组成,其中: + 1)用户名首字符为字母或数字,中间可穿插下划线、短横线、加号或点,符号后为字母或数字; + 2)域名由若干子域名段和顶级域名段组成,各段之间用点分隔,其中: + -- 子域名段:首字符为字母或数字,中间可穿插下划线或短横线,符号后为字母或数字; + -- 顶级域名段:至少包含两个字母或数字; + """ 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,}$", self.email, ): self.notification = "邮箱格式不正确,请重新输入" return False - return True - - 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(background=True) - async def resend_captcha_countdown(self) -> None: + async def resend_captcha_countdown_(self) -> None: """ - 重新发送验证码倒计时 + 再次发送验证码倒计时(后台任务) :return: None """ - async with self: self.captcha_sending_countdown = 59 @@ -131,7 +125,7 @@ class AuthState(rx.State): @rx.event(background=True) async def send_captcha_(self) -> None: """ - 发送验证码 + 发送验证码(后台任务) :return: None """ async with self: @@ -140,7 +134,7 @@ class AuthState(rx.State): database_state = await self.get_state(DatabaseState) captcha = await database_state.create_captchas_record(email=self.email) - # 构建邮件 + # 构建验证码邮件 message = MIMEText( CAPTCHA_HTML_TEMPLATE.format(captcha=captcha), "html", @@ -166,21 +160,52 @@ class AuthState(rx.State): self.captcha_sending_countdown = 0 @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(): return - # 将重新发送验证码倒计时和发送验证码事件添加至后台任务队列 - yield type(self).resend_captcha_countdown + # 将再次发送验证码倒计时和发送验证码事件添加至后台任务队列 + yield type(self).resend_captcha_countdown_ 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 async def login(self) -> None: """ @@ -189,7 +214,7 @@ class AuthState(rx.State): """ self.is_logging_in = True - # 验证邮箱 + # 验证邮箱格式 if not self.validate_email(): return diff --git a/agent/database.db b/agent/database.db index 3f0d68e..9e41141 100644 Binary files a/agent/database.db and b/agent/database.db differ