message页面和一些其它页面

This commit is contained in:
Xu Chang 2022-07-15 20:29:02 +08:00
parent 12728fc81a
commit 4e135506f9
13 changed files with 428 additions and 326 deletions

View File

@ -1,135 +1,57 @@
import { concat, without } from "lodash";
import { NotificationData } from "oak-frontend-base";
interface TimedNotificationdata extends NotificationData {
dieAt: number;
};
let KILLER: number | undefined = undefined;
export default OakComponent({
options: {
multipleSlots: true,
},
externalClasses: ['l-class', 'l-image-class', 'l-lass-image'],
properties: {
zIndex: {
type: Number,
value: 777,
},
show: Boolean,
icon: String,
iconColor: {
type: String,
value: '#fff',
},
iconSize: {
type: String,
value: '28',
},
image: String,
content: String,
type: {
type: String,
value: 'info',
options: ['info', 'warning', 'success', 'error', 'loading'],
},
duration: {
type: Number,
value: 1500,
},
openApi: {
type: Boolean,
value: true,
},
/**
* message距离顶部的距离
*/
top: {
type: Number,
value: 0,
},
},
data: {
status: false,
messages: [] as TimedNotificationdata[],
},
async formData({ }) {
const data = this.consumeNotification();
if (data) {
const now = Date.now();
return {
messages: [
...this.state.messages,
Object.assign(data, {
dieAt: now + (data.duration || 3000),
})
],
};
}
return {};
},
// 解决 addListener undefined 的错误
observers: {
show: function (show) {
show && this.changeStatus();
if (!show)
this.setData({
status: show,
});
},
},
messages(messages: TimedNotificationdata[]) {
if (messages.length > 0) {
let firstDieAt: number = Number.MAX_VALUE;
let vicitim: TimedNotificationdata;
for (const message of messages) {
if (message.dieAt < firstDieAt) {
vicitim = message;
firstDieAt = vicitim.dieAt;
}
}
lifetimes: {
attached() {
this.initMessage();
},
},
pageLifetimes: {
show() {
this.initMessage();
},
},
methods: {
changeStatus() {
this.setState({
status: true,
});
// @ts-ignore
if (this.data.timer) clearTimeout(this.data.timer);
// @ts-ignore
this.data.timer = setTimeout(() => {
this.setState({
status: false,
});
// @ts-ignore
if (this.data.success) this.data.success();
// @ts-ignore
this.data.timer = null;
}, this.data.duration);
},
initMessage() {
let oak;
// 小程序有wx、web有window
if (process.env.OAK_PLATFORM === 'wechatMp') {
// @ts-ignore
oak = wx.oak || {};
} else {
// @ts-ignore
oak = window.oak || {};
if (typeof KILLER === 'number') {
clearTimeout(KILLER);
}
KILLER = setTimeout(
() => {
const messages = without(this.state.messages, vicitim);
this.setState({
messages,
});
}
, Math.max(firstDieAt - Date.now(), 0));
}
oak.showMessage = (options: {
content: string;
image: string;
type: string;
duration: number;
success: any;
top: number;
}) => {
const {
content = '',
image = '',
type = 'info',
duration = 1500,
success = null,
top = 0,
} = options;
this.data.success = success;
this.setState({
// @ts-ignore
content,
image,
duration,
type,
top,
});
this.changeStatus();
return this;
};
oak.hideMessage = () => {
this.setState({
status: false,
});
};
},
},
else {
KILLER = undefined;
}
}
}
});

View File

