feat: 现代化登录/注册界面改造

- login.css: 全新现代化样式,支持亮/暗主题
- login.ui: 三Tab布局(密码登录/手机登录/注册),手机登录支持短信验证码
- sms_register.dspy: 短信验证注册后端,验证通过后自动注册并登录
- load_path.py: 添加 sms_register.dspy 到 any 权限
- 修复手机登录 setValue 调用 (上一轮已提交)
- 注册流程: 手机号+短信验证码+用户名+密码,短信验证通过后才允许注册
This commit is contained in:
yumoqing 2026-05-30 14:08:11 +08:00
parent 019e9702fc
commit 36569c0e41
4 changed files with 477 additions and 26 deletions

View File

@ -49,6 +49,7 @@ PATHS_ANY = [
f"/rbac/user/logout.dspy",
f"/rbac/user/register.dspy",
f"/rbac/user/register.ui",
f"/rbac/user/sms_register.dspy",
f"/rbac/user/reset_password/index.ui",
f"/rbac/user/reset_password/reset_password.dspy",
f"/rbac/user/up_login.dspy",

231
wwwroot/login.css Normal file
View File

@ -0,0 +1,231 @@
/* ===== Modern Login Popup Styling ===== */
/* Popup window card */
.login-window {
border-radius: 16px !important;
box-shadow: 0 25px 60px -12px rgba(0, 0, 0, 0.35) !important;
overflow: hidden !important;
border: none !important;
}
/* Title bar - gradient brand header */
.login-window .titlebar {
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #a855f7 100%) !important;
padding: 8px 16px !important;
border: none !important;
min-height: 48px !important;
}
.login-window .titlebar .text-w,
.login-window .titlebar .bricks-text {
color: #ffffff !important;
font-size: 16px !important;
font-weight: 600 !important;
}
/* Content area padding */
.login-window .flexbox {
padding: 0 !important;
}
/* Tab panel modern styling */
.login-window .tabpanel {
background: transparent !important;
border: none !important;
}
.login-window .tabpanel-tabs {
display: flex !important;
border-bottom: 2px solid #e2e8f0 !important;
background: #f8fafc !important;
padding: 0 !important;
gap: 0 !important;
}
.login-window .tabpanel-tab {
padding: 14px 24px !important;
cursor: pointer !important;
border-bottom: 3px solid transparent !important;
margin-bottom: -2px !important;
transition: all 0.2s ease !important;
color: #64748b !important;
font-weight: 500 !important;
font-size: 14px !important;
background: transparent !important;
}
.login-window .tabpanel-tab:hover {
color: #6366f1 !important;
background: rgba(99, 102, 241, 0.04) !important;
}
.login-window .tabpanel-tab-active,
.login-window .tabpanel-tab-selected {
color: #6366f1 !important;
border-bottom-color: #6366f1 !important;
background: transparent !important;
}
.login-window .tabpanel-content {
padding: 20px 24px !important;
background: #ffffff !important;
}
/* Form styling */
.login-window .vcontainer {
gap: 4px !important;
}
.login-window .inputbox {
border-radius: 10px !important;
border: 1.5px solid #e2e8f0 !important;
padding: 10px 14px !important;
transition: border-color 0.2s ease, box-shadow 0.2s ease !important;
background: #f8fafc !important;
font-size: 14px !important;
}
.login-window .inputbox:focus,
.login-window .inputbox:focus-within {
border-color: #6366f1 !important;
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.12) !important;
background: #ffffff !important;
outline: none !important;
}
.login-window input.inputbox {
height: 42px !important;
}
/* Form labels */
.login-window .field-label,
.login-window .bricks-form label {
font-weight: 500 !important;
color: #374151 !important;
font-size: 13px !important;
margin-bottom: 4px !important;
}
/* Toolbar buttons */
.login-window .htoolbar {
padding: 8px 0 !important;
gap: 8px !important;
justify-content: center !important;
}
.login-window .submit_btn {
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%) !important;
color: #ffffff !important;
border: none !important;
border-radius: 10px !important;
padding: 10px 32px !important;
font-weight: 600 !important;
font-size: 14px !important;
cursor: pointer !important;
transition: all 0.2s ease !important;
box-shadow: 0 2px 8px rgba(99, 102, 241, 0.3) !important;
}
.login-window .submit_btn:hover {
opacity: 0.92 !important;
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.4) !important;
transform: translateY(-1px) !important;
}
.login-window .reset_btn,
.login-window .clear_btn {
border-radius: 10px !important;
border: 1.5px solid #e2e8f0 !important;
background: #ffffff !important;
color: #64748b !important;
padding: 10px 20px !important;
font-weight: 500 !important;
transition: all 0.2s ease !important;
}
.login-window .reset_btn:hover,
.login-window .clear_btn:hover {
border-color: #cbd5e1 !important;
background: #f8fafc !important;
}
/* SMS send button */
.sms-send-btn {
border-radius: 10px !important;
border: 1.5px solid #6366f1 !important;
background: rgba(99, 102, 241, 0.06) !important;
color: #6366f1 !important;
padding: 8px 20px !important;
font-weight: 600 !important;
font-size: 13px !important;
cursor: pointer !important;
transition: all 0.2s ease !important;
}
.sms-send-btn:hover {
background: rgba(99, 102, 241, 0.12) !important;
}
.sms-send-btn:disabled {
opacity: 0.5 !important;
cursor: not-allowed !important;
}
/* Description text */
.login-window .bricks-text {
color: #64748b;
}
/* Form description */
.login-desc {
color: #94a3b8 !important;
font-size: 12px !important;
text-align: center !important;
margin-top: 4px !important;
}
/* Section separator */
.login-separator {
display: flex !important;
align-items: center !important;
gap: 12px !important;
margin: 8px 0 !important;
color: #cbd5e1 !important;
font-size: 12px !important;
}
.login-separator::before,
.login-separator::after {
content: '' !important;
flex: 1 !important;
height: 1px !important;
background: #e2e8f0 !important;
}
/* Dark theme support */
[data-theme="dark"] .login-window {
background: #1e293b !important;
}
[data-theme="dark"] .login-window .tabpanel-content {
background: #1e293b !important;
}
[data-theme="dark"] .login-window .tabpanel-tabs {
background: #0f172a !important;
border-bottom-color: #334155 !important;
}
[data-theme="dark"] .login-window .tabpanel-tab {
color: #94a3b8 !important;
}
[data-theme="dark"] .login-window .tabpanel-tab-active,
[data-theme="dark"] .login-window .tabpanel-tab-selected {
color: #818cf8 !important;
border-bottom-color: #818cf8 !important;
}
[data-theme="dark"] .login-window .inputbox {
background: #0f172a !important;
border-color: #475569 !important;
color: #e2e8f0 !important;
}
[data-theme="dark"] .login-window .inputbox:focus,
[data-theme="dark"] .login-window .inputbox:focus-within {
border-color: #818cf8 !important;
box-shadow: 0 0 0 3px rgba(129, 140, 248, 0.15) !important;
}
[data-theme="dark"] .login-window .field-label {
color: #cbd5e1 !important;
}
[data-theme="dark"] .login-window .reset_btn,
[data-theme="dark"] .login-window .clear_btn {
background: #334155 !important;
border-color: #475569 !important;
color: #cbd5e1 !important;
}
[data-theme="dark"] .sms-send-btn {
border-color: #818cf8 !important;
color: #818cf8 !important;
background: rgba(129, 140, 248, 0.1) !important;
}

