授权组件ui调整 mobile绑定手机号

This commit is contained in:
Wang Kejun 2023-02-08 17:31:21 +08:00
parent c65043e2c1
commit 56b6132dc2
19 changed files with 515 additions and 217 deletions

View File

@ -1,8 +1,6 @@
.oak-loginGrant {
&_dev {
height: 280px;
border: 1px dashed var(--oak-color-primary);
padding: 10px;
height: 180px;
&_header {
display: flex;
@ -42,31 +40,7 @@
box-shadow: 0 2px #00000004;
cursor: pointer;
transition: .3s;
}
&_btn:hover {
color: var(--oak-color-primary);
border-color: currentColor;
}
&_btn::after {
/*其他样式*/
opacity: 0;
box-shadow: 0 0 0 6px var(--oak-color-primary);
transition: .3s;
}
/*点击*/
&_btn:active::after {
box-shadow: none;
opacity: 0.4;
transition: 0s;
/*取消过渡*/
}
}
@ -93,9 +67,7 @@
}
&_prod {
height: 280px;
border: 1px dashed var(--oak-color-primary);
padding: 10px;
height: 180px;
display: flex;
flex-direction: column;

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
import { message } from 'antd';
import { message, Button } from 'antd';
import { random, template } from 'oak-domain/lib/utils/string';
import classNames from 'classnames';
import './index.less';
@ -65,11 +65,14 @@ function Grant(props: GrantProps) {
setCode(e.target.value);
}}
></input>
<button
className={`${prefixCls2}_dev_header_btn`}
<Button
type="primary"
shape="round"
size="large"
// block
onClick={() => {
if (disabled) {
messageApi.info(disableText || '禁用');
messageApi.info(disableText || 'disabled');
return;
}
window.location.href =
@ -77,13 +80,10 @@ function Grant(props: GrantProps) {
`?code=${code}&state=${state}`;
}}
>
</button>
</Button>
</div>
<div className={`${prefixCls2}_dev_bottom`}>
<span className={`${prefixCls2}_dev_bottom_title`}>
</span>
<span className={`${prefixCls2}_dev_bottom_desc`}>
1
</span>
@ -98,12 +98,15 @@ function Grant(props: GrantProps) {
V = (
<div className={`${prefixCls2}_prod`}>
<div className={`${prefixCls2}_prod_header`}>
<button
className={`${prefixCls2}_prod_header_btn`}
<Button
type="primary"
shape="round"
size="large"
// block
onClick={() => {
if (disabled) {
messageApi.info(disableText || '禁用');
return
messageApi.info(disableText || 'disabled');
return;
}
const url = WeChatLoginUrl(
redirectUri,
@ -115,13 +118,8 @@ function Grant(props: GrantProps) {
window.location.href = url;
}}
>
</button>
</div>
<div className={`${prefixCls2}_prod_bottom`}>
<span className={`${prefixCls2}_prod_bottom_title`}>
</span>
</Button>
</div>
</div>
);

View File

@ -1,5 +1,5 @@
{
"navigationBarTitleText": "手机号登录",
"navigationBarTitleText": "绑定手机号",
"usingComponents": {
"l-button": "../../../miniprogram_npm/lin-ui/button/index"
}

View File

@ -25,9 +25,11 @@ export default OakComponent({
if (typeof lastSendAt === 'number') {
counter = Math.max(60 - Math.ceil((now - lastSendAt) / 1000), 0);
if (counter > 0) {
(this as any).counterHandler = setTimeout(() => this.reRender(), 1000);
}
else if ((this as any).counterHandler) {
(this as any).counterHandler = setTimeout(
() => this.reRender(),
1000
);
} else if ((this as any).counterHandler) {
clearTimeout((this as any).counterHandler);
(this as any).counterHandler = undefined;
}
@ -37,11 +39,14 @@ export default OakComponent({
};
},
methods: {
onInput(e: any) {
const { dataset, value } = this.resolveInput(e);
const { attr } = dataset;
setMobile(value: string) {
this.setState({
[attr]: value,
mobile: value,
});
},
setCaptcha(value: string) {
this.setState({
captcha: value,
});
},
async sendCaptcha() {
@ -55,8 +60,7 @@ export default OakComponent({
});
this.save(SEND_KEY, Date.now());
this.reRender();
}
catch (err) {
} catch (err) {
this.setMessage({
type: 'error',
content: (err as Error).message,
@ -67,20 +71,22 @@ export default OakComponent({
const { eventLoggedIn } = this.props;
const { mobile, password, captcha } = this.state;
try {
await this.features.token.loginByMobile(mobile, password, captcha);
await this.features.token.loginByMobile(
mobile,
password,
captcha
);
if (eventLoggedIn) {
this.pub(eventLoggedIn);
}
else {
} else {
this.navigateBack();
}
}
catch (err) {
} catch (err) {
this.setMessage({
type: 'error',
content: (err as Error).message,
});
}
}
},
},
});

View File

@ -1,5 +1,5 @@
{
"Login": "进入",
"Login": "确定",
"Send": "发送验证码",
"placeholder": {
"Captcha": "输入4位短信验证码",

View File

@ -0,0 +1,101 @@
.loginbox-main {
height: 100vh;
display: flex;
flex: 1;
align-items: center;
flex-direction: column;
background: var(--oak-bg-color-container);
padding-top: 20%;
}
.loginbox-wrap {
width: 90%;
display: block;
background: var(--oak-bg-color-container);
border-radius: 4px;
overflow: hidden;
box-shadow: 0 2px 4px rgb(0 0 0 / 8%), 0 0 4px rgb(0 0 0 / 8%);
}
.loginbox-hd {
padding: 32px;
font-size: 14px;
font-weight: 500;
}
.loginbox-bd {
height: 200px;
}
.loginbox-only {
padding-top: 32px !important;
}
.loginbox-mobile {
position: relative;
padding: 0 32px;
}
.loginbox-password {
position: relative;
padding: 0 32px;
}
.loginbox-qrcode {
padding: 0 32px;
font-size: 14px;
&__sociallogin {
margin-bottom: 15px;
text-align: center;
color: #999;
}
&__refresh {
color: var(--oak-text-color-brand);
margin-left: 10px;
cursor: pointer;
&-icon {
color: var(--oak-text-color-brand)
}
}
&__iframe {
position: relative;
width: 160px;
height: 160px;
margin: 0 auto;
border: #999 solid 1px;
}
}
.current {
color: var(--oak-text-color-brand) !important;
cursor: default;
background-color: #fff;
border-radius: 4px;
}
.loginbox-input {
.t-input {
background-color: rgba(0, 0, 0, .04) !important;
}
}
.loginbox-ft {
height: 54px;
border-top: 1px solid #f2f3f5;
font-size: 14px;
&__btn {}
}
.loginbox-protocal {
padding: 20px 32px;
}

View File

@ -21,18 +21,8 @@
.loginbox-hd {
padding: 32px;
&__tab {
width: 100%;
height: 38px;
background-color: rgba(0, 0, 0, .04) !important;
}
&__tabcon {
width: 100%;
display: flex;
justify-content: center;
}
font-size: 14px;
font-weight: 500;
}
.loginbox-bd {

View File

@ -0,0 +1,118 @@
import React from 'react';
import {
isMobile,
isPassword,
isCaptcha,
} from 'oak-domain/lib/utils/validator';
import { WebComponentProps } from 'oak-frontend-base';
import { EntityDict } from '../../../general-app-domain';
import { MobileOutlined } from '@ant-design/icons';
import { Form, Input, Button } from 'antd';
import Style from './web.module.less';
export default function render(
props: WebComponentProps<
EntityDict,
'token',
false,
{
counter: number;
loginMode?: number;
loginAgreed?: boolean;
appId: string;
onlyCaptcha?: boolean;
onlyPassword?: boolean;
loading: boolean;
backUrl?: string;
mobile: string;
captcha: string;
},
{
setCaptcha: (mobile: string) => void;
setMobile: (mobile: string) => void;
sendCaptcha: () => Promise<void>;
loginByMobile: () => Promise<void>;
}
>
) {
const { mobile, captcha, counter } = props.data;
const { t, setMobile, setCaptcha, sendCaptcha, loginByMobile } =
props.methods;
const validMobile = isMobile(mobile);
const validCaptcha = isCaptcha(captcha);
const allowSubmit = validMobile && validCaptcha;
const LoginCaptcha = (
<Form colon={true}>
<Form.Item name="mobile">
<Input
allowClear
value={mobile}
data-attr="mobile"
type="tel"
maxLength={11}
prefix={<MobileOutlined />}
placeholder={t('placeholder.Mobile')}
size="large"
onChange={(e) => {
setMobile(e.target.value);
}}
className={Style['loginbox-input']}
/>
</Form.Item>
<Form.Item name="captcha">
<Input
allowClear
value={captcha}
data-attr="captcha"
// type="number"
maxLength={4}
placeholder={t('placeholder.Captcha')}
size="large"
onChange={(e) => {
setCaptcha(e.target.value);
}}
className={Style['loginbox-input']}
suffix={
<Button
type="link"
disabled={!validMobile || counter > 0}
onClick={() => sendCaptcha()}
>
{counter > 0 ? `${counter}秒后可重发` : t('Send')}
</Button>
}
/>
</Form.Item>
<Form.Item>
<Button
block
size="large"
type="primary"
htmlType="submit"
disabled={!allowSubmit}
onClick={() => loginByMobile()}
>
{t('Login')}
</Button>
</Form.Item>
</Form>
);
return (
<div className={Style['loginbox-main']}>
<div className={Style['loginbox-wrap']}>
<div className={Style['loginbox-hd']}>
</div>
<div className={Style['loginbox-bd']}>
<div className={Style['loginbox-mobile']}>
{LoginCaptcha}
</div>
</div>
</div>
</div>
);
}

View File

@ -6,10 +6,9 @@ import {
} from 'oak-domain/lib/utils/validator';
import { WebComponentProps } from 'oak-frontend-base';
import { EntityDict } from '../../../general-app-domain';
import { MailOutlined } from '@ant-design/icons';
import { Form, Input, Button, Checkbox } from 'antd';
import Style from './web.module.less';
import { MobileOutlined } from '@ant-design/icons';
import { Form, Input, Button } from 'antd';
import Style from './mobile.module.less';
export default function render(
props: WebComponentProps<
@ -30,15 +29,16 @@ export default function render(
password: string;
},
{
sendCaptcha: (mobile: string) => Promise<void>;
loginByMobile: (
mobile: string,
password?: string,
captcha?: string
) => Promise<void>;
setCaptcha: (mobile: string) => void;
setMobile: (mobile: string) => void;
sendCaptcha: () => Promise<void>;
loginByMobile: () => Promise<void>;
}
>) {
/* const { mobile, captcha, password, counter } = props.data;
>
) {
const { mobile, captcha, password, counter } = props.data;
const { t, setMobile, setCaptcha, sendCaptcha, loginByMobile } =
props.methods;
const validMobile = isMobile(mobile);
const validCaptcha = isCaptcha(captcha);
const allowSubmit = validMobile && validCaptcha;
@ -52,13 +52,11 @@ export default function render(
data-attr="mobile"
type="tel"
maxLength={11}
prefix={<MailOutlined />}
placeholder={this.t('placeholder.Mobile')}
prefix={<MobileOutlined />}
placeholder={t('placeholder.Mobile')}
size="large"
onChange={(e) => {
this.setState({
mobile: e.target.value,
});
setMobile(e.target.value);
}}
className={Style['loginbox-input']}
/>
@ -70,23 +68,19 @@ export default function render(
data-attr="captcha"
// type="number"
maxLength={4}
placeholder={this.t('placeholder.Captcha')}
placeholder={t('placeholder.Captcha')}
size="large"
onChange={(e) => {
this.setState({
captcha: e.target.value,
});
setCaptcha(e.target.value);
}}
className={Style['loginbox-input']}
suffix={
<Button
type="link"
disabled={!validMobile || counter > 0}
onClick={() => this.sendCaptcha()}
onClick={() => sendCaptcha()}
>
{counter > 0
? `${counter}秒后可重发`
: this.t('Send')}
{counter > 0 ? `${counter}秒后可重发` : t('Send')}
</Button>
}
/>
@ -99,9 +93,9 @@ export default function render(
type="primary"
htmlType="submit"
disabled={!allowSubmit}
onClick={() => this.loginByMobile()}
onClick={() => loginByMobile()}
>
{this.t('Login')}
{t('Login')}
</Button>
</Form.Item>
</Form>
@ -111,26 +105,14 @@ export default function render(
<div className={Style['loginbox-main']}>
<div className={Style['loginbox-wrap']}>
<div className={Style['loginbox-hd']}>
</div>
<div className={Style['loginbox-bd']}>
<div
className={Style['loginbox-mobile']}
>
<div className={Style['loginbox-mobile']}>
{LoginCaptcha}
</div>
</div>
<div className={Style['loginbox-ft']}>
<div className={Style['loginbox-ft__btn']}>
<div className={Style['loginbox-protocal']}>
<Checkbox>
<div> </div>
</Checkbox>
</div>
</div>
</div>
</div>
</div>
); */
return null;
);
}

View File

@ -8,25 +8,27 @@ export default OakComponent({
mobile: 1,
userId: 1,
},
filters: [{
filter() {
const token = this.features.token.getToken();
return {
userId: {
$in: {
entity: 'token',
data: {
userId: 1,
},
filter: {
id: token?.id,
ableState: 'enabled',
filters: [
{
filter() {
const token = this.features.token.getToken();
return {
userId: {
$in: {
entity: 'token',
data: {
userId: 1,
},
filter: {
id: token?.id,
ableState: 'enabled',
},
},
},
},
};
};
},
},
}],
],
formData: ({ data: mobiles }) => {
return {
mobiles,
@ -38,6 +40,9 @@ export default OakComponent({
refreshing: false,
deleteIdx: undefined,
},
properties: {
showBack: Boolean,
},
methods: {
async onRefreshMobile(e: any) {
this.setState({
@ -67,12 +72,11 @@ export default OakComponent({
const eventLoggedIn = `mobile:me:login:${Date.now()}`;
this.sub(eventLoggedIn, () => {
this.navigateBack();
})
});
this.navigateTo({
url: '/mobile/login',
onlyCaptcha: true,
eventLoggedIn,
});
}
},
},
});

View File

@ -0,0 +1,25 @@
.container {
height: 100vh;
display: flex;
flex-direction: column;
}
.list {
:global {
.t-list-item__meta {
&-avatar {
width: auto !important;
height: auto !important;
background: transparent;
border-radius: unset;
}
&-title {
margin: 0 !important;
}
}
}
}

View File

@ -1,25 +1,12 @@
.container {
height: 100vh;
display: flex;
flex-direction: column;
background: var(--oak-bg-color-container);
box-shadow: 0 2px 3px #0000001a;
border-radius: 3px;
padding: 30px 32px;
}
.list {
:global {
.t-list-item__meta {
&-avatar {
width: auto !important;
height: auto !important;
background: transparent;
border-radius: unset;
}
&-title {
margin: 0 !important;
}
}
}
margin-top: 10px !important;
}

View File

@ -0,0 +1,71 @@
import React, { useState } from 'react';
import { List, Button, Modal } from 'antd';
import { MobileOutlined, DeleteOutlined } from '@ant-design/icons';
import Style from './web.module.less';
import { WebComponentProps } from 'oak-frontend-base';
import { EntityDict } from '../../../general-app-domain';
import PageHeader from '../../../components/common/pageHeader';
export default function render(
props: WebComponentProps<
EntityDict,
'mobile',
true,
{
mobiles?: EntityDict['mobile']['OpSchema'][];
allowRemove: boolean;
showBack: boolean;
},
{
goAddMobile: () => void;
}
>
) {
const { mobiles, allowRemove, showBack = false } = props.data;
const { goAddMobile, removeItem, execute } = props.methods;
return (
<PageHeader showBack={showBack} title="我的手机号">
<div className={Style.container}>
<Button type="primary" onClick={() => goAddMobile()}>
</Button>
<List bordered className={Style.list}>
{mobiles?.map((ele, index) => (
<List.Item
key={index}
extra={
allowRemove && (
<div
onClick={() => {
const modal = Modal!.confirm!({
title: `确认删除吗?删除后无法用此号码登录`,
okText: '确定',
cancelText: '取消',
onOk: async (e) => {
removeItem(ele.id);
await execute();
modal!.destroy!();
},
onCancel: (e) => {
modal!.destroy!();
},
});
}}
>
<DeleteOutlined />
</div>
)
}
>
<List.Item.Meta
avatar={<MobileOutlined />}
title={ele.mobile}
></List.Item.Meta>
</List.Item>
))}
</List>
</div>
</PageHeader>
);
}

View File

@ -1,18 +1,24 @@
import React, { useState } from 'react';
import { List, Button, Modal, Dialog } from 'antd-mobile';
import { MobileOutlined, DeleteOutlined } from '@ant-design/icons';
import Style from './web.module.less';
import { WebComponentProps } from 'oak-frontend-base';
import { EntityDict } from '../../../general-app-domain';
import Style from './mobile.module.less';
export default function render(props: WebComponentProps<EntityDict, 'mobile', true, {
mobiles?: EntityDict['mobile']['OpSchema'][];
allowRemove: boolean;
}, {
goAddMobile: () => Promise<void>;
}>) {
export default function render(
props: WebComponentProps<
EntityDict,
'mobile',
true,
{
mobiles?: EntityDict['mobile']['OpSchema'][];
allowRemove: boolean;
},
{
goAddMobile: () => void;
}
>
) {
const { mobiles, allowRemove } = props.data;
const { goAddMobile, removeItem, execute } = props.methods;
return (
@ -23,20 +29,22 @@ export default function render(props: WebComponentProps<EntityDict, 'mobile', tr
key={index}
prefix={<MobileOutlined />}
extra={
allowRemove && <div
onClick={
async () => {
allowRemove && (
<div
onClick={async () => {
const result = await Dialog.confirm({
content: '确认删除吗?删除后无法用此号码登录',
})
content:
'确认删除吗?删除后无法用此号码登录',
});
if (result) {
removeItem(ele.id);
await execute();
}
}}
>
<DeleteOutlined />
</div>
>
<DeleteOutlined />
</div>
)
}
>
{ele.mobile}
@ -50,7 +58,7 @@ export default function render(props: WebComponentProps<EntityDict, 'mobile', tr
color="primary"
onClick={() => goAddMobile()}
>
</Button>
</div>
);

View File

@ -117,7 +117,7 @@ export default OakComponent({
content: '功能开发中',
});
},
setMobile() {
goAddMobile() {
this.navigateTo({
url: '/mobile/me',
});

View File

@ -17,7 +17,7 @@
<l-list title="生日" data-attr="birth" bind:lintap="setVisibleMp">
<view slot="right-section" class="value">{{birthText || '未设置'}}</view>
</l-list>
<l-list title="手机号" bind:lintap="setMobile">
<l-list title="手机号" bind:lintap="goAddMobile">
<view slot="right-section" class="value">{{mobile || '未绑定'}}</view>
</l-list>
<!-- <l-list tag-position="right" is-link="{{false}}" title="用户状态">

View File

@ -0,0 +1,6 @@
{
"avatar": "头像",
"mobile": "手机号",
"manage": "管理",
"bind": "绑定"
}

View File

@ -7,11 +7,13 @@ import {
Radio,
DatePicker,
Form,
Typography,
} from 'antd';
import dayjs from 'dayjs';
import { WebComponentProps } from 'oak-frontend-base';
import { EntityDict } from '../../../general-app-domain';
import PageHeader from '../../../components/common/pageHeader';
import OakAvatar from '../../../components/extraFile/avatar';
import Style from './web.module.less';
@ -33,28 +35,13 @@ export default function Render(
genderOptions: Array<{ label: string; value: string }>;
},
{
setMobile: () => void;
setAvatar: () => void;
setVisible: (visible: boolean, attr: string) => void;
setCustomData: (attr: string, value: string | number) => void;
onConfirm: (attr: string) => Promise<void>;
updateData: (attr: string, value: string | number) => void;
updateMyInfo: () => void;
goAddMobile: () => void;
}
>
) {
const { data, methods } = props;
const {
t,
clean,
setAvatar,
setVisible,
setMobile,
setCustomData,
onConfirm,
updateData,
updateMyInfo,
} = methods;
const { t, updateMyInfo, goAddMobile } = methods;
const {
nickname,
name,
@ -65,6 +52,8 @@ export default function Render(
showBack,
oakExecuting,
genderOptions,
oakFullpath,
oakDirty,
} = data;
return (
@ -74,6 +63,19 @@ export default function Render(
labelCol={{ xs: { span: 4 }, md: { span: 6 } }}
wrapperCol={{ xs: { span: 16 }, md: { span: 12 } }}
>
<Form.Item label={t('avatar')} name="extraFile$entity">
<>
<OakAvatar
oakAutoUnmount={true}
oakPath={
oakFullpath
? oakFullpath + '.extraFile$entity'
: undefined
}
entity="user"
/>
</>
</Form.Item>
<Form.Item
label={t('user:attr.name')}
name="name"
@ -167,6 +169,21 @@ export default function Render(
/>
</>
</Form.Item>
<Form.Item label={t('mobile')}>
<>
<Space>
<Typography>{mobile}</Typography>
<Button
size="small"
onClick={() => {
goAddMobile();
}}
>
{mobile ? t('manage') : t('bind')}
</Button>
</Space>
</>
</Form.Item>
<Form.Item
wrapperCol={{
xs: { offset: 4 },
@ -175,7 +192,7 @@ export default function Render(
>
<Space>
<Button
disabled={oakExecuting}
disabled={oakExecuting || !oakDirty}
type="primary"
onClick={() => {
updateMyInfo();

View File

@ -14,11 +14,10 @@ import type { DatePickerRef } from 'antd-mobile/es/components/date-picker';
import dayjs from 'dayjs';
import { WebComponentProps } from 'oak-frontend-base';
import { EntityDict } from '../../../general-app-domain';
import OakGallery from '../../../components/extraFile/gallery';
import OakAvatar from '../../../components/extraFile/avatar';
import Style from './mobile.module.less';
//todo 头像跟绑定手机号 未实现
type DataProps = {
@ -32,10 +31,11 @@ type DataProps = {
attr: string;
genderOptions: Array<{ label: string; value: string }>;
attrs: Record<string, string>;
id: string;
};
type MethodsProps = {
setMobile: () => void;
goAddMobile: () => void;
setAvatar: () => void;
setVisible: (visible: boolean, attr: string) => void;
setCustomData: (attr: string, value: string | number) => void;
@ -46,26 +46,39 @@ export default function render(
props: WebComponentProps<EntityDict, 'user', false, DataProps, MethodsProps>
) {
const { data, methods } = props;
const { t, clean, setAvatar, setVisible, setMobile } = methods;
const { visible, nickname, name, birth, gender, mobile, avatarUrl, attr } =
data;
const { t, clean, setAvatar, setVisible, goAddMobile } = methods;
const {
oakFullpath,
visible,
nickname,
name,
birth,
gender,
mobile,
avatarUrl,
attr,
id,
} = data;
return (
<div className={Style.container}>
<div className={Style.avatar_container}>
<Avatar
src={avatarUrl}
className={Style.avatar}
/>
<Button
size='mini'
color='primary'
onClick={setAvatar}
>
</Button>
</div>
<List className={Style.list}>
<List.Item
extra={
<OakAvatar
oakAutoUnmount={true}
oakPath={
oakFullpath
? oakFullpath + '.extraFile$entity'
: undefined
}
entity="user"
entityId={id}
/>
}
>
</List.Item>
<List.Item
extra={nickname ? nickname : '未设置'}
onClick={() => {
@ -93,10 +106,10 @@ export default function render(
<List.Item
extra={mobile ? mobile : '未设置'}
onClick={() => {
setMobile();
goAddMobile();
}}
>
{t('mobile')}
</List.Item>
</List>