From 36569c0e4198cd326a05235ac138e6788b4f5bd3 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Sat, 30 May 2026 14:08:11 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=8E=B0=E4=BB=A3=E5=8C=96=E7=99=BB?= =?UTF-8?q?=E5=BD=95/=E6=B3=A8=E5=86=8C=E7=95=8C=E9=9D=A2=E6=94=B9?= =?UTF-8?q?=E9=80=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - login.css: 全新现代化样式,支持亮/暗主题 - login.ui: 三Tab布局(密码登录/手机登录/注册),手机登录支持短信验证码 - sms_register.dspy: 短信验证注册后端,验证通过后自动注册并登录 - load_path.py: 添加 sms_register.dspy 到 any 权限 - 修复手机登录 setValue 调用 (上一轮已提交) - 注册流程: 手机号+短信验证码+用户名+密码,短信验证通过后才允许注册 --- scripts/load_path.py | 1 + wwwroot/login.css | 231 +++++++++++++++++++++++++++++++++ wwwroot/user/login.ui | 84 ++++++++---- wwwroot/user/sms_register.dspy | 187 ++++++++++++++++++++++++++ 4 files changed, 477 insertions(+), 26 deletions(-) create mode 100644 wwwroot/login.css create mode 100644 wwwroot/user/sms_register.dspy diff --git a/scripts/load_path.py b/scripts/load_path.py index bf89bd6..d03249f 100644 --- a/scripts/load_path.py +++ b/scripts/load_path.py @@ -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", diff --git a/wwwroot/login.css b/wwwroot/login.css new file mode 100644 index 0000000..949bd7b --- /dev/null +++ b/wwwroot/login.css @@ -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; +} diff --git a/wwwroot/user/login.ui b/wwwroot/user/login.ui index 6169b6d..2101880 100644 --- a/wwwroot/user/login.ui +++ b/wwwroot/user/login.ui @@ -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('发送验证码')})" + } + ] } ] } diff --git a/wwwroot/user/sms_register.dspy b/wwwroot/user/sms_register.dspy new file mode 100644 index 0000000..d6adaf5 --- /dev/null +++ b/wwwroot/user/sms_register.dspy @@ -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": "系统错误,请稍后重试" + } +}