@ -16,61 +16,60 @@ const typeToIcon = {
};
export default function render() {
console.log('message render');
const {
type,
content,
image,
zIndex = 777,
top = 100,
icon,
iconColor = '#fff',
iconSize = 16,
show,
} = this.props;
const { status } = this.state;
return (
<div
className={classNames('l-message', 'l-class', {
[`l-message-${type}`]: type,
'l-message-show': status,
})}
style={{
zIndex: zIndex,
top: `${top}px`,
}}
>
{status && (
<React.Fragment>
<div
style={{
marginRight: '15rpx',
}}
>
<Icon
name={type}
size={iconSize}
color={type === 'warning' ? '#333' : iconColor}
/>
</div>
{image && (
<img
src={image}
className={classNames(
'l-message-image',
'l-class-image',
'l-image-class'
)}
/>
)}
{content}
</React.Fragment>
)}
</div>
);
messages,
} = this.state;
if (messages.length > 0) {
return (
<div>
{
messages.map(
(ele, index) => {
const {
type,
content,
icon,
iconColor = '#fff',
iconSize = 16
} = ele;
return (
<div
className={classNames('l-message', 'l-message-show', {
[`l-message-${type}`]: type,
})}
style={{
zIndex: 777,
top: 31 * index,
}}
>
<React.Fragment>
<div
style={{
marginRight: '15rpx',
}}
>
<Icon
name={type}
size={iconSize}
color={type === 'warning' ? '#333' : iconColor}
/>
</div>
{content}
</React.Fragment>
</div>
)
}
)
}
</div>
);
}
return null;
}
function Icon({ name, size, color }) {
const I = typeToIcon[name]
return <I style={{ fontSize: `${size}px`, color }} />;
}

View File

