Reinit project
This commit is contained in:
1
src/pages/__tests__/__mocks__/umi-plugin-locale.ts
Normal file
1
src/pages/__tests__/__mocks__/umi-plugin-locale.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const formatMessage = (): string => 'Mock text';
|
||||
21
src/pages/__tests__/index.test.tsx
Normal file
21
src/pages/__tests__/index.test.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import 'jest';
|
||||
import Index from '..';
|
||||
import React from 'react';
|
||||
import renderer, { ReactTestInstance, ReactTestRenderer } from 'react-test-renderer';
|
||||
|
||||
jest.mock('umi-plugin-locale');
|
||||
|
||||
describe('Page: index', () => {
|
||||
it('Render correctly', () => {
|
||||
const wrapper: ReactTestRenderer = renderer.create(<Index />);
|
||||
expect(wrapper.root.children.length).toBe(1);
|
||||
const outerLayer = wrapper.root.children[0] as ReactTestInstance;
|
||||
expect(outerLayer.type).toBe('div');
|
||||
expect(outerLayer.children.length).toBe(2);
|
||||
const getStartLink = outerLayer.findAllByProps({
|
||||
href: 'https://umijs.org/guide/getting-started.html',
|
||||
}) as ReactTestInstance[];
|
||||
expect(getStartLink.length).toBe(1);
|
||||
expect(getStartLink[0].children).toMatchObject(['Mock text']);
|
||||
});
|
||||
});
|
||||
77
src/pages/account/index.tsx
Normal file
77
src/pages/account/index.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Title: 地产详情
|
||||
* Routes:
|
||||
* - ./src/pages/routes
|
||||
*/
|
||||
import React from 'react';
|
||||
import { connect, DispatchProp } from 'dva';
|
||||
import { formatMessage } from 'umi-plugin-locale';
|
||||
import { Button, Modal } from 'antd';
|
||||
import Header from '@/components/Header';
|
||||
import AccountItem from '@/components/AccountItem';
|
||||
import { UserModelState } from '@/models/user';
|
||||
|
||||
import styles from './style.less';
|
||||
|
||||
const AccountPage: React.FC<DispatchProp & UserModelState> = ({
|
||||
dispatch,
|
||||
user,
|
||||
}) => {
|
||||
|
||||
const onLogout = () => {
|
||||
Modal.confirm({
|
||||
title: formatMessage({ id: 'account.confirm' }),
|
||||
onOk: () => dispatch({ type: 'user/logout' }),
|
||||
okText: formatMessage({ id: 'common.ok' }),
|
||||
cancelText: formatMessage({ id: 'common.cancel' }),
|
||||
centered: true,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Header
|
||||
className={styles.header}
|
||||
title={formatMessage({ id: 'account.title' })}
|
||||
/>
|
||||
{user && <div className={styles.content}>
|
||||
<AccountItem
|
||||
className="group"
|
||||
title="account.nickname"
|
||||
message={user.nickname}
|
||||
showGo={false}
|
||||
/>
|
||||
<AccountItem
|
||||
className="group"
|
||||
title="account.phone"
|
||||
message={user.phone}
|
||||
/>
|
||||
<AccountItem
|
||||
title="account.email"
|
||||
message={user.email}
|
||||
/>
|
||||
<AccountItem
|
||||
className="group"
|
||||
title="account.loginPwd"
|
||||
/>
|
||||
<AccountItem
|
||||
title="account.fundPwd"
|
||||
/>
|
||||
<AccountItem
|
||||
title="account.factor"
|
||||
/>
|
||||
<Button
|
||||
className={styles.logout}
|
||||
size="large"
|
||||
onClick={onLogout}
|
||||
>
|
||||
{formatMessage({ id: 'account.logout' })}
|
||||
</Button>
|
||||
</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(
|
||||
({ user }: any) => ({ ...user }),
|
||||
)(AccountPage);
|
||||
30
src/pages/account/style.less
Normal file
30
src/pages/account/style.less
Normal file
@@ -0,0 +1,30 @@
|
||||
@import '../../global.less';
|
||||
|
||||
.container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
background-color: @bg-color;
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
|
||||
:global {
|
||||
.group {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logout {
|
||||
margin: 16px 10px;
|
||||
}
|
||||
47
src/pages/check/check.tsx
Normal file
47
src/pages/check/check.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
import { Button, Input } from 'antd';
|
||||
import { connect, DispatchProp } from 'dva';
|
||||
import { formatMessage } from 'umi-plugin-locale';
|
||||
import { CheckModelState } from './model';
|
||||
import AreaSelection from '@/components/AreaSelection';
|
||||
|
||||
const CheckPage: React.FC<DispatchProp & CheckModelState> = ({
|
||||
areacodes,
|
||||
areacode,
|
||||
phone,
|
||||
submitting,
|
||||
dispatch,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<AreaSelection
|
||||
className="item"
|
||||
areacode={areacode}
|
||||
areacodes={areacodes}
|
||||
onChange={areacode => dispatch({ type: 'check/onUpdateState', areacode })}
|
||||
size="large"
|
||||
/>
|
||||
|
||||
<Input
|
||||
className="item"
|
||||
placeholder={formatMessage({ id: 'common.phoneHolder' })}
|
||||
value={phone}
|
||||
onChange={e => dispatch({ type: 'check/onUpdatePhone', payload: e.target.value })}
|
||||
size="large"
|
||||
type="tel"
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="item gradient"
|
||||
type="primary"
|
||||
loading={submitting}
|
||||
onClick={() => dispatch({ type: 'check/submitCheck' })}
|
||||
size="large"
|
||||
>{formatMessage({ id: 'common.next' })}</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
({ check } : any) => ({ ...check })
|
||||
)(CheckPage) as any;
|
||||
70
src/pages/check/index.tsx
Normal file
70
src/pages/check/index.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Routes:
|
||||
* - ./src/pages/routes
|
||||
*/
|
||||
import React, { useEffect } from 'react';
|
||||
import { Spin } from 'antd';
|
||||
import { connect, DispatchProp } from 'dva';
|
||||
import FittedImage from 'react-fitted-image';
|
||||
import { CheckModelState, PageStep } from './model';
|
||||
import { PageState } from '@/types/common';
|
||||
import Header from '@/components/Header';
|
||||
import bgPic from '@/assets/bg_login.png';
|
||||
import styles from './style.less';
|
||||
import CheckPage from './check';
|
||||
import LoginPage from './login';
|
||||
import RegisterPage from './register';
|
||||
import { formatMessage } from 'umi-plugin-locale';
|
||||
|
||||
const Page: React.FC<DispatchProp & CheckModelState> = ({
|
||||
pageState,
|
||||
pageStep,
|
||||
dispatch,
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
dispatch({ type: 'check/load' });
|
||||
}, []);
|
||||
|
||||
if (pageState === PageState.Pending) {
|
||||
return (
|
||||
<div className={styles.loading}><Spin /></div>
|
||||
);
|
||||
}
|
||||
|
||||
if (pageState === PageState.Failure) {
|
||||
return (
|
||||
<div onClick={() => dispatch({ type: 'check/load' })}>加载失败,点击重新加载</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.check}>
|
||||
{
|
||||
pageStep !== PageStep.Check &&
|
||||
<Header
|
||||
className={styles.header}
|
||||
onReturn={() => dispatch({ type: 'check/back' })}
|
||||
/>
|
||||
}
|
||||
|
||||
<div className="cover">
|
||||
<div className="image">
|
||||
<FittedImage fit="contain" src={bgPic} />
|
||||
</div>
|
||||
<span>{formatMessage({ id: 'app.name' })}</span>
|
||||
</div>
|
||||
|
||||
{{
|
||||
[PageStep.Check]: <CheckPage />,
|
||||
[PageStep.Login]: <LoginPage />,
|
||||
[PageStep.Register]: <RegisterPage />,
|
||||
[PageStep.Reset]: null,
|
||||
[PageStep.ResetSuccess]: null,
|
||||
}[pageStep]}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
({ check } : any) => ({ ...check })
|
||||
)(Page);
|
||||
36
src/pages/check/login.tsx
Normal file
36
src/pages/check/login.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
import { Button, Input } from 'antd';
|
||||
import { connect, DispatchProp } from 'dva';
|
||||
import { formatMessage } from 'umi-plugin-locale';
|
||||
import { CheckModelState } from './model';
|
||||
|
||||
const LoginPage: React.FC<DispatchProp & CheckModelState> = ({
|
||||
password,
|
||||
submitting,
|
||||
dispatch,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<Input
|
||||
className="item"
|
||||
placeholder={formatMessage({ id: 'common.pwdHolder' })}
|
||||
value={password}
|
||||
onChange={e => dispatch({ type: 'check/onUpdatePassword', payload: e.target.value })}
|
||||
size="large"
|
||||
type="password"
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="item gradient"
|
||||
type="primary"
|
||||
loading={submitting}
|
||||
onClick={() => dispatch({ type: 'check/submitLogin' })}
|
||||
size="large"
|
||||
>{formatMessage({ id: 'check.login' })}</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
({ check } : any) => ({ ...check })
|
||||
)(LoginPage) as any;
|
||||
164
src/pages/check/model.ts
Normal file
164
src/pages/check/model.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import { Model } from "dva";
|
||||
import _ from 'lodash';
|
||||
import { message } from 'antd';
|
||||
import { delay } from 'dva/saga';
|
||||
import { PageState, AreaCode, AreaCodes } from '@/types/common';
|
||||
import { getAreaCodes, getUserInfo } from '@/services/common';
|
||||
import { validPhoneInput, validPwdInput, validPhone, validPwd } from '@/utils/validate';
|
||||
import { checkExistence, CheckResult, signIn } from './service';
|
||||
import { formatMessage } from 'umi-plugin-locale';
|
||||
|
||||
export enum PageStep {
|
||||
Check = 'check',
|
||||
Login = 'login',
|
||||
Register = 'register',
|
||||
Reset = 'reset',
|
||||
ResetSuccess = 'resetSuccess',
|
||||
}
|
||||
|
||||
export interface CheckModelState {
|
||||
pageState: PageState,
|
||||
pageStep: PageStep,
|
||||
areacodes: AreaCodes,
|
||||
areacode: AreaCode | undefined, // 选中的地区
|
||||
phone: string;
|
||||
password: string;
|
||||
submitting: boolean;
|
||||
}
|
||||
|
||||
const model: Model = {
|
||||
namespace: 'check',
|
||||
|
||||
state: {
|
||||
pageState: PageState.Pending,
|
||||
pageStep: PageStep.Check,
|
||||
areacodes: [],
|
||||
areacode: undefined,
|
||||
phone: '',
|
||||
password: '',
|
||||
submitting: false,
|
||||
} as CheckModelState,
|
||||
|
||||
effects: {
|
||||
*back(_, { select, put }) {
|
||||
const { pageStep: prev } = yield select((state: any) => state.check);
|
||||
let pageStep = PageStep.Check;
|
||||
switch (prev) {
|
||||
case PageStep.Login:
|
||||
case PageStep.Register:
|
||||
case PageStep.ResetSuccess:
|
||||
pageStep = PageStep.Check;
|
||||
break;
|
||||
case PageStep.Reset:
|
||||
pageStep = PageStep.Login;
|
||||
break;
|
||||
}
|
||||
yield put({ type: 'onUpdateState', pageStep });
|
||||
},
|
||||
|
||||
*load(_, { call, put, all }) {
|
||||
yield put({ type: 'onUpdatePageState', payload: PageState.Pending });
|
||||
yield delay(800);
|
||||
|
||||
try {
|
||||
const areacodes = yield call(getAreaCodes);
|
||||
yield all([
|
||||
put({ type: 'onUpdatePageState', payload: PageState.Success }),
|
||||
put({ type: 'onUpdateAreaCodes', payload: areacodes }),
|
||||
]);
|
||||
} catch(e) {
|
||||
yield put({ type: 'onUpdatePageState', payload: PageState.Failure });
|
||||
}
|
||||
},
|
||||
|
||||
*submitCheck(_, { put, call, select }) {
|
||||
const { areacode, phone } = yield select((state: any) => state.check);
|
||||
if (!areacode) return message.warn(formatMessage({ id: 'check.msgArea' }));
|
||||
if (!validPhone(phone)) return message.warn(formatMessage({ id: 'check.msgPhone' }));
|
||||
|
||||
yield put({ type: 'onUpdateState', submitting: true });
|
||||
try {
|
||||
const checkResult: CheckResult = yield call(checkExistence, areacode, phone);
|
||||
yield put({
|
||||
type: 'onUpdateState',
|
||||
pageStep: {
|
||||
[CheckResult.Login]: PageStep.Login,
|
||||
[CheckResult.Register]: PageStep.Register,
|
||||
}[checkResult],
|
||||
});
|
||||
} catch(e) {
|
||||
message.error(e.message);
|
||||
} finally {
|
||||
yield put({ type: 'onUpdateState', submitting: false });
|
||||
}
|
||||
},
|
||||
|
||||
*submitLogin(_, { put, call, select }) {
|
||||
const { areacode, phone, password } = yield select((state: any) => state.check);
|
||||
if (!areacode) return message.warn(formatMessage({ id: 'check.msgArea' }));
|
||||
if (!validPhone(phone)) return message.warn(formatMessage({ id: 'check.msgPhone' }));
|
||||
if (!validPwd(password)) return message.warn(formatMessage({ id: 'check.msgPwd' }))
|
||||
|
||||
yield put({ type: 'onUpdateState', submitting: true });
|
||||
try {
|
||||
const token = yield call(signIn, areacode, phone, password);
|
||||
yield put({ type: 'user/updateToken', payload: token });
|
||||
const user = yield call(getUserInfo, true);
|
||||
yield put({ type: 'user/updateUser', payload: user });
|
||||
} catch(e) {
|
||||
message.error(e.message);
|
||||
} finally {
|
||||
yield put({ type: 'onUpdateState', submitting: false });
|
||||
}
|
||||
},
|
||||
|
||||
*submitRegister(_, { put }) {
|
||||
yield put({ type: 'onUpdateState', submitting: true });
|
||||
yield delay(800);
|
||||
yield put({ type: 'onUpdateState', submitting: false });
|
||||
yield put({ type: 'onUpdateState', pageStep: PageStep.Check });
|
||||
},
|
||||
},
|
||||
|
||||
reducers: {
|
||||
onUpdatePageState(state, { payload: pageState } : any) {
|
||||
return {
|
||||
...state,
|
||||
pageState,
|
||||
};
|
||||
},
|
||||
|
||||
onUpdateAreaCodes(state, { payload: areacodes } : any) {
|
||||
return {
|
||||
...state,
|
||||
areacodes,
|
||||
areacode: areacodes[0],
|
||||
};
|
||||
},
|
||||
|
||||
onUpdatePhone(state, { payload } : any) {
|
||||
const phone = validPhoneInput(payload) ? payload : state.phone;
|
||||
return {
|
||||
...state,
|
||||
phone,
|
||||
};
|
||||
},
|
||||
|
||||
onUpdatePassword(state, { payload } : any) {
|
||||
const password = validPwdInput(payload) ? payload : state.password;
|
||||
return {
|
||||
...state,
|
||||
password,
|
||||
};
|
||||
},
|
||||
|
||||
onUpdateState(state, newState) {
|
||||
return {
|
||||
...state,
|
||||
...newState,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default model;
|
||||
36
src/pages/check/register.tsx
Normal file
36
src/pages/check/register.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
import { Button, Input } from 'antd';
|
||||
import { connect, DispatchProp } from 'dva';
|
||||
import { formatMessage } from 'umi-plugin-locale';
|
||||
import { CheckModelState } from './model';
|
||||
|
||||
const RegisterPage: React.FC<DispatchProp & CheckModelState> = ({
|
||||
password,
|
||||
submitting,
|
||||
dispatch,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<Input
|
||||
className="item"
|
||||
placeholder={formatMessage({ id: 'common.pwdHolder' })}
|
||||
value={password}
|
||||
onChange={e => dispatch({ type: 'check/onUpdatePassword', payload: e.target.value })}
|
||||
size="large"
|
||||
type="password"
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="item gradient"
|
||||
type="primary"
|
||||
loading={submitting}
|
||||
onClick={() => dispatch({ type: 'check/submitRegister' })}
|
||||
size="large"
|
||||
>{formatMessage({ id: 'check.register' })}</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
({ check } : any) => ({ ...check })
|
||||
)(RegisterPage) as any;
|
||||
30
src/pages/check/service.ts
Normal file
30
src/pages/check/service.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import http from '@/utils/http';
|
||||
import { AreaCode } from '@/types/common';
|
||||
import { encodePhone } from '@/utils/transform';
|
||||
|
||||
export enum CheckResult {
|
||||
Login = 0,
|
||||
Register = 1,
|
||||
}
|
||||
|
||||
export async function checkExistence(areacode: AreaCode, phone: string): Promise<CheckResult> {
|
||||
const { code, msg } = await http.get('/sign/check', { phone: encodePhone(areacode, phone) });
|
||||
if (code !== CheckResult.Login && code !== CheckResult.Register) {
|
||||
throw Error(msg);
|
||||
}
|
||||
return code as CheckResult;
|
||||
}
|
||||
|
||||
export async function signIn(areacode: AreaCode, phone: string, password: string): Promise<string> {
|
||||
const { ok, msg, data } = await http.post(
|
||||
'/sign/in',
|
||||
{
|
||||
phone: encodePhone(areacode, phone),
|
||||
password,
|
||||
},
|
||||
);
|
||||
if (!ok || !data || !data.token) {
|
||||
throw Error(msg);
|
||||
}
|
||||
return data.token;
|
||||
}
|
||||
49
src/pages/check/style.less
Normal file
49
src/pages/check/style.less
Normal file
@@ -0,0 +1,49 @@
|
||||
|
||||
.check {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
|
||||
:global {
|
||||
.item {
|
||||
margin: 10px 5% 0;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.ant-btn {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.cover {
|
||||
height: 40vh;
|
||||
transition: height .2s;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: height .2s;
|
||||
}
|
||||
|
||||
.image {
|
||||
width: 30%;
|
||||
margin-bottom: 20px;
|
||||
// width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
background-color: #fff0;
|
||||
}
|
||||
|
||||
.loading {
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
line-height: 30;
|
||||
line-height: 80vh;
|
||||
}
|
||||
150
src/pages/estate/index.tsx
Normal file
150
src/pages/estate/index.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* Title: 地产详情
|
||||
* Routes:
|
||||
* - ./src/pages/routes
|
||||
*/
|
||||
import React, { useEffect } from 'react';
|
||||
import { connect, DispatchProp } from 'dva';
|
||||
import { Spin, Tabs } from 'antd';
|
||||
import { formatMessage } from 'umi-plugin-locale';
|
||||
import FittedImage from 'react-fitted-image';
|
||||
import { EstateModelState, PageTab } from './model';
|
||||
import { ExtModelState } from '@/models/ext';
|
||||
import { PageState, EstateType } from '@/types/common';
|
||||
import Header from '@/components/Header';
|
||||
import Info, { InfoItem } from '@/components/Info';
|
||||
import Failure from '@/components/Failure';
|
||||
|
||||
import styles from './style.less';
|
||||
|
||||
const EstatePage: React.FC<DispatchProp & EstateModelState & ExtModelState> = ({
|
||||
dispatch,
|
||||
tab,
|
||||
estate,
|
||||
estateExt,
|
||||
pageState,
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
dispatch({ type: 'estate/load', payload: estateExt });
|
||||
return () => {
|
||||
dispatch({ type: 'estate/clear' });
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!estateExt) return null;
|
||||
|
||||
let body = null;
|
||||
|
||||
if (pageState === PageState.Failure) {
|
||||
body = (
|
||||
<div className={styles.loading}>
|
||||
<Failure />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (pageState === PageState.Pending) {
|
||||
body = (
|
||||
<div className={styles.loading}><Spin /></div>
|
||||
);
|
||||
}
|
||||
|
||||
if (pageState === PageState.Success && estate) {
|
||||
const image = (
|
||||
<FittedImage
|
||||
className="image"
|
||||
src={estate.image || ''}
|
||||
fit="cover"
|
||||
/>
|
||||
);
|
||||
const record = (
|
||||
<div>
|
||||
<Info title={formatMessage({ id: 'estate.title1' })}>
|
||||
<InfoItem
|
||||
title={formatMessage({ id: 'estate.inPrice' })}
|
||||
info={`${estate.inPrice} LOG`}
|
||||
type="primary"
|
||||
/>
|
||||
<InfoItem
|
||||
title={formatMessage({ id: 'estate.expectProfit' })}
|
||||
info={estate.profitShow}
|
||||
type="primary"
|
||||
/>
|
||||
<InfoItem
|
||||
title={formatMessage({ id: 'estate.inTime' })}
|
||||
info={estate.inTime}
|
||||
type="primary"
|
||||
/>
|
||||
<InfoItem
|
||||
title={formatMessage({ id: 'estate.lockTime' })}
|
||||
info={estate.lockTime}
|
||||
type="primary"
|
||||
/>
|
||||
</Info>
|
||||
{estate.type === EstateType.Sold &&
|
||||
<Info title={formatMessage({ id: 'estate.title2' })}>
|
||||
<InfoItem
|
||||
title={formatMessage({ id: 'estate.outPrice' })}
|
||||
info={`${estate.outPrice} LOG`}
|
||||
type="secondary"
|
||||
/>
|
||||
<InfoItem
|
||||
title={formatMessage({ id: 'estate.taxfee' })}
|
||||
info={`${estate.taxfeeAmount} LOG`}
|
||||
type="secondary"
|
||||
/>
|
||||
<InfoItem
|
||||
title={formatMessage({ id: 'estate.actualProfit' })}
|
||||
info={`${estate.profitPrice} LOG`}
|
||||
type="secondary"
|
||||
/>
|
||||
<InfoItem
|
||||
title={formatMessage({ id: 'estate.outTime' })}
|
||||
info={estate.outTime}
|
||||
type="secondary"
|
||||
/>
|
||||
</Info>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
const detail = (
|
||||
<div className="detail">{estate.desc}</div>
|
||||
);
|
||||
body = (
|
||||
<div className={styles.content}>
|
||||
<div className="banner">
|
||||
{image}
|
||||
</div>
|
||||
<div className="banner overlay">
|
||||
{image}
|
||||
</div>
|
||||
<Tabs
|
||||
className="tab-indicator"
|
||||
size="small"
|
||||
activeKey={tab}
|
||||
onChange={value => dispatch({ type: 'estate/onUpdateState', payload: { tab: value } })}
|
||||
>
|
||||
<Tabs.TabPane key={PageTab.Record} tab={formatMessage({ id: 'estate.tab1' })} />
|
||||
<Tabs.TabPane key={PageTab.Detail} tab={formatMessage({ id: 'estate.tab2' })} />
|
||||
</Tabs>
|
||||
<div className="tab-content">
|
||||
{tab === PageTab.Record ? record : detail}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.estate}>
|
||||
<Header
|
||||
className={styles.header}
|
||||
title={estateExt.name}
|
||||
/>
|
||||
<div className={styles.body}>{body}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(
|
||||
({ estate, ext }: any) => ({ ...estate, ...ext }),
|
||||
)(EstatePage);
|
||||
83
src/pages/estate/model.ts
Normal file
83
src/pages/estate/model.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { Model, routerRedux } from "dva";
|
||||
import { message } from 'antd';
|
||||
import { Estate, PageState } from '@/types/common';
|
||||
import { getEstateDetail } from './service';
|
||||
import { isDev } from '@/utils/utils';
|
||||
|
||||
export enum PageTab {
|
||||
Record = '0',
|
||||
Detail = '1',
|
||||
}
|
||||
|
||||
export interface EstateModelState {
|
||||
tab: PageTab.Record;
|
||||
estate?: Estate;
|
||||
pageState: PageState;
|
||||
}
|
||||
|
||||
const initState: EstateModelState = {
|
||||
tab: PageTab.Record,
|
||||
estate: undefined,
|
||||
pageState: PageState.Pending,
|
||||
};
|
||||
|
||||
const model: Model = {
|
||||
namespace: 'estate',
|
||||
|
||||
state: initState,
|
||||
|
||||
effects: {
|
||||
*changeTab({ payload: tab }, { put }) {
|
||||
yield put({ type: 'onUpdateState', payload: { tab } });
|
||||
},
|
||||
|
||||
*load({ payload }, { put, call }) {
|
||||
if (!payload) {
|
||||
yield put(routerRedux.goBack());
|
||||
return;
|
||||
}
|
||||
|
||||
yield put({
|
||||
type: 'onUpdateState',
|
||||
payload: { pageState: PageState.Pending },
|
||||
});
|
||||
try {
|
||||
const estate = yield call(getEstateDetail, payload.id);
|
||||
if (payload.id === estate.id || isDev) { // TODO: Remember to remove this
|
||||
yield put({
|
||||
type: 'onUpdateState',
|
||||
payload: {
|
||||
pageState: PageState.Success,
|
||||
estate,
|
||||
tab: PageTab.Record,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
message.error(e.message);
|
||||
yield put({
|
||||
type: 'onUpdateState',
|
||||
payload: { pageState: PageState.Failure },
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
*clear(_, { put }) {
|
||||
yield put({ type: 'onUpdateState', payload: initState });
|
||||
if (!isDev) {
|
||||
yield put({ type: 'ext/onUpdateState', payload: { estateExt: undefined } });
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
reducers: {
|
||||
onUpdateState(state, { payload }: any) {
|
||||
return {
|
||||
...state,
|
||||
...payload,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default model;
|
||||
18
src/pages/estate/service.ts
Normal file
18
src/pages/estate/service.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import http from '@/utils/http';
|
||||
import { Estate, EstateType } from '@/types/common';
|
||||
import { calcEstate } from '@/utils/transform';
|
||||
|
||||
export async function getEstateDetail(id: string) {
|
||||
const {
|
||||
ok,
|
||||
data,
|
||||
msg,
|
||||
} = await http.get<Estate>('/estateorder/detail', { id });
|
||||
if (ok && data) {
|
||||
data.type = data.outTime ? EstateType.Sold : EstateType.Hold;
|
||||
calcEstate(data);
|
||||
return data;
|
||||
}
|
||||
|
||||
throw Error(msg);
|
||||
}
|
||||
87
src/pages/estate/style.less
Normal file
87
src/pages/estate/style.less
Normal file
@@ -0,0 +1,87 @@
|
||||
|
||||
@import '../../global.less';
|
||||
|
||||
.estate {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
background-color: rgba(255, 255, 255, .7);
|
||||
}
|
||||
|
||||
.body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.loading {
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
line-height: 30;
|
||||
line-height: 80vh;
|
||||
}
|
||||
|
||||
.content {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
|
||||
:global {
|
||||
@banner-height: 200px;
|
||||
|
||||
.banner {
|
||||
flex-shrink: 0;
|
||||
height: @banner-height;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
position: absolute;
|
||||
|
||||
.image {
|
||||
height: @banner-height;
|
||||
}
|
||||
}
|
||||
|
||||
.overlay {
|
||||
height: @header-height;
|
||||
overflow: hidden;
|
||||
filter: blur(6px);
|
||||
}
|
||||
|
||||
.tab-indicator {
|
||||
margin-top: @banner-height;
|
||||
flex-shrink: 0;
|
||||
.ant-tabs-bar {
|
||||
margin: 0;
|
||||
}
|
||||
.ant-tabs-nav-scroll {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-tab-active, .ant-tabs-tab:active {
|
||||
color: @secondary-color;
|
||||
}
|
||||
.ant-tabs-ink-bar {
|
||||
background-color: @secondary-color;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
padding: 10px 0 20px;
|
||||
line-height: 1.6;
|
||||
|
||||
:global {
|
||||
.detail {
|
||||
margin: 0 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
40
src/pages/home/_layout.tsx
Normal file
40
src/pages/home/_layout.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Routes:
|
||||
* - ./src/pages/routes
|
||||
*/
|
||||
import React from 'react';
|
||||
import { connect, DispatchProp } from 'dva';
|
||||
import { RouterTypes, Redirect } from 'umi';
|
||||
import Tab from '@/components/Tab';
|
||||
|
||||
import styles from './style.less';
|
||||
import { HomeModelState } from './model';
|
||||
|
||||
const HomePage: React.FC<RouterTypes & DispatchProp & HomeModelState> = ({
|
||||
dispatch,
|
||||
tabItems,
|
||||
selectedTab,
|
||||
location: { pathname },
|
||||
children,
|
||||
}) => {
|
||||
if (pathname === '/home') return <Redirect to="/home/home" />
|
||||
|
||||
return (
|
||||
<div className={styles.home}>
|
||||
<section className={styles.page}>
|
||||
{children}
|
||||
</section>
|
||||
<footer className={styles.tab}>
|
||||
<Tab
|
||||
items={tabItems}
|
||||
selectedKey={selectedTab}
|
||||
onChange={payload => dispatch({ type: 'home/changeTab', payload })}
|
||||
/>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
({ home }: any) => ({ ...home }),
|
||||
)(HomePage);
|
||||
103
src/pages/home/estate/index.tsx
Normal file
103
src/pages/home/estate/index.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Radio } from 'antd';
|
||||
import FittedImage from 'react-fitted-image';
|
||||
import { connect, DispatchProp } from 'dva';
|
||||
import { formatMessage } from 'umi-plugin-locale';
|
||||
import List from '@/components/List';
|
||||
import Label from '@/components/Label';
|
||||
import { HomeEstateModelState } from './model';
|
||||
import { EstateType, Estate } from '@/types/common';
|
||||
import { dispatch } from '@/utils/utils';
|
||||
|
||||
import styles from './style.less';
|
||||
|
||||
const RadioGroup = Radio.Group;
|
||||
const RadioButton = Radio.Button;
|
||||
|
||||
const renderItem = (data: Estate) => {
|
||||
const holdTab = data.type === EstateType.Hold;
|
||||
return (
|
||||
<div
|
||||
className={styles.item}
|
||||
onClick={() => dispatch({ type: 'homeEstate/gotoDetail', payload: data })}
|
||||
>
|
||||
<div className="avatar">
|
||||
<FittedImage src={data.image || ''} fit="cover" />
|
||||
</div>
|
||||
<div className="content">
|
||||
<span className="name">{data.name}</span>
|
||||
<span className="desc">
|
||||
{
|
||||
holdTab ?
|
||||
formatMessage({ id: 'home.estate.timeDue' }, { time: data.lockTime }) :
|
||||
formatMessage({ id: 'home.estate.timeSold' }, { time: data.outTime })
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<div className="postfix">
|
||||
<span className={`cost-msg ${holdTab ? 'primary' : ''}`}>
|
||||
{formatMessage({ id: 'home.estate.cost' })} <span>{data.inPrice}</span> LOG
|
||||
</span>
|
||||
{!holdTab && <span className="sold-msg">
|
||||
{formatMessage({ id: 'home.estate.profit' })} {data.profitPrice} LOG
|
||||
</span>}
|
||||
{holdTab && <span className="hold-msg">
|
||||
{formatMessage({ id: 'home.estate.profit2' })}{data.profitShow}
|
||||
</span>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const EstatePage: React.FC<HomeEstateModelState & DispatchProp> = ({
|
||||
dispatch,
|
||||
loading,
|
||||
refreshing,
|
||||
tab,
|
||||
data,
|
||||
num,
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
if (!data.length) {
|
||||
dispatch({ type: 'homeEstate/refresh' });
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
<RadioGroup
|
||||
buttonStyle="solid"
|
||||
value={tab}
|
||||
onChange={e => dispatch({ type: 'homeEstate/changeTab', payload: e.target.value })}
|
||||
>
|
||||
<RadioButton value={EstateType.Hold}>{formatMessage({ id: 'home.estate.hold' })}</RadioButton>
|
||||
<RadioButton value={EstateType.Sold}>{formatMessage({ id: 'home.estate.sold' })}</RadioButton>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
<Label
|
||||
name={formatMessage({
|
||||
id: tab === EstateType.Hold ? 'home.estate.holdTitle' : 'home.estate.soldTitle',
|
||||
}, { num })}
|
||||
className={styles.label}
|
||||
/>
|
||||
|
||||
<List
|
||||
className={styles.list}
|
||||
data={data}
|
||||
renderItem={renderItem}
|
||||
refreshing={refreshing}
|
||||
onRefresh={() => dispatch({ type: 'homeEstate/refresh' })}
|
||||
loading={loading}
|
||||
onLoadMore={() => dispatch({ type: 'homeEstate/loadMore' })}
|
||||
itemKey={data => data.id}
|
||||
itemSize={80}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(
|
||||
({ homeEstate } : any) => ({ ...homeEstate }),
|
||||
)(EstatePage);
|
||||
110
src/pages/home/estate/model.ts
Normal file
110
src/pages/home/estate/model.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { Model, routerRedux } from 'dva';
|
||||
import { message } from 'antd';
|
||||
import { EstateType, Estates } from '@/types/common';
|
||||
import { getEstateList, EstateList } from './service';
|
||||
|
||||
export interface HomeEstateModelState {
|
||||
refreshing: boolean;
|
||||
loading: boolean;
|
||||
loadingFinished: boolean;
|
||||
data: Estates;
|
||||
num: number;
|
||||
pageIndex: number;
|
||||
|
||||
tab: EstateType;
|
||||
}
|
||||
|
||||
const EstateModel: Model = {
|
||||
namespace: 'homeEstate',
|
||||
|
||||
state: {
|
||||
refreshing: false,
|
||||
loading: false,
|
||||
enableLoad: true,
|
||||
loadingFinished: false,
|
||||
data: [],
|
||||
num: 0,
|
||||
pageIndex: 1,
|
||||
pageSize: 10,
|
||||
tab: EstateType.Hold,
|
||||
} as HomeEstateModelState,
|
||||
|
||||
effects: {
|
||||
*refresh(_, { put, call, select }) {
|
||||
const {
|
||||
pageSize,
|
||||
tab,
|
||||
} = yield select((state: any) => state.homeEstate);
|
||||
yield put({ type: 'onUpdateState', payload: { refreshing: true, loading: false } });
|
||||
try {
|
||||
const { total, list }: EstateList = yield call(getEstateList, 1, pageSize, tab);
|
||||
const { tab: newTab } = yield select((state: any) => state.homeEstate);
|
||||
if (tab === newTab) {
|
||||
yield put({
|
||||
type: 'onUpdateState',
|
||||
payload: {
|
||||
refreshing: false,
|
||||
pageIndex: 2,
|
||||
data: list,
|
||||
num: total,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
message.error(e.message);
|
||||
yield put({ type: 'onUpdateState', payload: { refreshing: false, pageIndex: 1 } });
|
||||
}
|
||||
},
|
||||
|
||||
*loadMore(_, { put, call, select }) {
|
||||
const {
|
||||
pageIndex,
|
||||
pageSize,
|
||||
tab,
|
||||
data,
|
||||
} = yield select((state: any) => state.homeEstate);
|
||||
yield put({ type: 'onUpdateState', payload: { loading: true, refreshing: false } });
|
||||
try {
|
||||
const { total, list }: EstateList = yield call(getEstateList, pageIndex, pageSize, tab);
|
||||
const { tab: newTab } = yield select((state: any) => state.homeEstate);
|
||||
if (tab === newTab) {
|
||||
yield put({
|
||||
type: 'onUpdateState',
|
||||
payload: {
|
||||
loading: false,
|
||||
pageIndex: pageIndex + 1,
|
||||
data: [...data, ...list],
|
||||
num: total,
|
||||
enableLoad: list.length === pageSize,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
message.error(e.message);
|
||||
yield put({ type: 'onUpdateState', payload: { loading: false } });
|
||||
}
|
||||
},
|
||||
|
||||
*changeTab({ payload: tab }, { put }) {
|
||||
yield put({ type: 'onUpdateState', payload: { tab, pageIndex: 1, data: [], num: 0 } });
|
||||
yield put({ type: 'refresh' });
|
||||
},
|
||||
|
||||
*gotoDetail({ payload }, { put }) {
|
||||
if (!payload) return;
|
||||
yield put(routerRedux.push('/estate'));
|
||||
yield put({ type: 'ext/onUpdateState', payload: { estateExt: payload } });
|
||||
},
|
||||
},
|
||||
|
||||
reducers: {
|
||||
onUpdateState(state, { payload } : any) {
|
||||
return {
|
||||
...state,
|
||||
...payload,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default EstateModel;
|
||||
22
src/pages/home/estate/service.ts
Normal file
22
src/pages/home/estate/service.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import http from '@/utils/http';
|
||||
import { Estates, EstateType } from '@/types/common';
|
||||
import { calcEstate } from '@/utils/transform';
|
||||
|
||||
export interface EstateList {
|
||||
total: number;
|
||||
list: Estates;
|
||||
}
|
||||
|
||||
export async function getEstateList(pageIndex: number, pageSize: number, type: EstateType) {
|
||||
const {
|
||||
ok,
|
||||
data,
|
||||
msg,
|
||||
} = await http.get<EstateList>('/estateorder/list', { type, pageIndex, pageSize });
|
||||
if (ok && data) {
|
||||
data.list.forEach(it => it.type = type);
|
||||
data.list.forEach(calcEstate);
|
||||
return data;
|
||||
}
|
||||
throw Error(msg);
|
||||
}
|
||||
96
src/pages/home/estate/style.less
Normal file
96
src/pages/home/estate/style.less
Normal file
@@ -0,0 +1,96 @@
|
||||
@import '../../../global.less';
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.header {
|
||||
height: @header-height;
|
||||
line-height: @header-height;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin: 0;
|
||||
padding: 10px;
|
||||
background-color: @bg-color;
|
||||
}
|
||||
|
||||
.list {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.item {
|
||||
height: 100%;
|
||||
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
padding: 0 10px;
|
||||
transition: background-color .2s;
|
||||
|
||||
&:active {
|
||||
background-color: @bg-color;
|
||||
}
|
||||
|
||||
:global {
|
||||
.avatar {
|
||||
flex-shrink: 0;
|
||||
align-self: center;
|
||||
@size: 56px;
|
||||
width: @size;
|
||||
height: @size;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin: 0 10px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
.desc {
|
||||
font-weight: lighter;
|
||||
font-size: 0.8rem;
|
||||
margin-top: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.postfix {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-end;
|
||||
font-size: 0.8rem;
|
||||
font-weight: lighter;
|
||||
|
||||
.cost-msg {
|
||||
margin-bottom: 6px;
|
||||
|
||||
&.primary {
|
||||
color: @primary-color;
|
||||
}
|
||||
|
||||
> span {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.hold-msg {
|
||||
background-color: @bg-primary-color;
|
||||
color: @primary-color;
|
||||
}
|
||||
|
||||
.sold-msg {
|
||||
color: @primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
7
src/pages/home/fund/index.tsx
Normal file
7
src/pages/home/fund/index.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
const FundPage: React.FC = () => {
|
||||
return <div>Fund</div>
|
||||
}
|
||||
|
||||
export default FundPage;
|
||||
36
src/pages/home/home/index.tsx
Normal file
36
src/pages/home/home/index.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
import { formatMessage } from 'umi-plugin-locale';
|
||||
import Label from '@/components/Label';
|
||||
|
||||
import styles from './style.less';
|
||||
|
||||
const AdCard: React.FC = () => (
|
||||
<section className={styles.adcard}>
|
||||
<code>// TODO</code>
|
||||
</section>
|
||||
);
|
||||
|
||||
const HomePage: React.FC = () => {
|
||||
const header = (
|
||||
<div className={styles.header}>
|
||||
<span className="title">{formatMessage({ id: 'home.home.statistic' })}</span>
|
||||
<span className="label">
|
||||
1000
|
||||
<span className="unit">LOG</span>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{header}
|
||||
<div className={styles.body}>
|
||||
<AdCard />
|
||||
|
||||
<Label name={formatMessage({ id: 'home.home.header' })} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default HomePage;
|
||||
17
src/pages/home/home/model.ts
Normal file
17
src/pages/home/home/model.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Model } from 'dva';
|
||||
|
||||
export interface HomeHomeModelState {
|
||||
total: string;
|
||||
}
|
||||
|
||||
const model: Model = {
|
||||
namespace: 'homeHome',
|
||||
|
||||
state: {},
|
||||
|
||||
effects: {},
|
||||
|
||||
reducers: {},
|
||||
};
|
||||
|
||||
export default model;
|
||||
57
src/pages/home/home/style.less
Normal file
57
src/pages/home/home/style.less
Normal file
@@ -0,0 +1,57 @@
|
||||
@import '../../../global.less';
|
||||
|
||||
@headerHeight: 36%;
|
||||
@offset: 10%;
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.header {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: @headerHeight;
|
||||
width: 100%;
|
||||
background: linear-gradient(to right, @primary-color, @secondary-color);
|
||||
color: white;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
:global {
|
||||
.title {
|
||||
font-size: 0.8rem;
|
||||
margin: 14% 0 0;
|
||||
font-weight: lighter;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 2.2rem;
|
||||
font-weight: lighter;
|
||||
}
|
||||
|
||||
.unit {
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
height: 100%;
|
||||
padding: @headerHeight + @offset 14px 0;
|
||||
}
|
||||
|
||||
.adcard {
|
||||
box-shadow: 1px 1px 10px lighten(@secondary-color, 18%);
|
||||
border-radius: 4px;
|
||||
height: 96px;
|
||||
background-color: #fff;
|
||||
}
|
||||
17
src/pages/home/market/index.tsx
Normal file
17
src/pages/home/market/index.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import FittedImage from 'react-fitted-image';
|
||||
import banner from '@/assets/m_banner.jpg';
|
||||
|
||||
import styles from './style.less';
|
||||
|
||||
const MarketPage: React.FC = () => {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.banner}>
|
||||
<FittedImage src={banner} fit="cover" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MarketPage;
|
||||
110
src/pages/home/market/model.ts
Normal file
110
src/pages/home/market/model.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { Model, routerRedux } from 'dva';
|
||||
import { message } from 'antd';
|
||||
import { EstateType, Estates } from '@/types/common';
|
||||
import { getEstateList, EstateList } from './service';
|
||||
|
||||
export interface HomeMarketModelState {
|
||||
refreshing: boolean;
|
||||
loading: boolean;
|
||||
loadingFinished: boolean;
|
||||
data: Estates;
|
||||
num: number;
|
||||
pageIndex: number;
|
||||
|
||||
tab: EstateType;
|
||||
}
|
||||
|
||||
const MarketModel: Model = {
|
||||
namespace: 'homeMarket',
|
||||
|
||||
state: {
|
||||
refreshing: false,
|
||||
loading: false,
|
||||
enableLoad: true,
|
||||
loadingFinished: false,
|
||||
data: [],
|
||||
num: 0,
|
||||
pageIndex: 1,
|
||||
pageSize: 10,
|
||||
tab: EstateType.Hold,
|
||||
} as HomeMarketModelState,
|
||||
|
||||
effects: {
|
||||
*refresh(_, { put, call, select }) {
|
||||
const {
|
||||
pageSize,
|
||||
tab,
|
||||
} = yield select((state: any) => state.homeEstate);
|
||||
yield put({ type: 'onUpdateState', payload: { refreshing: true, loading: false } });
|
||||
try {
|
||||
const { total, list }: EstateList = yield call(getEstateList, 1, pageSize, tab);
|
||||
const { tab: newTab } = yield select((state: any) => state.homeEstate);
|
||||
if (tab === newTab) {
|
||||
yield put({
|
||||
type: 'onUpdateState',
|
||||
payload: {
|
||||
refreshing: false,
|
||||
pageIndex: 2,
|
||||
data: list,
|
||||
num: total,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
message.error(e.message);
|
||||
yield put({ type: 'onUpdateState', payload: { refreshing: false, pageIndex: 1 } });
|
||||
}
|
||||
},
|
||||
|
||||
*loadMore(_, { put, call, select }) {
|
||||
const {
|
||||
pageIndex,
|
||||
pageSize,
|
||||
tab,
|
||||
data,
|
||||
} = yield select((state: any) => state.homeEstate);
|
||||
yield put({ type: 'onUpdateState', payload: { loading: true, refreshing: false } });
|
||||
try {
|
||||
const { total, list }: EstateList = yield call(getEstateList, pageIndex, pageSize, tab);
|
||||
const { tab: newTab } = yield select((state: any) => state.homeEstate);
|
||||
if (tab === newTab) {
|
||||
yield put({
|
||||
type: 'onUpdateState',
|
||||
payload: {
|
||||
loading: false,
|
||||
pageIndex: pageIndex + 1,
|
||||
data: [...data, ...list],
|
||||
num: total,
|
||||
enableLoad: list.length === pageSize,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
message.error(e.message);
|
||||
yield put({ type: 'onUpdateState', payload: { loading: false } });
|
||||
}
|
||||
},
|
||||
|
||||
*changeTab({ payload: tab }, { put }) {
|
||||
yield put({ type: 'onUpdateState', payload: { tab, pageIndex: 1, data: [], num: 0 } });
|
||||
yield put({ type: 'refresh' });
|
||||
},
|
||||
|
||||
*gotoDetail({ payload }, { put }) {
|
||||
if (!payload) return;
|
||||
yield put(routerRedux.push('/estate'));
|
||||
yield put({ type: 'ext/onUpdateState', payload: { estateExt: payload } });
|
||||
},
|
||||
},
|
||||
|
||||
reducers: {
|
||||
onUpdateState(state, { payload } : any) {
|
||||
return {
|
||||
...state,
|
||||
...payload,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default MarketModel;
|
||||
22
src/pages/home/market/service.ts
Normal file
22
src/pages/home/market/service.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import http from '@/utils/http';
|
||||
import { Estates, EstateType } from '@/types/common';
|
||||
import { calcEstate } from '@/utils/transform';
|
||||
|
||||
export interface EstateList {
|
||||
total: number;
|
||||
list: Estates;
|
||||
}
|
||||
|
||||
export async function getEstateList(pageIndex: number, pageSize: number, type: EstateType) {
|
||||
const {
|
||||
ok,
|
||||
data,
|
||||
msg,
|
||||
} = await http.get<EstateList>('/estateorder/list', { type, pageIndex, pageSize });
|
||||
if (ok && data) {
|
||||
data.list.forEach(it => it.type = type);
|
||||
data.list.forEach(calcEstate);
|
||||
return data;
|
||||
}
|
||||
throw Error(msg);
|
||||
}
|
||||
8
src/pages/home/market/style.less
Normal file
8
src/pages/home/market/style.less
Normal file
@@ -0,0 +1,8 @@
|
||||
.container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.banner {
|
||||
width: 100%;
|
||||
height: 240px;
|
||||
}
|
||||
107
src/pages/home/model.ts
Normal file
107
src/pages/home/model.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { Model, routerRedux } from "dva";
|
||||
import _ from 'lodash';
|
||||
import { TabItemModels } from '@/components/Tab';
|
||||
import homeIcon from '@/assets/home.svg';
|
||||
import homeIconS from '@/assets/home_s.svg';
|
||||
import estateIcon from '@/assets/estate.svg';
|
||||
import estateIconS from '@/assets/estate_s.svg';
|
||||
import marketIcon from '@/assets/market.svg';
|
||||
import marketIconS from '@/assets/market_s.svg';
|
||||
import fundIcon from '@/assets/fund.svg';
|
||||
import fundIconS from '@/assets/fund_s.svg';
|
||||
import profileIcon from '@/assets/profile.svg';
|
||||
import profileIconS from '@/assets/profile_s.svg';
|
||||
|
||||
export interface HomeModelState {
|
||||
tabItems: TabItemModels;
|
||||
selectedTab: string;
|
||||
}
|
||||
|
||||
const tabItems: TabItemModels = [
|
||||
{
|
||||
key: '/home/home',
|
||||
name: 'home.tabHome',
|
||||
icon: homeIcon,
|
||||
iconSelected: homeIconS,
|
||||
},
|
||||
{
|
||||
key: '/home/estate',
|
||||
name: 'home.tabEstate',
|
||||
icon: estateIcon,
|
||||
iconSelected: estateIconS,
|
||||
},
|
||||
{
|
||||
key: '/home/market',
|
||||
name: 'home.tabMarket',
|
||||
icon: marketIcon,
|
||||
iconSelected: marketIconS,
|
||||
},
|
||||
{
|
||||
key: '/home/fund',
|
||||
name: 'home.tabFund',
|
||||
icon: fundIcon,
|
||||
iconSelected: fundIconS,
|
||||
},
|
||||
{
|
||||
key: '/home/profile',
|
||||
name: 'home.tabProfile',
|
||||
icon: profileIcon,
|
||||
iconSelected: profileIconS,
|
||||
},
|
||||
];
|
||||
|
||||
const initState: HomeModelState = {
|
||||
tabItems,
|
||||
selectedTab: tabItems[0].key,
|
||||
};
|
||||
|
||||
const model: Model = {
|
||||
namespace: 'home',
|
||||
|
||||
state: initState,
|
||||
|
||||
effects: {
|
||||
*changeTab({ payload: selectedTab }, { put, select, all }) {
|
||||
const {
|
||||
router: { location: { pathname } },
|
||||
home: { selectedTab: oldSelectedTab },
|
||||
} = yield select();
|
||||
|
||||
const tasks = [];
|
||||
if (oldSelectedTab !== selectedTab) {
|
||||
tasks.push(put({ type: 'onUpdateState', payload: { selectedTab } }));
|
||||
}
|
||||
if (pathname !== selectedTab) {
|
||||
tasks.push(put(routerRedux.replace(selectedTab)));
|
||||
}
|
||||
|
||||
if (tasks.length) {
|
||||
yield all(tasks);
|
||||
}
|
||||
},
|
||||
|
||||
*clear(_, { put }) {
|
||||
yield put({ type: 'onUpdateState', payload: initState })
|
||||
},
|
||||
},
|
||||
|
||||
reducers: {
|
||||
onUpdateState(state, { payload }: any) {
|
||||
return {
|
||||
...state,
|
||||
...payload,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
subscriptions: {
|
||||
onRouteChange({ dispatch, history }) {
|
||||
const { pathname } = history.location;
|
||||
if (_.some(tabItems, it => it.key === pathname)) {
|
||||
dispatch({ type: 'changeTab', payload: pathname });
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default model;
|
||||
73
src/pages/home/profile/index.tsx
Normal file
73
src/pages/home/profile/index.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import React from 'react';
|
||||
import FittedImage from 'react-fitted-image';
|
||||
import { connect, DispatchProp, routerRedux } from 'dva';
|
||||
import { UserModelState } from '@/models/user';
|
||||
import SettingItem from '@/components/SettingItem';
|
||||
import logoAccount from '@/assets/p_account.png';
|
||||
import logoSystem from '@/assets/p_system.png';
|
||||
import logoIdentity from '@/assets/p_identity.png';
|
||||
import logoMsg from '@/assets/p_msg.png';
|
||||
import logoAbout from '@/assets/p_about.png';
|
||||
|
||||
import styles from './style.less';
|
||||
|
||||
const ProfilePage: React.FC<UserModelState & DispatchProp> = ({
|
||||
dispatch,
|
||||
user,
|
||||
}) => {
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
const bannerElement = (
|
||||
<div className={styles.banner}>
|
||||
<div className={styles.info}>
|
||||
<div className="avatar">
|
||||
{user.avatar ?
|
||||
<FittedImage src={user.avatar} fit="cover" /> :
|
||||
<span>{user.nickname ? user.nickname[0] : user.phone[0]}</span>
|
||||
}
|
||||
</div>
|
||||
<span className="name">{user.nickname || user.phone}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{bannerElement}
|
||||
<div className={styles.content}>
|
||||
<SettingItem
|
||||
className="group"
|
||||
logo={logoAccount}
|
||||
title="profile.account"
|
||||
onClick={() => dispatch(routerRedux.push('/account'))}
|
||||
/>
|
||||
<SettingItem
|
||||
className="group"
|
||||
logo={logoSystem}
|
||||
title="profile.system"
|
||||
onClick={() => dispatch(routerRedux.push('/system'))}
|
||||
/>
|
||||
<SettingItem
|
||||
className="group"
|
||||
logo={logoIdentity}
|
||||
title="profile.identity"
|
||||
/>
|
||||
<SettingItem
|
||||
className="group"
|
||||
logo={logoMsg}
|
||||
title="profile.msg"
|
||||
/>
|
||||
<SettingItem
|
||||
className="group"
|
||||
logo={logoAbout}
|
||||
title="profile.about"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
({ user }: any) => ({ ...user }),
|
||||
)(ProfilePage);
|
||||
60
src/pages/home/profile/style.less
Normal file
60
src/pages/home/profile/style.less
Normal file
@@ -0,0 +1,60 @@
|
||||
@import '../../../global.less';
|
||||
|
||||
.container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.banner {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.info {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
transform: translateY(-55%);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
:global {
|
||||
.avatar {
|
||||
@size: 68px;
|
||||
width: @size;
|
||||
height: @size;
|
||||
overflow: hidden;
|
||||
color: @primary-color;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: @bg-color;
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
border-radius: 50%;
|
||||
border: 2px solid @primary-color;
|
||||
}
|
||||
|
||||
.name {
|
||||
margin-top: 8px;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
|
||||
:global {
|
||||
.group {
|
||||
margin: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/pages/home/style.less
Normal file
19
src/pages/home/style.less
Normal file
@@ -0,0 +1,19 @@
|
||||
@import '../../global.less';
|
||||
@tab-height: 64px;
|
||||
|
||||
.home {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.page {
|
||||
height: 100%;
|
||||
padding-bottom: @tab-height;
|
||||
}
|
||||
|
||||
.tab {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: @tab-height;
|
||||
}
|
||||
7
src/pages/index.css
Normal file
7
src/pages/index.css
Normal file
@@ -0,0 +1,7 @@
|
||||
|
||||
.normal {
|
||||
font-family: Georgia, sans-serif;
|
||||
text-align: center;
|
||||
|
||||
line-height: 80vh;
|
||||
}
|
||||
13
src/pages/index.tsx
Executable file
13
src/pages/index.tsx
Executable file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Routes:
|
||||
* - ./src/pages/routes
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Redirect } from 'umi'
|
||||
import { UserModelState } from '@/models/user';
|
||||
|
||||
const Page: React.FC<UserModelState> = () => {
|
||||
return <Redirect to="/home" />;
|
||||
}
|
||||
|
||||
export default Page;
|
||||
31
src/pages/routes.tsx
Normal file
31
src/pages/routes.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Redirect, RouterTypes } from 'umi'
|
||||
import { connect } from 'dva';
|
||||
import { UserModelState } from '@/models/user';
|
||||
|
||||
import styles from './index.css';
|
||||
|
||||
const unauthorizedPaths = [
|
||||
'/check',
|
||||
'/register',
|
||||
];
|
||||
|
||||
const Page: React.FC<RouterTypes & UserModelState> = ({
|
||||
isLogin,
|
||||
children,
|
||||
location: { pathname },
|
||||
}) => {
|
||||
if (pathname !== '/' && !isLogin && !unauthorizedPaths.includes(pathname)) {
|
||||
return <Redirect to="/check" />
|
||||
}
|
||||
if (isLogin && unauthorizedPaths.includes(pathname)) {
|
||||
return <Redirect to="/home" />;
|
||||
}
|
||||
return (
|
||||
<>{children}</>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(
|
||||
({ user }: any) => ({ ...user }),
|
||||
)(Page);
|
||||
70
src/pages/system/index.tsx
Normal file
70
src/pages/system/index.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Title: 地产详情
|
||||
* Routes:
|
||||
* - ./src/pages/routes
|
||||
*/
|
||||
import React, { useState } from 'react';
|
||||
import { connect, DispatchProp } from 'dva';
|
||||
import { formatMessage } from 'umi-plugin-locale';
|
||||
import Header from '@/components/Header';
|
||||
import AccountItem from '@/components/AccountItem';
|
||||
import ChoiceModal from '@/components/ChoiceModal';
|
||||
import { UserModelState } from '@/models/user';
|
||||
import { fromLangToText, supportedLangs } from '@/utils/transform';
|
||||
|
||||
import styles from './style.less';
|
||||
|
||||
const langs = supportedLangs.map(it => ({
|
||||
key: it.lang,
|
||||
value: it.text,
|
||||
}));
|
||||
|
||||
const SystemPage: React.FC<DispatchProp & UserModelState> = ({
|
||||
dispatch,
|
||||
lang,
|
||||
}) => {
|
||||
const [langVisible, setLangVisible] = useState(false);
|
||||
|
||||
const onItemSelected = (key: string) => {
|
||||
setLangVisible(false);
|
||||
dispatch({ type: 'user/changeLang', payload: key });
|
||||
};
|
||||
|
||||
const langModal = (
|
||||
<ChoiceModal
|
||||
closable={false}
|
||||
data={langs}
|
||||
selectedKey={lang}
|
||||
onItemSelected={onItemSelected}
|
||||
visible={langVisible}
|
||||
onCancel={() => setLangVisible(false)}
|
||||
title={formatMessage({ id: 'system.chooseLang' })}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Header
|
||||
className={styles.header}
|
||||
title={formatMessage({ id: 'system.title' })}
|
||||
/>
|
||||
<div className={styles.content}>
|
||||
<AccountItem
|
||||
className="group"
|
||||
title="system.language"
|
||||
preview={fromLangToText(lang)}
|
||||
onClick={() => setLangVisible(true)}
|
||||
/>
|
||||
<AccountItem
|
||||
className="group"
|
||||
title="system.currency"
|
||||
/>
|
||||
</div>
|
||||
{langModal}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(
|
||||
({ user }: any) => ({ ...user }),
|
||||
)(SystemPage);
|
||||
30
src/pages/system/style.less
Normal file
30
src/pages/system/style.less
Normal file
@@ -0,0 +1,30 @@
|
||||
@import '../../global.less';
|
||||
|
||||
.container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
background-color: @bg-color;
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
|
||||
:global {
|
||||
.group {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logout {
|
||||
margin: 16px 10px;
|
||||
}
|
||||
Reference in New Issue
Block a user