This commit is contained in:
parent
cd08996e16
commit
bd1e5912a5
|
|
@ -20,18 +20,22 @@ app = rx.App(
|
|||
"*::before, *::after": {
|
||||
"box_sizing": "border-box",
|
||||
},
|
||||
"--prismui-background-color-1": "#ffffff",
|
||||
"--prismui-background-color-1": "#ffffff", # 纯白背景色
|
||||
"--prismui-background-color-2": "rgb(235, 235, 235)",
|
||||
"--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%)",
|
||||
"--mc-float-block-shadow": "rgba(213, 213, 213, 0.25)",
|
||||
"--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": "rgba(25, 25, 25, .06)",
|
||||
"--devui-shadow-length-connected-overlay": "0px 0px 12px 0px rgba(0, 0, 0, 0.1)",
|
||||
"--prismui-box-shadow-1": "0 4px 12px #0000001a", # 导航按钮阴影
|
||||
"--devui-icon-fill-weak": "#babbc0",
|
||||
"--prismui-background-color-weak": "#babbc0",
|
||||
"--devui-border-radius-feedback": "4px",
|
||||
"--prismui-border-radius": "8px",
|
||||
"--prismui-border-radius-1": "8px",
|
||||
"--devui-border-radius-full": "100px",
|
||||
"--font-family": "HuaweiFont,Helvetica,Arial,PingFangSC-Regular,Hiragino Sans GB,Microsoft YaHei,微软雅黑,Microsoft JhengHei",
|
||||
"--prismui-font-size": "14px",
|
||||
|
|
@ -43,7 +47,7 @@ app = rx.App(
|
|||
"--devui-font-size-lg": "14px",
|
||||
"--prismui-color-text": "#252b3a",
|
||||
"--prismui-color-text-light": "#babbc0",
|
||||
"--prismui-color-1": "#252b3a",
|
||||
"--prismui-color-1": "#252b3a", # 文本颜色
|
||||
"--prismui-color-2": "#575d6c",
|
||||
"--prismui-color-3": "#babbc0",
|
||||
"--devui-text-weak": "#575d6c",
|
||||
|
|
|
|||
|
|
@ -1,120 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
渲染框架相关组件
|
||||
"""
|
||||
|
||||
import reflex as rx
|
||||
|
||||
from application.states import SidebarState
|
||||
|
||||
# 侧边栏图标导航按钮字典
|
||||
buttons = {
|
||||
"conversation": {"src": "/conversation.svg", "text": "会话"},
|
||||
"library": {"src": "/library.svg", "text": "知识库"},
|
||||
}
|
||||
|
||||
|
||||
def sidebar() -> rx.Component:
|
||||
"""
|
||||
侧边栏
|
||||
"""
|
||||
|
||||
def button(
|
||||
button: str,
|
||||
) -> rx.Component:
|
||||
"""
|
||||
侧边栏图标导航按钮
|
||||
:param button: 指定侧边栏图标导航按钮
|
||||
"""
|
||||
# 指定按钮的激活状态
|
||||
is_actived = button == SidebarState.get_activated_button
|
||||
|
||||
return rx.box(
|
||||
# 渲染图标容器
|
||||
rx.box(
|
||||
# 渲染图标
|
||||
rx.image(
|
||||
src=buttons[button]["src"],
|
||||
width="24px",
|
||||
height="24px",
|
||||
object_fit="contain",
|
||||
),
|
||||
display="flex",
|
||||
justify_content="center",
|
||||
align_items="center",
|
||||
width="36px",
|
||||
height="36px",
|
||||
border_radius="var(--prismui-border-radius)",
|
||||
# 若激活则渲染背景颜色和阴影,否则不渲染
|
||||
style={
|
||||
"background-color": rx.cond(
|
||||
is_actived, "var(--prismui-background-color)", "transparent"
|
||||
),
|
||||
"box-shadow": rx.cond(is_actived, "0 4px 12px #0000001a", "none"),
|
||||
},
|
||||
),
|
||||
# 渲染标签
|
||||
rx.text(
|
||||
buttons[button]["text"],
|
||||
color="var(--prismui-color-text)",
|
||||
font_size="var(--prismui-font-size-sm)",
|
||||
line_height="20px",
|
||||
),
|
||||
display="flex",
|
||||
flex_direction="column",
|
||||
align_items="center",
|
||||
gap="4px",
|
||||
cursor="pointer",
|
||||
# 点击事件:将指定图标导航按钮设置为侧边栏中激活的图标导航按钮
|
||||
on_click=SidebarState.switch_activated_button(button),
|
||||
)
|
||||
|
||||
return rx.vstack(
|
||||
rx.vstack(
|
||||
rx.vstack(
|
||||
# 品牌图标
|
||||
rx.image(
|
||||
src="/logo.png",
|
||||
width="34px",
|
||||
height="34px",
|
||||
object_fit="contain",
|
||||
),
|
||||
# 品牌名称
|
||||
rx.text(
|
||||
"棱镜球",
|
||||
line_height="20px",
|
||||
font_size="11px",
|
||||
font_weight="700",
|
||||
color="var(--prismui-color-text)",
|
||||
),
|
||||
display="flex",
|
||||
flex_direction="column",
|
||||
justify_content="center",
|
||||
align_items="center",
|
||||
gap="4px",
|
||||
),
|
||||
# 渲染分隔线
|
||||
rx.divider(
|
||||
width="32px",
|
||||
height="1px",
|
||||
margin="16px 0",
|
||||
background_color="var(--prismui-background-color-weak)",
|
||||
),
|
||||
# 渲染侧边栏中会话图标导航按钮
|
||||
button("conversation"),
|
||||
# 渲染侧边栏中知识库图标导航按钮
|
||||
button("library"),
|
||||
display="flex",
|
||||
flex_direction="column",
|
||||
align_items="center",
|
||||
width="100%",
|
||||
margin_top="12px",
|
||||
),
|
||||
display="flex",
|
||||
flex_direction="column",
|
||||
justify_content="space-between",
|
||||
align_items="center",
|
||||
width="60px",
|
||||
height="100%",
|
||||
box_sizing="border-box",
|
||||
)
|
||||
|
|
@ -6,7 +6,7 @@ import reflex as rx
|
|||
|
||||
from application.models import Conversation, Dialog, ThoughtNode
|
||||
from application.states import (
|
||||
ConversationHistoryState,
|
||||
ConversationHistoryCollapseState,
|
||||
ConversationState,
|
||||
CreateConversationState,
|
||||
)
|
||||
|
|
@ -323,6 +323,7 @@ def render_thought_node(thought_node_id: int, thought_node: ThoughtNode):
|
|||
"thinking",
|
||||
rx.text(
|
||||
thought_node.content,
|
||||
line_height="22px",
|
||||
font_size="var(--prismui-font-size-2)",
|
||||
color="var(--prismui-color-3)",
|
||||
),
|
||||
|
|
@ -426,27 +427,28 @@ def render_dialog_history_item(dialog_id: str, dialog: Dialog) -> rx.Component:
|
|||
on_click=lambda: ConversationState.toggle_collapse(dialog_id),
|
||||
),
|
||||
rx.box(
|
||||
rx.auto_scroll(
|
||||
rx.foreach(
|
||||
thought_nodes,
|
||||
lambda i, _: render_thought_node(i[0], i[1]),
|
||||
rx.box(
|
||||
rx.auto_scroll(
|
||||
rx.foreach(
|
||||
thought_nodes,
|
||||
lambda i, _: render_thought_node(i[0], i[1]),
|
||||
),
|
||||
width="100%",
|
||||
margin_top="8px",
|
||||
align_self="start",
|
||||
),
|
||||
width="100%",
|
||||
margin_top="8px",
|
||||
align_self="start",
|
||||
style={"overflow": "hidden", "min-height": "0"},
|
||||
),
|
||||
width="100%",
|
||||
overflow="hidden",
|
||||
style={
|
||||
"max-height": rx.cond(is_collapse_expanded, "1200px", "20px"),
|
||||
"display": "grid",
|
||||
"overflow": "hidden",
|
||||
"grid_template_rows": rx.cond(is_collapse_expanded, "1fr", "0fr"),
|
||||
"opacity": rx.cond(is_collapse_expanded, "1", "0"),
|
||||
"transition": "max-height 0.2s ease-out, opacity 0.2s ease-out",
|
||||
# 双属性同步过渡,和千问一致
|
||||
"transition": "grid-template-rows 0.3s ease-out, opacity 0.3s ease-out",
|
||||
},
|
||||
),
|
||||
width="100%",
|
||||
margin_top="8px",
|
||||
text_align="left",
|
||||
overflow="visible",
|
||||
),
|
||||
# 渲染回答
|
||||
rx.markdown(
|
||||
|
|
@ -744,16 +746,16 @@ def render_conversation_workplace() -> rx.Component:
|
|||
|
||||
|
||||
def render_conversation_history_collapse_button(
|
||||
is_collapse_expanded: bool,
|
||||
is_expanded: bool,
|
||||
) -> rx.Component:
|
||||
"""
|
||||
渲染会话历史折叠面板的打开/关闭按钮
|
||||
:param is_collapse_expanded: 会话历史折叠面板展开状态
|
||||
:param is_expanded: 会话历史折叠面板展开状态
|
||||
:return: Component
|
||||
"""
|
||||
return rx.button(
|
||||
rx.cond(
|
||||
is_collapse_expanded,
|
||||
is_expanded,
|
||||
rx.icon(
|
||||
"chevron-left",
|
||||
width="16px",
|
||||
|
|
@ -769,17 +771,17 @@ def render_conversation_history_collapse_button(
|
|||
),
|
||||
position="absolute",
|
||||
top="50%",
|
||||
left=rx.cond(is_collapse_expanded, "calc(clamp(240px, 25%, 380px) - 8px)", "0"),
|
||||
left=rx.cond(is_expanded, "calc(clamp(240px, 25%, 380px) - 8px)", "0"),
|
||||
z_index=99,
|
||||
width="16px",
|
||||
height="40px",
|
||||
background="var(--prismui-background-color-1)",
|
||||
box_shadow="2px 0 4px 0 var(--mc-float-block-shadow)",
|
||||
border_radius=rx.cond(is_collapse_expanded, "6px", "0 6px 6px 0"),
|
||||
border_radius=rx.cond(is_expanded, "6px", "0 6px 6px 0"),
|
||||
transition="all 0.3s ease-in-out",
|
||||
cursor="pointer",
|
||||
# 点击事件:打开/关闭对话历史折叠面板
|
||||
on_click=ConversationHistoryState.toggle_collapse,
|
||||
# 点击事件:切换会话历史折叠面板展开状态
|
||||
on_click=ConversationHistoryCollapseState.toggle_expanded,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -787,14 +789,14 @@ def render_conversation() -> rx.Component:
|
|||
"""
|
||||
渲染会话页面:布局参考 MetaChat,左侧为会话历史,右侧为对话历史和输入框
|
||||
"""
|
||||
# 会话历史折叠面板展开状态
|
||||
is_collapse_expanded = ConversationHistoryState.is_collapse_expanded
|
||||
# 会话历史折叠面板的展开状态
|
||||
is_expanded = ConversationHistoryCollapseState.is_expanded
|
||||
|
||||
return rx.box(
|
||||
# 渲染对话历史折叠面板
|
||||
# 渲染折叠面板
|
||||
rx.hstack(
|
||||
# 渲染对话历史折叠面板
|
||||
render_conversation_history_collapse(is_collapse_expanded),
|
||||
# 渲染折叠面板
|
||||
render_conversation_history_collapse(is_expanded),
|
||||
# 渲染会话工作区
|
||||
render_conversation_workplace(),
|
||||
position="relative",
|
||||
|
|
@ -806,7 +808,7 @@ def render_conversation() -> rx.Component:
|
|||
transition="all 0.3s ease-in-out",
|
||||
),
|
||||
# 渲染对话历史折叠面板打开/关闭按钮
|
||||
render_conversation_history_collapse_button(is_collapse_expanded),
|
||||
render_conversation_history_collapse_button(is_expanded),
|
||||
position="relative",
|
||||
width="100%",
|
||||
height="100%",
|
||||
|
|
|
|||
|
|
@ -4,34 +4,250 @@
|
|||
"""
|
||||
import reflex as rx
|
||||
|
||||
from application.components import sidebar
|
||||
from application.pages import render_conversation, render_library
|
||||
from application.pages.conversation import render_conversation
|
||||
from application.states import AuthState, SidebarState
|
||||
|
||||
|
||||
# 导航按钮字典
|
||||
nav_buttons: dict = {
|
||||
"conversation": {"src": "/conversation.svg", "text": "会话"},
|
||||
"library": {"src": "/library.svg", "text": "知识库"},
|
||||
}
|
||||
|
||||
"""
|
||||
约定:
|
||||
1 若组件仅用于当前页面则无需抽象为通用组件(application.components.*)
|
||||
"""
|
||||
|
||||
|
||||
def render_nav_button(
|
||||
nav_button: str,
|
||||
) -> rx.Component:
|
||||
"""
|
||||
渲染导航按钮
|
||||
:param nav_button: 指定的导航按钮
|
||||
:return : Component
|
||||
"""
|
||||
# 导航按钮激活状态
|
||||
is_actived = nav_button == SidebarState.activated_nav_button
|
||||
|
||||
return rx.vstack(
|
||||
# 渲染图标
|
||||
rx.image(
|
||||
nav_buttons[nav_button]["src"],
|
||||
width="36px",
|
||||
height="36px",
|
||||
object_fit="contain",
|
||||
padding="6px",
|
||||
background_color=rx.cond(
|
||||
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"),
|
||||
),
|
||||
# 渲染标签
|
||||
rx.text(
|
||||
nav_buttons[nav_button]["text"],
|
||||
line_height="20px",
|
||||
color="var(--prismui-color-1)",
|
||||
font_size="var(--prismui-font-size-2)",
|
||||
),
|
||||
align_items="center",
|
||||
gap="4px",
|
||||
cursor="pointer",
|
||||
# 点击事件:将指定导航按钮设置为侧边栏中激活的导航按钮
|
||||
on_click=SidebarState.switch_nav_button(nav_button),
|
||||
)
|
||||
|
||||
|
||||
def render_sidebar() -> rx.Component:
|
||||
"""
|
||||
渲染侧边栏
|
||||
:return: Component
|
||||
"""
|
||||
|
||||
return rx.vstack(
|
||||
rx.vstack(
|
||||
rx.vstack(
|
||||
# 品牌图标
|
||||
rx.image(
|
||||
src="/logo.png",
|
||||
width="34px",
|
||||
height="34px",
|
||||
object_fit="contain",
|
||||
),
|
||||
# 品牌名称
|
||||
rx.text(
|
||||
"棱镜球",
|
||||
line_height="20px",
|
||||
font_size="11px",
|
||||
font_weight="var(--prismui-font-weight-1)",
|
||||
color="var(--prismui-color-1)",
|
||||
),
|
||||
align_items="center",
|
||||
gap="4px",
|
||||
),
|
||||
# 渲染分隔线
|
||||
rx.divider(
|
||||
width="32px",
|
||||
height="1px",
|
||||
margin="16px 0",
|
||||
background_color="var(--prismui-background-color-4)",
|
||||
),
|
||||
rx.vstack(
|
||||
# 渲染会话导航按钮
|
||||
render_nav_button("conversation"),
|
||||
# 渲染知识库导航按钮
|
||||
render_nav_button("library"),
|
||||
gap="8px",
|
||||
),
|
||||
align_items="center",
|
||||
width="100%",
|
||||
margin_top="12px",
|
||||
),
|
||||
flex_shrink="0",
|
||||
justify_content="space-between",
|
||||
align_items="center",
|
||||
width="60px",
|
||||
min_width="60px",
|
||||
max_width="60px",
|
||||
height="100%",
|
||||
)
|
||||
|
||||
|
||||
def render_login_modal() -> rx.Component:
|
||||
"""
|
||||
渲染登录模态窗
|
||||
:return: Component
|
||||
"""
|
||||
|
||||
return rx.center(
|
||||
rx.hstack(
|
||||
rx.vstack(
|
||||
rx.text(
|
||||
"登录",
|
||||
font_size="20px",
|
||||
font_weight="600",
|
||||
margin_bottom="24px",
|
||||
),
|
||||
rx.input(
|
||||
placeholder="请输入邮箱地址",
|
||||
value=AuthState.email,
|
||||
on_change=AuthState.set_email,
|
||||
width="100%",
|
||||
),
|
||||
rx.hstack(
|
||||
rx.input(
|
||||
placeholder="请输入验证码",
|
||||
value=AuthState.captcha,
|
||||
on_change=AuthState.set_captcha,
|
||||
flex=1,
|
||||
),
|
||||
rx.button(
|
||||
"获取验证码",
|
||||
variant="outline",
|
||||
width="120px",
|
||||
on_click=AuthState.send_email_code,
|
||||
),
|
||||
gap="8px",
|
||||
width="100%",
|
||||
),
|
||||
rx.cond(
|
||||
AuthState.email == "",
|
||||
rx.text("请输入邮箱地址", color="red", font_size="12px"),
|
||||
rx.box(height="16px"),
|
||||
),
|
||||
rx.hstack(
|
||||
rx.checkbox(
|
||||
checked=AuthState.agree_policy,
|
||||
on_change=AuthState.toggle_agree,
|
||||
),
|
||||
rx.text("我已阅读并同意 ", font_size="13px"),
|
||||
rx.link("用户协议、隐私政策", font_size="13px", href="#"),
|
||||
align_items="center",
|
||||
),
|
||||
rx.button(
|
||||
"登录",
|
||||
width="100%",
|
||||
bg="#0a0e29",
|
||||
color="white",
|
||||
margin_top="12px",
|
||||
on_click=AuthState.login_by_email_code,
|
||||
),
|
||||
rx.text(
|
||||
"登录有问题?去反馈",
|
||||
font_size="12px",
|
||||
color="#666",
|
||||
margin_top="8px",
|
||||
),
|
||||
width="360px",
|
||||
gap="16px",
|
||||
padding="40px 32px",
|
||||
align_items="start",
|
||||
),
|
||||
rx.divider(orientation="vertical"),
|
||||
rx.vstack(
|
||||
rx.image(
|
||||
src="/qrcode.png",
|
||||
width="160px",
|
||||
height="160px",
|
||||
padding="8px",
|
||||
border="1px solid #eee",
|
||||
border_radius="8px",
|
||||
bg="white",
|
||||
),
|
||||
rx.text(
|
||||
"添加微信申请使用",
|
||||
font_size="18px",
|
||||
font_weight="600",
|
||||
margin_top="16px",
|
||||
),
|
||||
align_items="center",
|
||||
width="320px",
|
||||
padding="40px 32px",
|
||||
style={"background": "#f8f9fc"},
|
||||
),
|
||||
),
|
||||
position="fixed",
|
||||
top="0",
|
||||
left="0",
|
||||
width="100vw",
|
||||
height="100vh",
|
||||
bg="rgba(255, 255, 255, 0.85)",
|
||||
z_index="9999",
|
||||
overflow="hidden",
|
||||
)
|
||||
|
||||
|
||||
def render_index() -> rx.Component:
|
||||
"""
|
||||
首页
|
||||
"""
|
||||
|
||||
return rx.hstack(
|
||||
# 渲染侧边栏
|
||||
sidebar(),
|
||||
# 根据用户所点中的侧边栏图标导航按钮渲染相应页面
|
||||
rx.match(
|
||||
SidebarState.get_activated_button,
|
||||
# 渲染知识库页面
|
||||
("library", render_library()),
|
||||
# 渲染会话页面
|
||||
render_conversation(),
|
||||
return rx.fragment(
|
||||
rx.hstack(
|
||||
# 渲染侧边栏
|
||||
render_sidebar(),
|
||||
# 根据侧边栏状态中激活的导航按钮渲染相应页面(本项目采用卡片布局)
|
||||
rx.match(
|
||||
SidebarState.activated_nav_button,
|
||||
# 渲染知识库页面
|
||||
("library", render_library()),
|
||||
# 渲染会话页面
|
||||
render_conversation(),
|
||||
),
|
||||
width="100%",
|
||||
height="100vh",
|
||||
overflow="auto",
|
||||
padding="8px 8px 8px 0",
|
||||
background="var(--prismui-background-1)",
|
||||
# 挂载事件:
|
||||
on_mount=AuthState.check,
|
||||
),
|
||||
rx.cond(
|
||||
AuthState.user_id == "",
|
||||
render_login_modal(),
|
||||
rx.fragment(),
|
||||
),
|
||||
width="100%",
|
||||
height="100vh",
|
||||
padding="8px 8px 8px 0",
|
||||
overflow="auto",
|
||||
box_sizing="border-box",
|
||||
background="var(--mc-global-bg)",
|
||||
# 挂载事件:因测试需模拟已认证
|
||||
on_mount=AuthState.authenticate,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from application.states.auth import AuthState
|
||||
from application.states.conversation import ConversationState
|
||||
from application.states.conversation_history import ConversationHistoryState
|
||||
from application.states.conversation_historycollapse import ConversationHistoryCollapseState
|
||||
from application.states.create_conversation import CreateConversationState
|
||||
from application.states.database import DatabaseState
|
||||
from application.states.sidebar import SidebarState
|
||||
|
|
@ -9,7 +9,7 @@ from application.states.sidebar import SidebarState
|
|||
|
||||
__all__ = [
|
||||
"AuthState",
|
||||
"ConversationHistoryState",
|
||||
"ConversationHistoryCollapseState",
|
||||
"ConversationState",
|
||||
"CreateConversationState",
|
||||
"DatabaseState",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,11 @@
|
|||
"""
|
||||
import reflex as rx
|
||||
|
||||
import re
|
||||
from sqlmodel import select
|
||||
from application.models.user import EmailVerifyCode
|
||||
from application.database.base import get_async_db
|
||||
|
||||
|
||||
class AuthState(rx.State):
|
||||
"""
|
||||
|
|
@ -13,10 +18,112 @@ class AuthState(rx.State):
|
|||
# 当前登录的用户唯一标识(通过本地存储同步)
|
||||
user_id: str = rx.LocalStorage("user_id", sync=True)
|
||||
|
||||
email: str = ""
|
||||
captcha: str = ""
|
||||
is_policies_agreed: bool = False
|
||||
|
||||
@rx.event
|
||||
async def authenticate(self) -> None:
|
||||
async def check(self) -> None:
|
||||
"""
|
||||
认证
|
||||
检查认证状态
|
||||
"""
|
||||
|
||||
self.user_id = "1"
|
||||
self.user_id = ""
|
||||
|
||||
@rx.event
|
||||
def set_email(self, email: str) -> None:
|
||||
"""
|
||||
设置邮箱
|
||||
:param email: 邮箱
|
||||
:return: None
|
||||
"""
|
||||
self.email = email.strip()
|
||||
|
||||
@rx.event
|
||||
def set_captcha(self, captcha: str) -> None:
|
||||
"""
|
||||
设置验证码
|
||||
:param captcha: 验证码
|
||||
:return: None
|
||||
"""
|
||||
self.captcha = captcha.strip()
|
||||
|
||||
@rx.event
|
||||
def toggle_policies_agreed(self) -> None:
|
||||
"""
|
||||
切换协议同意状态
|
||||
:return: None
|
||||
"""
|
||||
self.is_policies_agreed = not self.is_policies_agreed
|
||||
|
||||
@rx.event
|
||||
async def send_email_code(self):
|
||||
"""生成并存储邮箱验证码(异步数据库,不阻塞事件循环)"""
|
||||
|
||||
email_rule = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
|
||||
if not re.fullmatch(email_rule, self.email):
|
||||
return rx.toast("请输入合法邮箱地址", color="red")
|
||||
|
||||
# 异步数据库会话
|
||||
async for db in get_async_db():
|
||||
# 清理该邮箱旧验证码(await 异步查询)
|
||||
old_codes = await db.exec(
|
||||
select(EmailVerifyCode).where(EmailVerifyCode.email == self.email)
|
||||
)
|
||||
old_list = old_codes.all()
|
||||
for item in old_list:
|
||||
await db.delete(item)
|
||||
|
||||
# 6位验证码,5分钟有效期
|
||||
import random
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
code_str = "".join(random.choices("0123456789", k=6))
|
||||
expire_time = datetime.utcnow() + timedelta(minutes=5)
|
||||
db.add(
|
||||
EmailVerifyCode(email=self.email, code=code_str, expire_at=expire_time)
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
# 本地调试打印验证码
|
||||
print(f"邮箱 {self.email} 测试验证码:{code_str}")
|
||||
return rx.toast("验证码已发送(控制台查看测试码)", color="green")
|
||||
|
||||
@rx.event
|
||||
async def login_by_email_code(self):
|
||||
"""邮箱验证码登录,不存在用户自动注册"""
|
||||
if not self.email:
|
||||
return rx.toast("请填写邮箱", color="red")
|
||||
if not self.code:
|
||||
return rx.toast("请填写验证码", color="red")
|
||||
if not self.agree_policy:
|
||||
return rx.toast("请勾选同意用户协议与隐私政策", color="red")
|
||||
|
||||
db = get_db_session()
|
||||
# 查询有效验证码
|
||||
code_record = db.exec(
|
||||
select(EmailVerifyCode).where(
|
||||
EmailVerifyCode.email == self.email, EmailVerifyCode.code == self.code
|
||||
)
|
||||
).first()
|
||||
if not code_record or code_record.is_expired():
|
||||
return rx.toast("验证码错误或已过期", color="red")
|
||||
|
||||
# 一次性验证码使用后删除
|
||||
db.delete(code_record)
|
||||
|
||||
# 查找用户,无则新建
|
||||
user = db.exec(select(User).where(User.email == self.email)).first()
|
||||
if not user:
|
||||
user = User(email=self.email, nickname=self.email.split("@")[0])
|
||||
db.add(user)
|
||||
user.last_login_at = datetime.utcnow()
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
|
||||
# 登录成功,弹窗自动消失
|
||||
self.user_id = str(user.id)
|
||||
self.email = ""
|
||||
self.code = ""
|
||||
self.agree_policy = False
|
||||
return rx.toast("登录成功", color="green")
|
||||
|
|
|
|||
|
|
@ -1,22 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
会话历史状态
|
||||
"""
|
||||
import reflex as rx
|
||||
|
||||
|
||||
class ConversationHistoryState(rx.State):
|
||||
"""
|
||||
会话历史状态
|
||||
"""
|
||||
|
||||
# 会话历史折叠面板展开状态,True表示展开,False表示收起
|
||||
is_collapse_expanded: bool = False
|
||||
|
||||
@rx.event
|
||||
def toggle_collapse(self) -> None:
|
||||
"""
|
||||
打开/关闭会话历史折叠面板
|
||||
:return: None
|
||||
"""
|
||||
self.is_collapse_expanded = not self.is_collapse_expanded
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
会话历史折叠面板状态
|
||||
"""
|
||||
import reflex as rx
|
||||
|
||||
|
||||
class ConversationHistoryCollapseState(rx.State):
|
||||
"""
|
||||
会话历史折叠面板状态
|
||||
"""
|
||||
|
||||
# 展开状态,True表示展开,False表示折叠
|
||||
is_expanded: bool = False
|
||||
|
||||
@rx.event
|
||||
def toggle_expanded(self) -> None:
|
||||
"""
|
||||
切换展开状态
|
||||
:return: None
|
||||
"""
|
||||
self.is_expanded = not self.is_expanded
|
||||
|
|
@ -13,9 +13,9 @@ class SidebarState(rx.State):
|
|||
"""
|
||||
|
||||
@rx.var
|
||||
async def generate_storage_key(self) -> str:
|
||||
async def storage_key(self) -> str:
|
||||
"""
|
||||
生成 LocalStorage 键
|
||||
LocalStorage 键
|
||||
"""
|
||||
# 获取认证状态
|
||||
auth = await self.get_state(AuthState)
|
||||
|
|
@ -23,19 +23,12 @@ class SidebarState(rx.State):
|
|||
user_id = auth.user_id or "default"
|
||||
return f"sidebar_{user_id}"
|
||||
|
||||
# 侧边栏中激活的图标导航按钮
|
||||
activated_button: str = rx.LocalStorage(generate_storage_key, sync=True)
|
||||
|
||||
@rx.var
|
||||
def get_activated_button(self) -> str:
|
||||
"""
|
||||
获取侧边栏中激活的图标导航按钮
|
||||
"""
|
||||
return self.activated_button
|
||||
# 激活的导航按钮
|
||||
activated_nav_button: str = rx.LocalStorage(storage_key, sync=True)
|
||||
|
||||
@rx.event
|
||||
def switch_activated_button(self, button: str):
|
||||
def switch_nav_button(self, button: str):
|
||||
"""
|
||||
将指定图标导航按钮设置为侧边栏中激活的图标导航按钮
|
||||
将指定导航按钮设置为激活的导航按钮
|
||||
"""
|
||||
self.activated_button = button
|
||||
self.activated_nav_button = button
|
||||
|
|
|
|||
Binary file not shown.
Loading…
Reference in New Issue