feat: 现代化登录/注册界面改造
- login.css: 全新现代化样式,支持亮/暗主题 - login.ui: 三Tab布局(密码登录/手机登录/注册),手机登录支持短信验证码 - sms_register.dspy: 短信验证注册后端,验证通过后自动注册并登录 - load_path.py: 添加 sms_register.dspy 到 any 权限 - 修复手机登录 setValue 调用 (上一轮已提交) - 注册流程: 手机号+短信验证码+用户名+密码,短信验证通过后才允许注册
This commit is contained in:
parent
019e9702fc
commit
36569c0e41
@ -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
231
wwwroot/login.css
Normal 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;
|
||||
}
|
||||
@ -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('发送验证码')})"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
187
wwwroot/user/sms_register.dspy
Normal file
187
wwwroot/user/sms_register.dspy
Normal 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": "系统错误,请稍后重试"
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user