本地购物车

This commit is contained in:
bentz
2019-03-08 21:43:49 +08:00
parent 6e4bdff9be
commit 115483054a
16 changed files with 1105 additions and 804 deletions

1517
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -35,6 +35,7 @@
"axios": "^0.18.0",
"classnames": "^2.2.6",
"copy-to-clipboard": "^3.0.8",
"dot-prop-immutable": "^1.5.0",
"lodash": "^4.17.11",
"memoize-one": "^5.0.0",
"moment": "^2.24.0",

View File

@@ -10,7 +10,7 @@ export function detail (tid) {
export function create (data) {
// return req.post('/trade.create', data)
return req.post('http://pjj.aixue7.com/index.php/api/h5app/wxapp/order', data)
return req.post('http://pjj.aixue7.com/index.php/api/h5app/wxapp/order_new', data)
}
export function confirm (tid) {

View File

@@ -8,6 +8,10 @@ export function logout () {
return req.post('/user.logout')
}
export function refreshToken () {
return req.get('http://pjj.aixue7.com/index.php/api/h5app/wxapp/token/refresh')
}
export function reg (data) {
// return req.post('http://pjj.aixue7.com/index.php/api/weapp/deposit/rechargeruleshttp://api.espier.local/index.php/api/h5app/wxapp/member', data)
return req.post('http://pjj.aixue7.com/index.php/api/h5app/wxapp/member?company_id=1', data)

View File

@@ -25,7 +25,7 @@ class App extends Component {
'pages/item/point-list',
'pages/item/point-detail',
'pages/cart/index',
'pages/cart/espier-index',
'pages/cart/espier-checkout',
'pages/cart/coupon-picker',
'pages/article/index',

View File

@@ -25,7 +25,7 @@ export default class TabBar extends Component {
tabList: [
{ title: '首页', iconType: 'shop', iconPrefixClass: 'sp-icon', url: '/pages/home/index' },
{ title: '分类', iconType: 'menu', iconPrefixClass: 'sp-icon', url: '/pages/category/index' },
{ title: '购物车', iconType: 'cart', text: this.props.cartTotalCount || '', max: '99', iconPrefixClass: 'sp-icon', url: '/pages/cart/index' },
{ title: '购物车', iconType: 'cart', text: this.props.cartTotalCount || '', max: '99', iconPrefixClass: 'sp-icon', url: '/pages/cart/espier-index' },
{ title: '会员', iconType: 'user', iconPrefixClass: 'sp-icon', url: '/pages/member/index' }
]
}

View File

@@ -34,12 +34,10 @@ function wxParse(bindName = 'wxParseData', type='html', data='<div class="color:
var transData = {};//存放转化后的数据
if (type == 'html') {
transData = HtmlToJson.html2json(data, bindName);
console.log(JSON.stringify(transData, ' ', ' '));
} else if (type == 'md' || type == 'markdown') {
var converter = new showdown.Converter();
var html = converter.makeHtml(data);
transData = HtmlToJson.html2json(html, bindName);
console.log(JSON.stringify(transData, ' ', ' '));
}
transData.view = {};
transData.view.imagePadding = 0;

View File

@@ -1,7 +1,7 @@
import Taro, { Component } from '@tarojs/taro'
import { View, Text } from '@tarojs/components'
import { AtForm, AtInput, AtButton } from 'taro-ui'
import { SpToast, SpIconMenu } from '@/components'
import { SpToast } from '@/components'
import S from '@/spx'
import api from '@/api'
@@ -38,6 +38,7 @@ export default class Login extends Component {
...this.state.info,
...value
}
if (!data.username || !/1\d{10}/.test(data.username)) {
return S.toast('请输入正确的手机号')
}

View File

@@ -3,10 +3,9 @@ import { View, Text, Image, ScrollView, Switch } from '@tarojs/components'
import { connect } from '@tarojs/redux'
import { AtButton, AtInput, AtActionSheet, AtActionSheetItem } from 'taro-ui'
import { Loading, Price, SpCell, AddressPicker, SpToast } from '@/components'
import qs from 'qs'
import api from '@/api'
import S from '@/spx'
import { pickBy, navigateTo } from '@/utils'
import { pickBy } from '@/utils'
import { lockScreen } from '@/utils/dom'
import CheckoutItems from './checkout-items'
@@ -16,7 +15,8 @@ import './espier-checkout.scss'
coupon: cart.coupon,
fastbuy: cart.fastbuy
}), (dispatch) => ({
onClearFastbuy: () => dispatch({ type: 'cart/clearFastbuy' })
onClearFastbuy: () => dispatch({ type: 'cart/clearFastbuy' }),
onClearCart: () => dispatch({ type: 'cart/clear' })
}))
export default class CartCheckout extends Component {
constructor (props) {
@@ -79,7 +79,8 @@ export default class CartCheckout extends Component {
receipt_type: 'logistics',
order_type: 'normal',
promotion: 'normal',
member_discount: false
member_discount: false,
pay_type: 'deposit'
}
this.setState({
@@ -223,9 +224,9 @@ export default class CartCheckout extends Component {
return S.notify('请选择地址')
}
const data = await api.trade.create(this.params)
console.log(data)
const url = `/pages/cashier/index`
const { order_id } = await api.trade.create(this.params)
const url = `/pages/cashier/index?order_id=${order_id}`
this.props.onClearCart()
Taro.navigateTo({ url })
}
@@ -420,14 +421,6 @@ export default class CartCheckout extends Component {
onClickBack={this.toggleCheckoutItems.bind(this, false)}
/>
<AtActionSheet
isOpened={showShippingPicker}
onClose={this.toggleState.bind(this, 'showShippingPicker', false)}
>
<AtActionSheetItem onClick={this.handleShippingChange}>顺丰</AtActionSheetItem>
<AtActionSheetItem onClick={this.handleShippingChange}>自提</AtActionSheetItem>
</AtActionSheet>
<View className='toolbar checkout-toolbar'>
<View className='checkout__total'>
<Text className='total-items'>{total.items_count}</Text>件,合计: <Price primary unit='cent' value={total.total_fee} />

View File

@@ -0,0 +1,240 @@
import Taro, { Component } from '@tarojs/taro'
import { View, Text, ScrollView } from '@tarojs/components'
import { connect } from '@tarojs/redux'
import { AtInputNumber, AtButton } from 'taro-ui'
import { GoodsItem, SpCheckbox, SpNote, Loading, Price } from '@/components'
import { log, navigateTo, pickBy } from '@/utils'
// import api from '@/api'
import { withLogin } from '@/hocs'
import './index.scss'
@connect(({ cart }) => ({
list: cart.list
}), (dispatch) => ({
onCartUpdate: (item, num) => dispatch({ type: 'cart/update', payload: { item, num } }),
onCartDel: (item) => dispatch({ type: 'cart/delete', payload: item })
}))
@withLogin()
export default class CartIndex extends Component {
constructor (props) {
super(props)
this.state = {
selection: new Set(),
totalPrice: '0.00',
items: null,
cartMode: 'default'
}
}
componentDidMount () {
}
componentDidShow () {
// this.fetch()
}
async fetch () {
// const { cartlist } = await api.cart.get()
// // const list = resolveCartItems(cartlist)
// // const items = normalizeItems(list)
// const list = []
// const items = []
// this.setState({
// list,
// items
// })
}
get isTotalChecked () {
return this.props.list.length === this.state.selection.size
}
toggleCartMode = () => {
const cartMode = this.state.cartMode !== 'edit' ? 'edit' : 'default'
this.setState({
cartMode
})
}
handleSelectionChange (item_id, checked) {
this.state.selection[checked ? 'add' : 'delete'](item_id)
const selection = new Set(this.state.selection)
this.setState({
selection
})
log.debug(`[cart change] item: ${item_id}, selection: ${selection}`)
}
handleQuantityChange (idx, quantity) {
const item = this.props.list[idx]
this.props.onCartUpdate(item, quantity)
}
handleAllSelect = (checked) => {
const { items, selection } = this.state
if (checked) {
for (let item of items.values()) {
selection.add(item.item_id)
}
} else {
selection.clear()
}
this.setState({
selection
})
}
handleDelect = () => {
const selection = [...this.state.selection]
selection.forEach(item_id => {
this.props.onCartDel({ item_id })
})
}
handleCheckout = () => {
Taro.navigateTo({
url: '/pages/cart/checkout'
})
this.setState({
selection: new Set()
})
}
transformCartList (list) {
return pickBy(list, {
item_id: 'item_id',
title: 'item_name',
desc: 'brief',
curSymbol: 'cur.symbol',
img: ({ pics }) => pics[0],
price: ({ price }) => (+price / 100).toFixed(2),
market_price: ({ market_price }) => (+market_price / 100).toFixed(2),
quantity: 'num'
})
}
navigateTo = navigateTo
render () {
const { list } = this.props
const { selection, totalPrice, cartMode } = this.state
if (!list) {
return <Loading />
}
const totalSelection = selection.size
const totalItems = totalSelection
const isEmpty = !list.length
const cartList = this.transformCartList(list)
return (
<View className='page-cart-index'>
<ScrollView
className='cart-list__wrap'
scrollY
>
{
!isEmpty && (
<View className='cart-list__actions'>
<Text
clasName='btn-cart-mode'
onClick={this.toggleCartMode}
>{cartMode === 'edit' ? '完成' : '编辑'}</Text>
</View>
)
}
<View className='cart-list'>
<View className='cart-group__shop'>
{
cartList.map((item, idx) => {
return (
<GoodsItem
key={item.item_id}
info={item}
renderFooter={
<block>
<AtInputNumber
min={1}
max={999999}
value={item.quantity}
onChange={this.handleQuantityChange.bind(this, idx)}
/>
</block>
}
>
<SpCheckbox
key={item.item_id}
checked={selection.has(item.item_id)}
onChange={this.handleSelectionChange.bind(this, item.item_id)}
/>
</GoodsItem>
)
})
}
</View>
{
!list.length && (
<View>
<SpNote customStyle='margin-bottom: 20px' img='cart_empty.png'>快去给我挑点宝贝吧~</SpNote>
<AtButton
circle
type='primary'
onClick={this.navigateTo.bind(this, '/pages/home/index', true)}
>随便逛逛</AtButton>
</View>
)
}
</View>
</ScrollView>
<View
className={`toolbar cart-toolbar ${isEmpty && 'hidden'}`}
>
<View className='cart-toolbar__hd'>
<SpCheckbox
checked={this.isTotalChecked}
onChange={this.handleAllSelect}
>全选</SpCheckbox>
</View>
{
cartMode !== 'edit'
? <View className='cart-toolbar__bd'>
<Text className='cart-total__hint'>不含运费</Text>
<View className='cart-total'>
<Price
primary
value={totalPrice}
/>
</View>
<AtButton
circle
type='primary'
className='btn-checkout'
disabled={totalItems <= 0}
onClick={this.handleCheckout}
>结算({totalItems})</AtButton>
</View>
: <View className='cart-toolbar__bd'>
<AtButton
circle
type='primary'
className='btn-checkout'
onClick={this.handleDelect}
>删除</AtButton>
</View>
}
</View>
</View>
)
}
}

View File

@@ -16,9 +16,9 @@
.goods-item {
position: relative;
}
.sp-checkbox {
margin-right: 24px;
margin-left: -8px;
.sp-checkbox__wrap {
margin-left: 8px;
margin-right: 8px;
}
.at-input-number {
transform: scale(0.8);

View File

@@ -10,7 +10,7 @@ export default class Index extends Component {
componentWillMount () {
if (process.env.NODE_ENV === 'development') {
Taro.redirectTo({
url: '/pages/item/espier-detail?id=270'
url: '/pages/item/espier-detail?id=268'
})
}
}

View File

@@ -34,7 +34,7 @@ export default class GoodsBuyToolbar extends Component {
</View>
<View
className='goods-buy-toolbar__menu-item'
onClick={this.navigateTo.bind(this, '/pages/cart/index')}
onClick={this.navigateTo.bind(this, '/pages/cart/espier-index')}
>
<AtBadge
value={2}

View File

@@ -14,7 +14,8 @@ import './espier-detail.scss'
@connect(({ cart }) => ({
cart
}), (dispatch) => ({
onFastbuy: (item) => dispatch({ type: 'cart/fastbuy', payload: { item } })
onFastbuy: (item) => dispatch({ type: 'cart/fastbuy', payload: { item } }),
onAddCart: (item) => dispatch({ type: 'cart/add', payload: { item } })
}))
@withBackToTop
export default class Detail extends Component {
@@ -123,15 +124,12 @@ export default class Detail extends Component {
}
if (type === 'cart') {
const data = await api.cart.add(info)
url = `/pages/cart/espier-index`
if (data.cart_id) {
return Taro.navigateTo({
url
})
} else {
S.toast('加入购物车失败,请稍后再试')
}
this.props.onAddCart(info)
return Taro.navigateTo({
url
})
}
if (type === 'fastbuy') {

View File

@@ -3,7 +3,8 @@ import api from '@/api'
import { getCurrentRoute, log } from '@/utils'
const globalData = {}
const TOKEN_IDENTIFIER = 'authToken'
const TOKEN_IDENTIFIER = 'auth_token'
const TOKEN_TIMESTAMP = 'refresh_token_time'
function remove (arr, item) {
const idx = arr.indexOf(item)
@@ -23,8 +24,16 @@ function isAsync (func) {
}
class Spx {
constructor () {
constructor (options = {}) {
this.hooks = []
this.options = {
autoRefreshToken: true,
...options
}
if (this.options.autoRefreshToken) {
this.startRefreshToken()
}
}
getAuthToken () {
@@ -38,6 +47,25 @@ class Spx {
setAuthToken (token) {
this.set(TOKEN_IDENTIFIER, token)
Taro.setStorageSync(TOKEN_IDENTIFIER, token)
Taro.setStorageSync(TOKEN_TIMESTAMP, Date.now() + 55 * 60 * 1000)
}
startRefreshToken () {
if (this._refreshTokenTimer) {
clearTimeout(this._refreshTokenTimer)
}
const checkAndRefresh = async () => {
const expired = Taro.getStorageSync(TOKEN_TIMESTAMP)
if (!expired) return
const delta = expired - Date.now()
if (delta > 0 && delta <= 5 * 60 * 1000) {
const { token } = await api.user.refreshToken()
clearTimeout(this._refreshTokenTimer)
this.setAuthToken(token)
}
}
setInterval(checkAndRefresh, 5 * 60 * 1000)
}
async getUserInfo () {

View File

@@ -1,4 +1,5 @@
import { createReducer } from 'redux-create-reducer'
import dotProp from 'dot-prop-immutable'
const initState = {
list: [],
@@ -7,15 +8,6 @@ const initState = {
}
const cart = createReducer(initState, {
['cart/update'](state, action) {
const { list } = action.payload
return {
...state,
list
}
},
['cart/checkout'](state, action) {
const checkoutItem = action.payload
@@ -24,7 +16,6 @@ const cart = createReducer(initState, {
checkoutItem
}
},
['cart/fastbuy'](state, action) {
const { item , num = 1 } = action.payload
@@ -36,14 +27,56 @@ const cart = createReducer(initState, {
}
}
},
['cart/add'](state, action) {
const { item, num = 1 } = action.payload
const idx = state.list.findIndex(t => item.item_id === t.item_id)
let list
if (idx >= 0) {
list = dotProp.set(state.list, `${idx}`, { ...item, num: state.list[idx].num + num })
} else {
list = [...state.list, { ...item, num }]
}
return {
...state,
list
}
},
['cart/update'](state, action) {
const { item, num } = action.payload
const idx = state.list.findIndex(t => item.item_id === t.item_id)
let list
if (idx >= 0) {
list = dotProp.set(state.list, `${idx}`, { ...item, num })
} else {
list = [...state.list, { ...item, num }]
}
return {
...state,
list
}
},
['cart/delete'](state, action) {
const { item_id } = action.payload
const idx = state.list.findIndex(t => t.item_id === item_id)
return dotProp.delete(state, `list.${idx}`)
},
['cart/clearFastbuy'](state) {
return {
...state,
fastbuy: null
}
},
['cart/clear'](state) {
return {
...state,
...initState
}
},
['cart/changeCoupon'](state, action) {
const coupon = action.payload