Reinit project

This commit is contained in:
Limo Saplf
2019-08-31 20:44:15 +08:00
commit b51ae824c7
115 changed files with 20955 additions and 0 deletions

View File

@@ -0,0 +1 @@
export const formatMessage = (): string => 'Mock text';

View 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']);
});
});

View 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);

View 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
View 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
View 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
View 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
View 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;

View 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;

View 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;
}

View 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
View 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
View 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;

View 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);
}

View 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;
}
}
}
}
}

View 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);

View 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);

View 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;

View 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);
}

View 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;
}
}
}
}

View File

@@ -0,0 +1,7 @@
import React from 'react';
const FundPage: React.FC = () => {
return <div>Fund</div>
}
export default FundPage;

View 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;

View 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;

View 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;
}

View 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;

View 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;

View 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);
}

View File

@@ -0,0 +1,8 @@
.container {
height: 100%;
}
.banner {
width: 100%;
height: 240px;
}

107
src/pages/home/model.ts Normal file
View 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;

View 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);

View 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
View 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
View File

@@ -0,0 +1,7 @@
.normal {
font-family: Georgia, sans-serif;
text-align: center;
line-height: 80vh;
}

13
src/pages/index.tsx Executable file
View 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
View 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);

View 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);

View 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;
}