@ -3,7 +3,7 @@ import { composeFileUrl } from '../../../../src/utils/extraFile';
const SEND_KEY = 'captcha:sendAt';
export default OakPage({
path: 'mobile:me',
entity: 'mobile',
isList: false,
projection: {
id: 1,
mobile: 1,
@ -15,6 +15,10 @@ export default OakPage({
captcha: '',
counter: 0,
},
properties: {
onlyCaptcha: Boolean,
onlyPassword: Boolean,
},
async formData({ features }) {
const lastSendAt = features.localStorage.load(SEND_KEY);
const now = Date.now();

View File

@ -7,12 +7,157 @@ import { isMobile, isPassword, isCaptcha } from 'oak-domain/lib/utils/validator'
const { TabPane } = Tabs;
export default function render() {
const { mobile, captcha, password, counter} = this.state;
const { onlyCaptcha, onlyPassword } = this.props;
const { mobile, captcha, password, counter } = this.state;
const validMobile = isMobile(mobile);
const validCaptcha = isCaptcha(captcha);
const validPassword = isPassword(password);
const allowSubmit = validMobile && (validCaptcha|| validPassword);
const allowSubmit = validMobile && (validCaptcha || validPassword);
const LoginPassword = (
<Form
name="normal_login"
className="login-form"
initialValues={{ remember: true }}
>
<Form.Item
name="mobile"
>
<Input
allowClear
value={mobile}
type="tel"
data-attr="mobile"
maxLength={11}
prefix={<MobileOutlined className="site-form-item-icon" />}
placeholder="Mobile"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.onInput(e)}
/>
</Form.Item>
<Form.Item
name="password"
>
<Input
allowClear
value={password}
data-attr="password"
prefix={<LockOutlined className="site-form-item-icon" />}
type="password"
placeholder="Password"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.onInput(e)}
/>
</Form.Item>
<Form.Item>
<Form.Item name="remember" valuePropName="checked" noStyle>
<Checkbox>Remember me</Checkbox>
</Form.Item>
</Form.Item>
<Form.Item>
<Button
type="primary"
htmlType="submit"
className="login-form-button"
disabled={!allowSubmit}
onClick={() => this.loginByMobile()}
>
Log in
</Button>
</Form.Item>
</Form>
);
const LoginCaptcha = (
<Form
name="normal_login"
className="login-form"
initialValues={{ remember: true }}
>
<Form.Item
name="mobile"
>
<Input.Group compact>
<Input
allowClear
value={mobile}
data-attr="mobile"
type="tel"
maxLength={11}
prefix={<MobileOutlined className="site-form-item-icon" />}
placeholder="Mobile"
style={{ width: 'calc(100% - 65px)' }}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.onInput(e)}
/>
<Button
type="primary"
style={{
width: 65,
}}
disabled={!validMobile || counter > 0}
onClick={() => this.sendCaptcha()}
>
{counter > 0 ? counter : 'Send'}
</Button>
</Input.Group>
</Form.Item>
<Form.Item
name="captcha"
>
<Input
allowClear
value={captcha}
data-attr="captcha"
prefix={<FieldNumberOutlined className="site-form-item-icon" />}
type="number"
maxLength={4}
placeholder="Captcha"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.onInput(e)}
/>
</Form.Item>
<Form.Item>
<Form.Item name="remember" valuePropName="checked" noStyle>
<Checkbox>Remember me</Checkbox>
</Form.Item>
</Form.Item>
<Form.Item>
<Button
type="primary"
htmlType="submit"
className="login-form-button"
disabled={!allowSubmit}
onClick={() => this.loginByMobile()}
>
Log in
</Button>
</Form.Item>
</Form>
);
if (onlyCaptcha) {
return (
<div className='page-body'>
<div style={{
flex: 2,
}} />
{LoginCaptcha}
<div style={{
flex: 3,
}} />
</div>
);
}
else if (onlyPassword) {
return (
<div className='page-body'>
<div style={{
flex: 2,
}} />
{LoginPassword}
<div style={{
flex: 3,
}} />
</div>
);
}
return (
<div className='page-body'>
<div style={{
@ -21,122 +166,10 @@ export default function render() {
<Card className="card">
<Tabs defaultActiveKey="1" size="large" tabBarStyle={{ width: '100%' }}>
<TabPane tab="in Password" key="1">
<Form
name="normal_login"
className="login-form"
initialValues={{ remember: true }}
>
<Form.Item
name="mobile"
>
<Input
allowClear
value={mobile}
type="tel"
data-attr="mobile"
maxLength={11}
prefix={<MobileOutlined className="site-form-item-icon" />}
placeholder="Mobile"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.onInput(e)}
/>
</Form.Item>
<Form.Item
name="password"
>
<Input
allowClear
value={password}
data-attr="password"
prefix={<LockOutlined className="site-form-item-icon" />}
type="password"
placeholder="Password"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.onInput(e)}
/>
</Form.Item>
<Form.Item>
<Form.Item name="remember" valuePropName="checked" noStyle>
<Checkbox>Remember me</Checkbox>
</Form.Item>
</Form.Item>
<Form.Item>
<Button
type="primary"
htmlType="submit"
className="login-form-button"
disabled={!allowSubmit}
onClick={() => this.loginByMobile()}
>
Log in
</Button>
</Form.Item>
</Form>
{LoginPassword}
</TabPane>
<TabPane tab="in Captcha" key="2">
<Form
name="normal_login"
className="login-form"
initialValues={{ remember: true }}
>
<Form.Item
name="mobile"
>
<Input.Group compact>
<Input
allowClear
value={mobile}
data-attr="mobile"
type="tel"
maxLength={11}
prefix={<MobileOutlined className="site-form-item-icon" />}
placeholder="Mobile"
style={{ width: 'calc(100% - 65px)' }}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.onInput(e)}
/>
<Button
type="primary"
style={{
width:65,
}}
disabled={!validMobile || counter > 0}
onClick={() => this.sendCaptcha()}
>
{counter > 0 ? counter : 'Send'}
</Button>
</Input.Group>
</Form.Item>
<Form.Item
name="captcha"
>
<Input
allowClear
value={captcha}
data-attr="captcha"
prefix={<FieldNumberOutlined className="site-form-item-icon" />}
type="number"
maxLength={4}
placeholder="Captcha"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.onInput(e)}
/>
</Form.Item>
<Form.Item>
<Form.Item name="remember" valuePropName="checked" noStyle>
<Checkbox>Remember me</Checkbox>
</Form.Item>
</Form.Item>
<Form.Item>
<Button
type="primary"
htmlType="submit"
className="login-form-button"
disabled={!allowSubmit}
onClick={() => this.loginByMobile()}
>
Log in
</Button>
</Form.Item>
</Form>
{LoginCaptcha}
</TabPane>
</Tabs>
</Card>

View File

@ -8,6 +8,7 @@
flex: 1;
flex-direction: column;
box-sizing: border-box;
align-items: stretch;
background-color: @background-color-base;
.safe-area-inset-bottom();
}
@ -23,6 +24,12 @@
padding: @size-spacing-base;
margin: @size-spacing-base;
margin-bottom: 0;
align-items: center;
justify-content: space-between;
text {
font-size: x-large;
}
}
.btn-box {
display: flex;

View File

@ -33,6 +33,11 @@ export default OakPage({
formData: async ({ data: mobiles }) => ({
mobiles,
}),
data: {
confirmDeleteModalVisible: false,
refreshing: false,
deleteIdx: undefined,
},
methods: {
async onRefreshMobile(e: any) {
this.setState({
@ -47,5 +52,17 @@ export default OakPage({
refreshing: false,
});
},
goAddMobile() {
const eventLoggedIn = `mobile:me:login:${Date.now()}`;
this.sub(eventLoggedIn, () => {
this.navigateBack();
})
this.navigateTo({
url: '/mobile/login',
onlyCaptcha: true,
eventLoggedIn,
});
}
},
});

View File

@ -1,9 +1,61 @@
import React, { Component } from 'react';
import { List, Modal, Typography, Button } from 'antd';
const { Title } = Typography;
import { PhoneOutlined, MinusOutlined } from '@ant-design/icons';
export default function render() {
const { mobiles, confirmDeleteModalVisible, deleteIdx } = this.state;
return (
<div>
react
<div className='page-body'>
<List
grid={{ gutter: 16, column: 1 }}
itemLayout="horizontal"
dataSource={mobiles}
renderItem={(item, idx) => (
<List.Item>
<div className='card'>
<text>{item.mobile}</text>
<Button
type='primary'
shape='circle'
icon={<MinusOutlined style={{ fontSize: 20 }} />}
size="large"
onClick={() => {
this.setState({
confirmDeleteModalVisible: true,
deleteIdx: idx,
});
}}
/>
</div>
</List.Item>
)}
/>
<div style={{ flex: 1 }} />
<div className='btn-box'>
<Button size='large' type="primary" block onClick={() => this.goAddMobile()}>
</Button>
</div>
<Modal
closable={false}
visible={confirmDeleteModalVisible}
onOk={async () => {
this.execute('remove', undefined, `${deleteIdx}`);
this.setState({
confirmDeleteModalVisible: false,
deleteIdx: undefined,
});
}}
onCancel={() => this.setState({
confirmDeleteModalVisible: false,
deleteIdx: undefined,
})}
okText="确认"
cancelText="取消"
>
</Modal>
</div>
);
}

View File

@ -56,8 +56,13 @@ export default OakPage({
filters: [{
filter: async ({ features }) => {
const tokenId = await features.token.getToken();
if (tokenId) {
return {
id: tokenId,
};
}
return {
id: tokenId,
id: 'none',
};
},
}],
@ -89,8 +94,13 @@ export default OakPage({
},
data: {
refreshing: false,
showDrawer: false,
},
methods: {
setValue(input: any) {
const { dataset, value } = this.resolveInput(input);
this.setUpdateData(dataset!.attr, value);
},
async onRefresh() {
this.setState({
refreshing: true,

View File

@ -1,9 +1,9 @@
import React, { Component } from 'react';
import { UserOutlined, RightOutlined } from '@ant-design/icons';
import { Avatar, Image, Button, List } from 'antd';
import { Avatar, Image, Button, List, Drawer, Input } from 'antd';
export default function render() {
const { avatar, nickname, isLoggedIn, refreshing, mobile, mobileCount } = this.state;
const { avatar, nickname, isLoggedIn, refreshing, mobile, mobileCount, showDrawer, oakDirty, bbb } = this.state;
const mobileText = mobileCount > 1 ? `${mobileCount}条手机号` : ( mobile || '未设置');
return (
<div className='page-body'>
@ -18,7 +18,9 @@ export default function render() {
size="small"
disabled={refreshing}
loading={refreshing}
onClick={() => this.onRefresh()}
onClick={() => this.setState({
showDrawer: true,
})}
>
</Button> :
@ -41,12 +43,48 @@ export default function render() {
<List.Item.Meta
title="手机号"
description={mobileText}
onClick={() => console.log('aaa')}
onClick={() => this.goMyMobile()}
/>
<RightOutlined />
</List.Item>
</List>
</div>
<Drawer
height={150}
closable={false}
placement="bottom"
visible={showDrawer}
onClose={() => {
this.setState({ showDrawer: false });
this.resetUpdateData();
}}
>
<Input
size="large"
placeholder="请输入昵称"
value={bbb}
onChange={(input) => {
console.log(input.currentTarget.value);
this.setState({
bbb: input.currentTarget.value,
});
}}
/>
<div style={{ height: 15 }} />
<Button
size="large"
type="primary"
disabled={!oakDirty}
block
onClick={async () => {
await this.execute('update', undefined, '0.user');
this.setState({ showDrawer: false });
this.resetUpdateData();
}}
>
{this.t('common:confirm')}
</Button>
</Drawer>
</div>
);
}

View File

@ -1,6 +1,6 @@
import { EntityDict } from 'general-app-domain';
import { AppType } from 'general-app-domain/Application/Schema';
import { Action, Feature } from 'oak-frontend-base';
import { Action, Feature, LocalStorage } from 'oak-frontend-base';
import { Aspect, AspectWrapper, Context, SelectRowShape } from 'oak-domain/lib/types';
import { RWLock } from 'oak-domain/lib/utils/concurrent';
import { Cache } from 'oak-frontend-base';
@ -39,27 +39,33 @@ export class Application<ED extends EntityDict, Cxt extends GeneralRuntimeContex
private application?: SelectRowShape<ED['application']['Schema'], typeof projection>;
private rwLock: RWLock;
private cache: Cache<ED, Cxt, AD & CommonAspectDict<ED, Cxt>>;
private storage: LocalStorage<ED, Cxt, AD & CommonAspectDict<ED, Cxt>>;
constructor(
aspectWrapper: AspectWrapper<ED, Cxt, AD>,
type: AppType,
url: string,
cache: Cache<ED, Cxt, AD & CommonAspectDict<ED, Cxt>>,
storage: LocalStorage<ED, Cxt, AD & CommonAspectDict<ED, Cxt>>,
callback: (application: SelectRowShape<ED['application']['Schema'], typeof projection>) => void) {
super(aspectWrapper);
this.rwLock = new RWLock();
this.cache = cache;
this.refresh(type, url, callback);
this.storage = storage;
const applicationId = storage.load('application:applicationId');
this.rwLock.acquire('X');
if (applicationId) {
this.applicationId = applicationId;
this.getApplicationFromCache(callback);
}
else {
this.refresh(type, url, callback);
}
this.rwLock.release();
}
private async refresh(type: AppType, url: string, callback: (application: SelectRowShape<ED['application']['Schema'], typeof projection>) => void) {
this.rwLock.acquire('X');
const { result: applicationId } = await this.getAspectWrapper().exec('getApplication', {
url,
type,
});
this.applicationId = applicationId;
const result = await this.cache.get('application', {
private async getApplicationFromCache(callback: (application: SelectRowShape<ED['application']['Schema'], typeof projection>) => void) {
const { result } = await this.cache.refresh('application', {
data: projection,
filter: {
id: this.applicationId,
@ -67,10 +73,19 @@ export class Application<ED extends EntityDict, Cxt extends GeneralRuntimeContex
});
assert(result.length === 1);
this.application = result[0] as any;
this.rwLock.release();
callback(this.application!);
}
private async refresh(type: AppType, url: string, callback: (application: SelectRowShape<ED['application']['Schema'], typeof projection>) => void) {
const { result: applicationId } = await this.getAspectWrapper().exec('getApplication', {
url,
type,
});
this.applicationId = applicationId;
this.storage.save('application:applicationId', applicationId);
this.getApplicationFromCache(callback);
}
async getApplication() {
this.rwLock.acquire('S');
const result = this.application!;

View File

@ -17,8 +17,8 @@ export function initialize<ED extends EntityDict, Cxt extends GeneralRuntimeCont
context: Cxt,
) {
const application = new Application<ED, Cxt, AD & CommonAspectDict<ED, Cxt>>(
aspectWrapper, type, url, basicFeatures.cache, (application) => context.setApplication(application));
const token = new Token<ED, Cxt, AD & CommonAspectDict<ED, Cxt>>(aspectWrapper, basicFeatures.cache, context);
aspectWrapper, type, url, basicFeatures.cache, basicFeatures.localStorage, (application) => context.setApplication(application));
const token = new Token<ED, Cxt, AD & CommonAspectDict<ED, Cxt>>(aspectWrapper, basicFeatures.cache, basicFeatures.localStorage, context);
const extraFile = new ExtraFile<ED, Cxt, AD & CommonAspectDict<ED, Cxt>>(aspectWrapper);
return {
token,

View File

@ -1,5 +1,5 @@
import { EntityDict } from 'general-app-domain';
import { Action, Feature } from 'oak-frontend-base';
import { Action, Feature, LocalStorage } from 'oak-frontend-base';
import { RWLock } from 'oak-domain/lib/utils/concurrent';
import { WebEnv, WechatMpEnv } from 'general-app-domain/Token/Schema';
import { Cache } from 'oak-frontend-base';
@ -15,12 +15,20 @@ export class Token<ED extends EntityDict, Cxt extends GeneralRuntimeContext<ED>,
private rwLock: RWLock;
private cache: Cache<ED, Cxt, AD & CommonAspectDict<ED, Cxt>>;
private context: Cxt;
private storage: LocalStorage<ED, Cxt, AD & CommonAspectDict<ED, Cxt>>;
constructor(aspectWrapper: AspectWrapper<ED, Cxt, AD & CommonAspectDict<ED, Cxt>>, cache: Cache<ED, Cxt, AD & CommonAspectDict<ED, Cxt>>, context: Cxt) {
constructor(aspectWrapper: AspectWrapper<ED, Cxt, AD & CommonAspectDict<ED, Cxt>>,
cache: Cache<ED, Cxt, AD & CommonAspectDict<ED, Cxt>>, storage: LocalStorage<ED, Cxt, AD & CommonAspectDict<ED, Cxt>>, context: Cxt) {
super(aspectWrapper);
this.rwLock = new RWLock();
this.cache = cache;
this.context = context;
this.storage = storage;
const token = storage.load('token:token');
if (token) {
this.token = token;
this.context.setToken(token);
}
}
@Action
@ -31,6 +39,7 @@ export class Token<ED extends EntityDict, Cxt extends GeneralRuntimeContext<ED>,
const { result } = await this.getAspectWrapper().exec('loginByMobile', { password, mobile, captcha, env });
this.token = result;
this.rwLock.release();
this.storage.save('token:token', result);
this.context.setToken(result);
}
catch (err) {
@ -51,8 +60,9 @@ export class Token<ED extends EntityDict, Cxt extends GeneralRuntimeContext<ED>,
env: env as WechatMpEnv,
});
this.token = result;
this.context.setToken(result);
this.rwLock.release();
this.storage.save('token:token', result);
this.context.setToken(result);
}
catch(err) {
this.rwLock.release();
@ -81,6 +91,7 @@ export class Token<ED extends EntityDict, Cxt extends GeneralRuntimeContext<ED>,
async logout() {
this.token = undefined;
this.context.setToken(undefined);
this.storage.remove('token:token');
}
async getToken() {

View File

@ -1,7 +1 @@
import { v1 } from 'uuid';
let iter = 0;
while( iter ++ < 20) {
console.log(v1());
}
console.log(undefined === undefined);