View File

@ -1,14 +1,16 @@
{% set sms_code_url = entire_url('/rbac/gen_sms_code.dspy') %}
{% set code_login_url = entire_url('/rbac/user/code_login.dspy') %}
{% set sms_register_url = entire_url('/rbac/user/sms_register.dspy') %}
{
"id": "login_window",
"widgettype": "PopupWindow",
"options": {
"title": "登录/注册",
"title": "欢迎登录",
"css": "login-window",
"auto_open": true,
"anthor": "cc",
"cwidth": 22,
"cheight": 22
"cwidth": 26,
"cheight": 28
},
"subwidgets": [
{
@ -22,7 +24,7 @@
"items": [
{
"name": "userpasswd",
"label": "用户密码",
"label": "密码登录",
"content": {
"widgettype": "Form",
"options": {
@ -48,15 +50,22 @@
},
{
"name": "phonecode",
"label": "手机验证码",
"label": "手机登录",
"content": {
"widgettype": "VBox",
"options": {"gap": "8px"},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"text": "未注册的手机号将自动创建账号",
"css": "login-desc"
}
},
{
"widgettype": "Form",
"id": "phone_form",
"options": {
"description": "限中国国内手机号",
"cols": 1,
"fields": [
{"name": "cell_no", "label": "手机号", "uitype": "str"},
@ -80,7 +89,7 @@
{
"widgettype": "Button",
"id": "gen_code_btn",
"options": {"label": "发送验证码"},
"options": {"label": "发送验证码", "css": "sms-send-btn"},
"binds": [
{
"wid": "self",
@ -96,28 +105,51 @@
},
{
"name": "register",
"label": "注册",
"label": "注册账号",
"content": {
"widgettype": "Form",
"options": {
"cols": 1,
"fields": [
{"name": "username", "label": "用户名", "uitype": "str"},
{"name": "mobile", "label": "手机号", "uitype": "str"},
{"name": "password", "label": "密码", "uitype": "password"},
{"name": "cfm_password", "label": "确认密码", "uitype": "password"}
]
},
"binds": [
"widgettype": "VBox",
"options": {"gap": "8px"},
"subwidgets": [
{
"wid": "self",
"event": "submit",
"actiontype": "urlwidget",
"target": "self",
"widgettype": "Form",
"id": "register_form",
"options": {
"method": "POST",
"url": "{{entire_url('register.dspy')}}"
}
"cols": 1,
"fields": [
{"name": "username", "label": "用户名", "uitype": "str"},
{"name": "mobile", "label": "手机号", "uitype": "str"},
{"name": "codeid", "uitype": "hide", "value": ""},
{"name": "check_code", "label": "短信验证码", "uitype": "str"},
{"name": "password", "label": "密码", "uitype": "password"},
{"name": "cfm_password", "label": "确认密码", "uitype": "password"}
]
},
"binds": [
{
"wid": "self",
"event": "submit",
"actiontype": "urlwidget",
"target": "self",
"options": {
"method": "POST",
"url": "{{sms_register_url}}"
}
}
]
},
{
"widgettype": "Button",
"id": "reg_sms_btn",
"options": {"label": "发送验证码", "css": "sms-send-btn"},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "script",
"target": "self",
"script": "var form=bricks.getWidgetById('register_form',bricks.app);if(!form)return;var cell=form._getValue().mobile;if(!cell||cell.length<11){alert('请输入正确的手机号');return;}var btn=this;btn.disabled=true;btn.text_w&&btn.text_w.set_otext('发送中...');fetch('{{sms_code_url}}?_webbricks_=1&cellphone='+encodeURIComponent(cell)).then(function(r){return r.json()}).then(function(d){if(d.status==='ok'){var w=form.name_inputs['codeid'];if(w)w.setValue(d.data.key);btn.text_w&&btn.text_w.set_otext('已发送');var s=60;var t=setInterval(function(){s--;if(s<=0){clearInterval(t);btn.disabled=false;btn.text_w&&btn.text_w.set_otext('重新发送')}else btn.text_w&&btn.text_w.set_otext(s+'s')},1000)}else{alert(d.data.message||'发送验证码出错');btn.disabled=false;btn.text_w&&btn.text_w.set_otext('发送验证码')}}).catch(function(e){alert('网络错误: '+e);btn.disabled=false;btn.text_w&&btn.text_w.set_otext('发送验证码')})"
}
]
}
]
}

View File

@ -0,0 +1,187 @@
# 短信验证注册 - 接收前端表单参数(username, mobile, codeid, check_code, password, cfm_password)
# 先验证短信码,通过后注册并自动登录
debug(f'sms_register.dspy: {params_kw=}')
username = params_kw.username
mobile = params_kw.mobile
key = params_kw.codeid
sms_code = params_kw.check_code
password = params_kw.password
cfm_password = params_kw.cfm_password
# 基本参数校验
if not username:
return {
"widgettype": "Error",
"options": {
"timeout": 3,
"title": "注册失败",
"message": "请输入用户名"
}
}
if not mobile:
return {
"widgettype": "Error",
"options": {
"timeout": 3,
"title": "注册失败",
"message": "请输入手机号"
}
}
if not password:
return {
"widgettype": "Error",
"options": {
"timeout": 3,
"title": "注册失败",
"message": "请输入密码"
}
}
if password != cfm_password:
return {
"widgettype": "Error",
"options": {
"timeout": 3,
"title": "注册失败",
"message": "两次输入的密码不一致"
}
}
if not key or not sms_code:
return {
"widgettype": "Error",
"options": {
"timeout": 3,
"title": "注册失败",
"message": "请先发送并输入短信验证码"
}
}
# 验证短信码
try:
ok = await sms_engine.check_sms_code(key, sms_code)
except Exception as e:
exception(f'sms_register sms check error: {e}')
ok = False
if not ok:
return {
"widgettype": "Error",
"options": {
"timeout": 3,
"title": "验证失败",
"message": "短信验证码错误或已过期,请重新获取"
}
}
# 短信验证通过,注册用户
db = DBPools()
dbname = get_module_dbname('rbac')
try:
async with db.sqlorContext(dbname) as sor:
# 检查手机号是否已注册
existing = await sor.R('users', {'mobile': mobile})
if existing:
return {
"widgettype": "Error",
"options": {
"timeout": 5,
"title": "注册失败",
"message": "该手机号已注册,请直接登录"
}
}
# 检查用户名是否已存在
existing_user = await sor.R('users', {'username': username})
if existing_user:
return {
"widgettype": "Error",
"options": {
"timeout": 5,
"title": "注册失败",
"message": "用户名已被占用"
}
}
# 调用注册函数
reg_params = DictObject(
username=username,
mobile=mobile,
password=password,
cfm_password=cfm_password
)
data = await register_user(sor, reg_params)
data = DictObject(**data)
if data.status == 'error':
debug(f"sms_register error: {data.data.message}")
return {
"widgettype": "Error",
"options": {
"timeout": 5,
"title": "注册失败",
"message": data.data.message
}
}
user = data.data.user
orgid = user.orgid
try:
await openCustomerAccounts(sor, '0', orgid)
debug(f'{orgid} accounts opened')
except Exception as e:
exception(f'{e},{orgid=}')
# 注册成功后自动登录
await remember_user(user.id, username=user.username, userorgid=user.orgid)
return {
"widgettype": "Message",
"options": {
"timeout": 3,
"auto_open": True,
"title": "注册成功",
"message": f"{user.username} 注册成功,已自动登录"
},
"binds": [
{
"wid": "self",
"event": "dismissed",
"actiontype": "urlwidget",
"target": "window.user_container",
"options": {
"url": entire_url('/rbac/user/userinfo.ui')
}
},
{
"wid": "self",
"event": "dismissed",
"actiontype": "script",
"target": "body.login_window",
"script": "this.destroy()"
},
{
"wid": "self",
"event": "dismissed",
"actiontype": "script",
"target": "self",
"script": "if(bricks.app && bricks.app.dispatch) bricks.app.dispatch('user_logined')"
}
]
}
except Exception as e:
exception(f'sms_register error: {e}')
return {
"widgettype": "Error",
"options": {
"timeout": 5,
"title": "系统错误",
"message": f"注册失败: {e}"
}
}
return {
"widgettype": "Error",
"options": {
"timeout": 5,
"title": "注册失败",
"message": "系统错误,请稍后重试"
}
}