Authored by Zhu-Arthur

优惠券相关

import React, { Component } from 'react';
import {
import ReactNative, {
Dimensions,
Image,
SectionList,
StyleSheet,
Text,
... ... @@ -10,30 +12,70 @@ import {
import CouponListCell from './CouponListCell';
let {width, height} = Dimensions.get('window');
const DEVICE_WIDTH_RATIO = width / 375;
export default class CouponList extends Component {
state = { }
render() {
let data = [{title: '', data: this.props.data || []}];
if (!this.props.isFetching && data[0].data.length == 0) {
return (
<View style={styles.emptyContainer}>
<Image source={require('../../images/coupon.png')} style={styles.emptyImage} />
<Text style={styles.couponErrorPageText}>暂无优惠券</Text>
</View>
)
} else {
return (
<SectionList
style={styles.container}
contentContainerStyle={styles.contentContainerStyle}
renderItem={this.renderItem}
sections={data}
onEndReached={this.props.onEndReached}
keyExtractor={(item, index) => '' + index}
/>
);
}
}
renderItem = ({item, index}) => {
return (
<CouponListCell />
<CouponListCell data={item} type={this.props.type} goCouponProductList={() => this.goCouponProductList(item)}/>
)
}
goCouponProductList(item) {
let url = `http://m.yohobuy.com?openby:yohobuy={"action":"go.couponProductList",
"params":{"coupon_id":"${item.coupon_id}","coupon_code":"${item.coupon_code}"}, "coupon_title":"${item.coupon_name}"}`;
ReactNative.NativeModules.YH_CommonHelper.jumpWithUrl(url);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f0f0f0',
},
emptyContainer: {
flex: 1,
backgroundColor: '#f0f0f0',
justifyContent: 'center',
alignItems: 'center',
},
contentContainerStyle: {
paddingVertical: 5 * DEVICE_WIDTH_RATIO
},
emptyImage: {
width: 104 * DEVICE_WIDTH_RATIO,
height: 65 * DEVICE_WIDTH_RATIO,
// marginTop: 131 * DEVICE_WIDTH_RATIO,
marginBottom: 15 * DEVICE_WIDTH_RATIO,
},
couponErrorPageText: {
fontSize: 14,
color: '#b0b0b0',
}
})
\ No newline at end of file
... ...
import React, { Component } from 'react';
import {
Dimensions,
Image,
SectionList,
StyleSheet,
Text,
... ... @@ -8,18 +10,240 @@ import {
View,
} from 'react-native';
let {width, height} = Dimensions.get('window');
const DEVICE_WIDTH_RATIO = width / 375;
export default class CouponListCell extends Component {
state = { }
render() {
let { type } = this.props;
let data = this.props.data || {};
let coupon_value = data && data.coupon_value_str ? data.coupon_value_str : 0;
let coupon_name = data && data.coupon_name ? data.coupon_name : '';
let coupon_validity = data && data.coupon_validity ? data.coupon_validity : '';
let use_rule = data && data.use_rule ? data.use_rule : '';
let notes = data && data.notes ? data.notes : [];
let { catalog, catalog_name, is_online_avail, is_overdue_soon } = data;
let image, color;
if (catalog == '100') {
image = require('../../images/bgyellow.png');
color = '#ffa72e';
} else if (catalog == '200') {
image = require('../../images/bgred.png');
color = '#fc5960';
} else {
image = require('../../images/bgblack.png');
color = '#000000';
}
let arrowImage = this.state.showDetail ? require('../../images/up.png') : require('../../images/down.png');
let grayImage = type == 'use' ? require('../../images/yishiyong.png') : type == 'overtime' ? require('../../images/guoqi.png') : require('../../images/yishiyong.png');
if (type != 'notuse') {
color = '#b0b0b0';
image = require('../../images/bggrey.png');
}
return (
<View />
<View style={styles.container}>
<View style={[styles.row, this.state.showDetail && styles.shadow]}>
<View>
<Image source={image} resizeMode="stretch" style={styles.bgImg}/>
<View style={styles.leftAb}>
<Text style={[styles.price, {color}, (data.coupon_value != data.coupon_value_str) && {fontSize: 24 * DEVICE_WIDTH_RATIO}]} numberOfLines={1}>
{coupon_value}
</Text>
{use_rule ? <Text style={[styles.priceDetail, {color}]}>
{use_rule}
</Text> : null}
</View>
</View>
<View style={styles.right}>
<Text style={[styles.titleTip, {color}]} numberOfLines={2} >
[{catalog_name}]
<Text style={[styles.title, type != 'notuse' && {color}]} >
{' '} {coupon_name}
</Text>
</Text>
<Text style={styles.time}>
{coupon_validity}
</Text>
<TouchableOpacity
activeOpacity={1.0}
onPress={() => {
this.setState({showDetail: !this.state.showDetail})
}}>
<View style={styles.bottom}>
<Text style={styles.bottomText}>
使用说明
</Text>
<Image source={arrowImage} style={styles.bottomIcon} resizeMode="contain"/>
</View>
</TouchableOpacity>
{(type == 'notuse' && is_online_avail) ? <TouchableOpacity onPress={this.props.goCouponProductList} style={styles.immediatelyUse}>
<Text style={styles.immediatelyUseText}>立即使用</Text>
</TouchableOpacity> : null}
{type != 'notuse' ? <Image source={grayImage} style={styles.grayImage} /> : null}
{is_overdue_soon ? <Image source={require('../../images/tip.png')} style={styles.overDueSoon} resizeMode="stretch" /> : null}
</View>
</View>
{this.state.showDetail ?
<View style={styles.detail}>
{notes.map((item, i) => {
return (
<View key={i} style={styles.detailTextView}>
<Text style={styles.detailText} numberOfLines={2}>
{item}
</Text>
</View>
)
})}
</View>
: null
}
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f0f0f0',
marginVertical: 5 * DEVICE_WIDTH_RATIO,
marginHorizontal: 10 * DEVICE_WIDTH_RATIO,
},
row: {
flexDirection: 'row',
zIndex: 1,
},
bgImg: {
width: 110 * DEVICE_WIDTH_RATIO,
height: 100 * DEVICE_WIDTH_RATIO,
},
right: {
width: 245 * DEVICE_WIDTH_RATIO,
height: 100 * DEVICE_WIDTH_RATIO,
backgroundColor: '#ffffff',
borderTopRightRadius: 4 * DEVICE_WIDTH_RATIO,
borderBottomRightRadius: 4 * DEVICE_WIDTH_RATIO,
},
price: {
fontSize: 30 * DEVICE_WIDTH_RATIO,
color: '#002B47',
letterSpacing: 0,
textAlign: 'center',
fontWeight: 'bold',
},
priceDetail: {
fontFamily: 'PingFang-SC-Regular',
fontSize: 12*DEVICE_WIDTH_RATIO,
color: '#002B47',
letterSpacing: 0,
textAlign: 'center',
},
leftAb: {
position: 'absolute',
top: 0,
left: 8*DEVICE_WIDTH_RATIO,
right: 5*DEVICE_WIDTH_RATIO,
bottom: 0,
justifyContent: 'center',
alignItems: 'center',
},
titleTip: {
fontFamily: 'PingFang-SC-Medium',
fontSize: 12*DEVICE_WIDTH_RATIO,
color: '#002B47',
letterSpacing: 0,
marginLeft: 10*DEVICE_WIDTH_RATIO,
marginTop: 10*DEVICE_WIDTH_RATIO,
height: 35*DEVICE_WIDTH_RATIO,
maxWidth: 185*DEVICE_WIDTH_RATIO,
fontWeight: 'bold'
},
title: {
fontFamily: 'PingFang-SC-Regular',
fontSize: 12*DEVICE_WIDTH_RATIO,
color: '#444444',
fontWeight: 'normal',
letterSpacing: 0,
lineHeight: 15*DEVICE_WIDTH_RATIO,
},
time: {
marginLeft: 10*DEVICE_WIDTH_RATIO,
fontFamily: 'PingFang-SC-Regular',
fontSize: 11*DEVICE_WIDTH_RATIO,
color: '#b0b0b0',
letterSpacing: 0,
},
bottom: {
marginTop: 5*DEVICE_WIDTH_RATIO,
height: 35*DEVICE_WIDTH_RATIO,
flexDirection: 'row',
marginLeft: 10*DEVICE_WIDTH_RATIO,
alignItems: 'center',
},
bottomText: {
fontFamily: 'PingFang-SC-Regular',
fontSize: 11*DEVICE_WIDTH_RATIO,
color: '#B0B0B0',
letterSpacing: 0,
},
bottomIcon: {
width: 10*DEVICE_WIDTH_RATIO,
height: 10*DEVICE_WIDTH_RATIO,
marginLeft: 5*DEVICE_WIDTH_RATIO,
},
immediatelyUse: {
position: 'absolute',
right: 10*DEVICE_WIDTH_RATIO,
bottom: 10*DEVICE_WIDTH_RATIO,
width: 65*DEVICE_WIDTH_RATIO,
height: 25*DEVICE_WIDTH_RATIO,
borderWidth: 1,
borderColor: '#444444',
borderRadius: 12.5*DEVICE_WIDTH_RATIO,
justifyContent: 'center',
alignItems: 'center',
},
immediatelyUseText: {
fontSize: 10*DEVICE_WIDTH_RATIO,
color: '#444444',
},
grayImage: {
position: 'absolute',
top: 21.5*DEVICE_WIDTH_RATIO,
right: 10*DEVICE_WIDTH_RATIO,
width: 63*DEVICE_WIDTH_RATIO,
height: 57*DEVICE_WIDTH_RATIO,
},
detail: {
width: 355*DEVICE_WIDTH_RATIO,
marginTop: -6*DEVICE_WIDTH_RATIO,
backgroundColor: 'rgba(255,255,255,0.7)',
paddingTop: 18*DEVICE_WIDTH_RATIO,
paddingBottom: 11*DEVICE_WIDTH_RATIO,
paddingHorizontal: 11*DEVICE_WIDTH_RATIO,
zIndex: 0,
},
detailTextView: {
flexDirection: 'row',
},
detailText: {
fontFamily: 'PingFang-SC-Regular',
fontSize: 11*DEVICE_WIDTH_RATIO,
color: '#444444',
letterSpacing: 0,
},
shadow: {
shadowColor: 'rgba(0,0,0,0.1)',
shadowOffset: {width: 0, height: 3},
shadowOpacity: 0.5,
shadowRadius: 5,
elevation: 1,
},
overDueSoon: {
position: 'absolute',
top: 0,
right: 0,
width: 42*DEVICE_WIDTH_RATIO,
height: 42*DEVICE_WIDTH_RATIO,
}
})
\ No newline at end of file
... ...
... ... @@ -18,13 +18,15 @@ export default class CouponTabs extends Component {
render() {
const { activeTab } = this.props;
let image = this.props.showFilter ? require('../../images/blackupa.png') : require('../../images/blackdwona.png');
return (
<View style={styles.tabs}>
{this.props.tabs.map((tab, i) => {
return (
<TouchableOpacity key={i} activeOpacity={0.8} style={styles.tab} onPress={() => this.props.goToPage(i)}>
<TouchableOpacity key={i} activeOpacity={0.8} style={styles.tab} onPress={() => this._goToPage(i)}>
<View style={[styles.tabRow, i > 0 && styles.tabBorder]}>
<Text style={[styles.tabText, activeTab == i && styles.activeTabText]}>{tab}{this.renderNums(i)}</Text>
{i == 0 && <Image source={activeTab == i ? image : require('../../images/down.png')} style={styles.image} resizeMode="stretch" />}
</View>
</TouchableOpacity>
)
... ... @@ -58,6 +60,13 @@ export default class CouponTabs extends Component {
return ` (${nums})`;
}
}
_goToPage(i) {
let shouldGo = this.props.selectTab(i);
if (shouldGo) {
this.props.goToPage(i);
}
}
}
const styles = StyleSheet.create({
... ... @@ -91,7 +100,6 @@ const styles = StyleSheet.create({
},
tabText: {
fontSize: 14,
lineHeight: 30,
color: '#b0b0b0',
},
activeTabText: {
... ... @@ -100,5 +108,10 @@ const styles = StyleSheet.create({
icon: {
width: 24,
height: 38,
},
image: {
width: 9,
height: 6,
marginLeft: 5,
}
});
\ No newline at end of file
... ...
... ... @@ -21,5 +21,8 @@ export default keyMirror({
HIDE_SUCCESS_PROMPT: null,
HIDE_NET_ERROR_PROMPT: null,
SHOW_PROMPT_TIP: null,
GET_COUPONLIST_REQUEST: null,
GET_COUPONLIST_FAILURE: null,
});
... ...
... ... @@ -13,6 +13,7 @@ import {Map} from 'immutable';
import * as couponActions from '../reducers/coupon/couponActions';
import CouponList from '../components/coupon/CouponList';
import CouponTabs from '../components/coupon/CouponTabs';
import Prompt from '../components/coupon/Prompt';
const actions = [
couponActions,
... ... @@ -46,13 +47,16 @@ class CouponListContainer extends Component {
props.actions.getCouponList('overtime', true);
}
state = {couponCode: ''}
state = {couponCode: '', selectedTab: 0, showFilter: false, selectedFilter: 0}
render() {
const { couponNums } = this.props.coupon;
const { showFilter, selectedFilter } = this.state;
const { couponNums, notuse, use, overtime, showSuccessTip } = this.props.coupon;
return (
<View style={styles.container}>
<ScrollableTabView
renderTabBar={() => <CouponTabs couponNums={couponNums} />}
onChangeTab={this.onChangeTab}
renderTabBar={() => <CouponTabs couponNums={couponNums} selectTab={this.selectTab} showFilter={this.state.showFilter}/>}
>
<View style={styles.container} tabLabel="未使用">
<View style={styles.bindCouponContainer}>
... ... @@ -63,18 +67,82 @@ class CouponListContainer extends Component {
underlineColorAndroid="transparent"
/>
<TouchableOpacity
onPress={() => null}
onPress={this.bindCoupon}
style={[styles.bindCouponBtn, this.state.couponCode.length > 0 && styles.blackBack]}
>
<Text style={styles.bindCouponText}>兑换</Text>
</TouchableOpacity>
</View>
<CouponList />
<CouponList isFetching={notuse.isFetching} data={notuse.list} onEndReached={() => this.props.actions.getCouponList('notuse')} type="notuse"/>
</View>
<View style={styles.container} tabLabel="已使用">
<CouponList isFetching={use.isFetching} data={use.list} onEndReached={() => this.props.actions.getCouponList('use')} type="use"/>
</View>
<View style={styles.container} tabLabel="已失效">
<CouponList isFetching={overtime.isFetching} data={overtime.list} onEndReached={() => this.props.actions.getCouponList('overtime')} type="overtime"/>
</View>
<View style={{flex: 1, backgroundColor: 'yellow'}} tabLabel="已使用"/>
<View style={{flex: 1, backgroundColor: 'green'}} tabLabel="已失效"/>
</ScrollableTabView>
{showSuccessTip ? <Prompt
text={showSuccessTip}
duration={800}
onPromptHidden={this._onPromptHidden}
/> : null}
{showFilter ? <View style={styles.filterContianer}>
<View style={styles.filterTabs}>
{notuse.filters && notuse.filters.map((item, index) => {
return (
<TouchableOpacity
activeOpacity={0.9}
key={index}
style={[styles.filterTab, (selectedFilter == item.filter_id) && styles.selectedFilterTab]}
onPress={() => this.pressFilter(item.filter_id)}>
<Text style={[styles.filterText, (selectedFilter == item.filter_id) && styles.selectedFilterText]}>{item.filter_name}</Text>
</TouchableOpacity>
)
})}
</View>
<TouchableOpacity style={styles.container} onPress={() => this.setState({showFilter: false})} />
</View> : null}
</View>
)
}
selectTab = i => {
if (i == 0 && this.state.selectedTab == 0) {
this.setState({showFilter: !this.state.showFilter});
return false;
} else {
return true;
}
}
onChangeTab = tab => {
const { i } = tab;
let { showFilter } = this.state;
if (i != 0) {
showFilter = false;
}
this.setState({selectedTab: i, showFilter});
}
bindCoupon = () => {
this.props.actions.bindCoupon(this.state.couponCode);
}
pressFilter(filter_id) {
if (this.state.selectedFilter == filter_id) {
return
}
this.props.actions.getCouponList('notuse', true, filter_id);
this.setState({selectedFilter: filter_id, showFilter: false});
}
_onPromptHidden = () => {
this.props.actions.promptHidden();
}
_onNetPromptHidden() {
this.props.actions.netPromptHidden();
}
}
... ... @@ -112,6 +180,40 @@ const styles = StyleSheet.create({
bindCouponText: {
fontSize: 14,
color: '#ffffff',
},
filterContianer: {
position: 'absolute',
top: 45,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.4)'
},
filterTabs: {
height: 65,
flexDirection: 'row',
justifyContent: 'space-around',
alignItems: 'center',
backgroundColor: '#ffffff'
},
filterTab: {
width: 73,
height: 33,
borderWidth: 1,
borderColor: '#e0e0e0',
borderRadius: 2,
justifyContent: 'center',
alignItems: 'center',
},
selectedFilterTab: {
backgroundColor: '#444444'
},
filterText: {
fontSize: 14,
color: '#444444',
},
selectedFilterText: {
color: '#ffffff',
}
})
... ...
... ... @@ -18,6 +18,9 @@ const {
HIDE_NET_ERROR_PROMPT,
GET_COUPONLIST_SUCCESS,
GET_COUPONNUMS_SUCCESS,
SHOW_PROMPT_TIP,
GET_COUPONLIST_REQUEST,
GET_COUPONLIST_FAILURE,
} = require('../../constants/actionTypes').default;
export function setContentCode(code) {
... ... @@ -377,26 +380,52 @@ export function getCouponListSuccess(payload) {
}
}
export function getCouponList(type, init) {
export function getCouponListRequest(payload) {
return {
type: GET_COUPONLIST_REQUEST,
payload,
}
}
export function getCouponListFailure(payload) {
return {
type: GET_COUPONLIST_FAILURE,
payload,
}
}
export function getCouponList(type, init, filter) {
return (dispatch, getState) => {
let {app, coupon} = getState();
let couponData = (coupon.toJS())[type] || {};
if (!init && (couponData.isFetching || couponData.reachedEnd)) return
dispatch(getCouponListRequest(type))
if (init) {
couponData.page = 1;
} else {
couponData.page += 1;
}
if (filter != null && filter != undefined) {
couponData.filter = filter;
}
return new CouponService(app.host).getCouponList(couponData)
.then(data => {
let length = couponData.list.length;
if (init) {
couponData.list = data.couponList;
} else {
couponData.list = [...couponData.list, ...data.couponList];
}
if (couponData.list.length == length) {
couponData.reachedEnd = true;
couponData.page -= 1;
}
couponData.filters = data.filters;
couponData.isFetching = false;
dispatch(getCouponListSuccess(couponData));
})
.catch(e => {
dispatch();
dispatch(getCouponListFailure(e));
})
}
}
... ... @@ -416,7 +445,28 @@ export function getCouponNums() {
dispatch(getCouponNumsSuccess(data));
})
.catch(e => {
dispatch(getCouponListFailure(e));
})
}
}
export function bindCoupon(coupon_code) {
return (dispatch, getState) => {
let {app} = getState();
return new CouponService(app.host).bindCoupon({coupon_code})
.then(data => {
dispatch(showPrompt(data.message || '兑换成功'));
dispatch(getCouponList('notuse', true));
})
.catch(e => {
dispatch(showPrompt(e.message || '兑换失败'));
})
}
}
export function showPrompt(payload) {
return {
type: SHOW_PROMPT_TIP,
payload,
}
}
\ No newline at end of file
... ...
... ... @@ -10,27 +10,33 @@ let InitialState = Record({
floors: List(),
showSuccessTip: false,
showNetErrorTip: false,
notuse: {
notuse: new (Record({
type: 'notuse',
list: List(),
isFetching: false,
reachedEnd: false,
list: [],
filter: 0,
page: 0,
limit: 10,
},
use: {
})),
use: new (Record({
type: 'use',
list: List(),
isFetching: false,
reachedEnd: false,
list: [],
filter: 0,
page: 0,
limit: 10,
},
overtime: {
})),
overtime: new (Record({
type: 'overtime',
list: List(),
isFetching: false,
reachedEnd: false,
list: [],
filter: 0,
page: 0,
limit: 10,
},
})),
couponNums: null,
});
... ...
... ... @@ -17,6 +17,9 @@ const {
HIDE_NET_ERROR_PROMPT,
GET_COUPONLIST_SUCCESS,
GET_COUPONNUMS_SUCCESS,
SHOW_PROMPT_TIP,
GET_COUPONLIST_REQUEST,
GET_COUPONLIST_FAILURE,
} = require('../../constants/actionTypes').default;
const initialState = new InitialState;
... ... @@ -78,12 +81,22 @@ export default function couponReducer(state=initialState, action) {
case GET_COUPONLIST_SUCCESS: {
let data = action.payload || {};
return state.set(data.type, data);
return state.set(data.type, new (Immutable.Record(data)));
}
case GET_COUPONNUMS_SUCCESS: {
return state.set('couponNums', action.payload);
}
case SHOW_PROMPT_TIP: {
return state.set('showSuccessTip', action.payload);
}
case GET_COUPONLIST_REQUEST: {
return state.setIn([action.payload, 'isFetching'], true);
}
case GET_COUPONLIST_FAILURE: {
return state.set('error', action.payload);
}
}
return state;
... ...
... ... @@ -67,6 +67,7 @@ export default class CouponService {
async getCouponList(data) {
let params = {...data};
delete params.list;
delete params.filters;
return await this.api.get({
url: '',
body: {
... ...