diff --git a/CHANGELOG.md b/CHANGELOG.md index f8b544b8715ed8605e2080ee95ff10a35d4de81e..c42948610b6bb74224051149428092c096598d4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,9 @@ 并且此项目遵循 [Semantic Versioning](https://semver.org/lang/zh-CN/). ## [Unreleased] +### Changed +- 优化人机识别校验 ## [0.7.12] - 2024-05-12 ### Added diff --git a/src/panel-component/auth-captcha/auth-captcha.controller.ts b/src/panel-component/auth-captcha/auth-captcha.controller.ts index 27dfa0cbd6567a206225522df8fd61318942b5ee..02cd95c9fe92b138a3add6fd884cb7d3b25964bd 100644 --- a/src/panel-component/auth-captcha/auth-captcha.controller.ts +++ b/src/panel-component/auth-captcha/auth-captcha.controller.ts @@ -1,8 +1,13 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable import/no-extraneous-dependencies */ /* eslint-disable @typescript-eslint/no-this-alias */ +import { reactive } from 'vue'; import { IPanelRawItem } from '@ibiz/model-core'; -import { PanelItemController, PanelNotifyState } from '@ibiz-template/runtime'; +import { + AppLoginViewController, + PanelItemController, + PanelNotifyState, +} from '@ibiz-template/runtime'; import axios, { AxiosRequestConfig } from 'axios'; import { AuthCaptchaState } from './auth-captcha.state'; @@ -16,6 +21,17 @@ import { AuthCaptchaState } from './auth-captcha.state'; export class AuthCaptchaController extends PanelItemController { declare state: AuthCaptchaState; + /** + * 验证码数据 + * + * @private + * @memberof AuthCaptchaController + */ + private captcha = reactive({ + 'Captcha-State': '', + 'Captcha-Code': '', + }); + protected createState(): AuthCaptchaState { return new AuthCaptchaState(this.parent?.state); } @@ -29,17 +45,7 @@ export class AuthCaptchaController extends PanelItemController { */ async panelStateNotify(_state: PanelNotifyState): Promise { super.panelStateNotify(_state); - const that = this; - Object.defineProperty(that.panel.state.data, 'captcha', { - enumerable: true, - configurable: true, - get() { - return { - captcha_state: that.state.state, - captcha_code: that.state.code, - }; - }, - }); + this.data.captcha = this.captcha; } /** @@ -52,6 +58,45 @@ export class AuthCaptchaController extends PanelItemController { protected async onInit(): Promise { super.onInit(); await this.loadCaptcha(); + const view = this.panel.view as AppLoginViewController; + view.hooks.beforeLogin.tapPromise(async context => { + if (!context.parentId || context.parentId === this.dataParent.model.id!) { + context.validate = context.validate && (await this.validate()); + } + }); + view.hooks.afterLogin.tap(context => { + if (!context.ok) { + this.loadCaptcha(); + } + }); + } + + /** + * 值校验 + * + * @return {*} {Promise} + * @memberof AuthCaptchaController + */ + async validate(): Promise { + // 人机识别目前仅支持空值校验 + if (this.state.code) { + this.state.error = undefined; + return true; + } + this.state.error = '请输入验证码'; + return false; + } + + /** + * 值改变 + * + * @memberof AuthCaptchaController + */ + onChange(): void { + Object.assign(this.captcha, { + 'Captcha-State': this.state.state, + 'Captcha-Code': this.state.code, + }); } /** @@ -71,11 +116,18 @@ export class AuthCaptchaController extends PanelItemController { }, data: {}, }; - const res = await axios(requestConfig); - if (res.status === 200 && res.data) { - this.state.state = res.data.state; - this.state.image = res.data.image; + try { + const res = await axios(requestConfig); + if (res.status === 200 && res.data) { + this.state.state = res.data.state; + this.state.image = res.data.image; + } + } catch (error) { + this.state.state = ''; + this.state.image = ''; + } finally { + this.state.loading = false; + this.onChange(); } - this.state.loading = false; } } diff --git a/src/panel-component/auth-captcha/auth-captcha.scss b/src/panel-component/auth-captcha/auth-captcha.scss index 38fe39fe7c1868732660e4a91fd0608730175b09..4c3e3a4a44ab3ac897ebbe1d006f8976fe825a87 100644 --- a/src/panel-component/auth-captcha/auth-captcha.scss +++ b/src/panel-component/auth-captcha/auth-captcha.scss @@ -1,5 +1,6 @@ @include b(auth-captcha) { display: flex; + position: relative; @include e('captcha') { width: calc(100% - 120px); } @@ -9,11 +10,27 @@ height: 100%; cursor: pointer; @include m('hint') { + width: 100%; + height: 100%; display: flex; align-items: center; justify-content: center; - width: 100%; - height: 100%; + color: var(--ibiz-color-text-3); + background-color: var(--ibiz-color-fill-0); + } + } + @include e('error') { + left: 0; + top: 100%; + line-height: 1; + font-size: 12px; + padding-top: 2px; + position: absolute; + color: var(--ibiz-color-danger); + } + @include when(error) { + .el-input__wrapper { + box-shadow: 0 0 0 1px var(--ibiz-color-danger) inset; } } } diff --git a/src/panel-component/auth-captcha/auth-captcha.state.ts b/src/panel-component/auth-captcha/auth-captcha.state.ts index bbf52936940e3e0843a00be87ff1e5e650d223e7..9139babc951c356801772a804c9bbe84813c887f 100644 --- a/src/panel-component/auth-captcha/auth-captcha.state.ts +++ b/src/panel-component/auth-captcha/auth-captcha.state.ts @@ -39,4 +39,12 @@ export class AuthCaptchaState extends PanelItemState { * @memberof AuthCaptchaState */ loading: boolean = false; + + /** + * 错误信息 + * + * @type {string} + * @memberof AuthCaptchaState + */ + error?: string; } diff --git a/src/panel-component/auth-captcha/auth-captcha.tsx b/src/panel-component/auth-captcha/auth-captcha.tsx index d75a92297003bdec1d76f8fe17794968febaa375..96a37d6e2d6df24431a3f33b05fbe7d6513a4ffa 100644 --- a/src/panel-component/auth-captcha/auth-captcha.tsx +++ b/src/panel-component/auth-captcha/auth-captcha.tsx @@ -29,6 +29,7 @@ export const AuthCaptcha = defineComponent({ ...result, ...props.controller.containerClass, ns.is('hidden', !props.controller.state.visible), + ns.is('error', !!c.state.error), ]; return result; }); @@ -42,11 +43,17 @@ export const AuthCaptcha = defineComponent({ } }; + const onChange = () => { + c.onChange(); + c.validate(); + }; + return { c, ns, classArr, onClick, + onChange, }; }, render() { @@ -55,6 +62,8 @@ export const AuthCaptcha = defineComponent({ + {this.c.state.error && ( +
{this.c.state.error}
+ )} ); }, diff --git a/src/panel-component/panel-button/panel-button.controller.ts b/src/panel-component/panel-button/panel-button.controller.ts index ce08e385f9afc0b0fed09f09f6944a0efb452b34..39cc4037b63580ea80b3b08eea8c0123f9f298cb 100644 --- a/src/panel-component/panel-button/panel-button.controller.ts +++ b/src/panel-component/panel-button/panel-button.controller.ts @@ -1,4 +1,6 @@ +/* eslint-disable object-shorthand */ import { + AppLoginViewController, PanelController, PanelItemController, PanelNotifyState, @@ -134,13 +136,25 @@ export class PanelButtonController extends PanelItemController { } event.stopPropagation(); event.preventDefault(); + // 登录按钮在应用登录视图上需提前校验登录表单 + const view = this.panel.view; + if ( + actionType === 'UIACTION' && + uiactionId === 'login' && + view.model.viewType === 'APPLOGINVIEW' && + !(await (view as AppLoginViewController).validate( + this.dataParent.model.id!, + )) + ) { + return; + } await UIActionUtil.execAndResolved( uiactionId!, { context: this.panel.context, params: this.panel.params, data: [this.data], - view: this.panel.view, + view: view, event, noWaitRoute: true, }, diff --git a/src/view-engine/login-view.engine.ts b/src/view-engine/login-view.engine.ts index 6caaa2e2e153633593eb555bdf8b147e221cd127..310e254a6674f30ee477ea76b5d1a32de31769f5 100644 --- a/src/view-engine/login-view.engine.ts +++ b/src/view-engine/login-view.engine.ts @@ -2,15 +2,24 @@ import { SysUIActionTag, ViewController, ViewEngineBase, + AppLoginViewController, + IViewState, + IViewEvent, } from '@ibiz-template/runtime'; import { RouteLocationNormalizedLoaded, useRoute } from 'vue-router'; -import { IPanelField } from '@ibiz/model-core'; +import { IPanelField, IAppView } from '@ibiz/model-core'; import { PanelFieldController } from '@ibiz-template/vue3-util'; import { notNilEmpty } from 'qx-util'; export class LoginViewEngine extends ViewEngineBase { route: RouteLocationNormalizedLoaded = useRoute(); + protected declare view: AppLoginViewController< + IAppView, + IViewState, + IViewEvent + >; + get AppLoginView(): ViewController { return this.view.getController('AppLoginView') as ViewController; } @@ -41,22 +50,22 @@ export class LoginViewEngine extends ViewEngineBase { async login(args: IData): Promise { let rememberme; const headers: IData = {}; + const data = args.data[0] || {}; if (this.AppLoginView.layoutPanel) { const panelData: IData = this.AppLoginView.layoutPanel.data; // 记住我 if (typeof panelData.isRemember === 'boolean') { rememberme = panelData.isRemember; } - // 验证码 - if (panelData.captcha) { - Object.assign(headers, panelData.captcha); + // 验证码从登录表单中获取 + if (data.captcha) { + Object.assign(headers, data.captcha); } // 自定义请求头数据 if (panelData.srfheaders) { Object.assign(headers, panelData.srfheaders); } } - const data = args.data[0] || {}; let username = data.username; if (notNilEmpty(data.orgid)) { username = `${data.username}@${data.orgid}`; @@ -67,7 +76,7 @@ export class LoginViewEngine extends ViewEngineBase { rememberme, headers, ); - + this.view.hooks.afterLogin.call({ ok: bol }); if (bol === true) { window.location.hash = (this.route.query.ru as string) || '/'; window.location.reload();