Commit 6302e5e2 by 姜雷

添加蓝牙洗衣机功能

parent 61458afd
import { ResponseDataEntity, baseFetch, customerFetch } from './index';
import { number } from 'prop-types';
export type PayConfigParams = {
areaId: number;
......
......@@ -10,6 +10,7 @@ import {
SMPRO_URL,
GX_URL,
APP_HOME_URL,
WSAHING_MACHINE_URL,
} from '../constants/index';
export type ResponseDataEntity<T> = {
......@@ -66,5 +67,6 @@ export const appHomeFetch = createFetch(APP_HOME_URL);
export const showerFetch = createFetch(SHOWER_APP_URL);
export const smaproFetch = createFetch(SMPRO_URL);
export const gxFetch = createFetch(GX_URL);
export const wmFetch = createFetch(WSAHING_MACHINE_URL);
export default createFetch;
import { wmFetch, ResponseDataEntity } from '.';
export enum FetchWasherStatus {
unusing = 0,
using = 1,
}
type FetchDeviceListByStatusParams = {
campusId: number;
customerId: number;
status: FetchWasherStatus;
};
export enum AppointOrderStatus { // 订单状态 0:预约中;1:已使用;2:预约超期
appointing = 0,
using = 1,
outtime = 2,
}
type AppointOrderService = {
duration: number; // 功能程序洗涤时长,单位:分钟
id: number; // 功能服务id
modelServiceConfigId: number;
name: string; // 功能服务名称
price: number; // 功能服务价格
};
export type AppointOrder = {
bluetoothMode: number; // 蓝牙模式 0:无蓝牙;1:纯蓝牙;2:蓝牙+其他
deviceCode: string; // 设备号
deviceName: string; // 设备名称
endDate: string; // 预约失效时间
money: number; // 订单金额
orderCode: string; // 订单编号
position: string; // 设备位置
services: AppointOrderService[];
startDate: string; // 预约开始时间
status: AppointOrderStatus;
};
export type WashingMachineDevice = {
bluetoothMode: number; // 蓝牙模式 0:无蓝牙;1:纯蓝牙;2:蓝牙+其他
deviceCode: string; // 设备号
deviceName: string; // 设备名称
isRunning: boolean; // 是否运行中
position: string; // 位置
};
type FetchDeviceListByStatusRes = {
appointOrder: AppointOrder;
deviceList: WashingMachineDevice[];
};
export const fetchDeviceListByStatus = (
entity: FetchDeviceListByStatusParams,
): Promise<ResponseDataEntity<FetchDeviceListByStatusRes>> =>
wmFetch({
url: '/dcxy/api/washer/devices',
data: entity,
});
export type Position = {
id: string;
name: string;
};
type FetchPositionListParams = {
campusId: number;
customerId: number;
name?: string;
};
type FetchPositionListRes = {
positions: Position[];
usedPositions: Position[];
};
export const fetchPositionList = (
entity: FetchPositionListParams,
): Promise<ResponseDataEntity<FetchPositionListRes>> =>
wmFetch({
url: `/dcxy/api/washer/${entity.campusId}/${entity.customerId}/positions`,
data: entity,
});
type PositionHandleParams = {
customerId: number;
positionId: string;
};
export const addUsedPosition = (
entity: PositionHandleParams,
): Promise<ResponseDataEntity<null>> =>
wmFetch({
url: `/dcxy/api/washer/${entity.customerId}/positions/${entity.positionId}?positionId=${entity.positionId}`,
method: 'POST',
data: entity,
});
export const delUsedPosition = (
entity: PositionHandleParams,
): Promise<ResponseDataEntity<null>> =>
wmFetch({
url: `/dcxy/api/washer/${entity.customerId}/positions/${entity.positionId}?positionId=${entity.positionId}`,
method: 'DELETE',
data: entity,
});
type FetchAppointmentListParams = {
customerId: number;
pageNum?: number;
pageSize?: number;
};
export const fetchAppointmentList = (
entity: FetchAppointmentListParams,
): Promise<ResponseDataEntity<AppointOrder[]>> =>
wmFetch({
url: '/dcxy/api/washer/appointOrders',
data: entity,
});
type WasherPayData = {
partyPayment: number; // 是否为第三方支付类型,默认:1-是
paymentWayId: number; // 支付方式ID
paymentWayName: string; // 支付方式名称
};
type FetchWasherDeviceInfoParams = {
deviceCode: string;
customerId: number;
};
export type FetchWasherDeviceInfoRes = {
bluetoothMode: number; // 蓝牙模式 0:无蓝牙;1:纯蓝牙;2:蓝牙+其他
deviceCode: string; // 设备号
deviceName: string; // 设备名称
enabledAppoint: boolean;
optionalPrograms: AppointOrderService[];
paymentWaysInner: WasherPayData[];
paymentWaysOuter: WasherPayData[];
position: string;
requiredPrograms: AppointOrderService[];
};
export const fetchWasherDeviceInfo = (
entity: FetchWasherDeviceInfoParams,
): Promise<ResponseDataEntity<FetchWasherDeviceInfoRes>> =>
wmFetch({
url: `/dcxy/api/washer/functionPage/${entity.deviceCode}`,
data: entity,
});
type CreateWasherOrderParams = {
customerId: number; // 用户ID
customerName: string; // 用户名称
customerPhone: string; // 用户手机号
deviceCode: string; // 设备编号
modelServiceIds: number[];
orderType: number; // 订单确认类型
payType: number; // 订单支付方式
};
export type CreateWasherOrderRes = {
appointmentTimeout: number; // 超时时间,单位分钟
deviceCode: string; // 设备号
deviceName: string; // 设备名称
money: number; // 总金额,最多2位小数
orderCode: string; // 订单编号
orderType: number; // 订单类型,1即时订单,2预约订单
payTimeout: number; // 支付超时时间,单位秒
paymentWayId: number; // 支付方式ID
position: string; // 设备位置
serviceId: number; // 消费的服务类型ID
services: AppointOrderService[];
};
export const createWasherOrder = (
entity: CreateWasherOrderParams,
): Promise<ResponseDataEntity<CreateWasherOrderRes>> =>
wmFetch({
url: '/dcxy/api/washer/order',
method: 'POST',
data: entity,
});
type PayWasherOrderParams = {
orderCode: string;
};
type PayWasherOrderRes = {
money: number; // 消费金额
orderCode: string; // 订单号
payStr: string; // 支付要素
};
export const payWasherOrder = (
entity: PayWasherOrderParams,
): Promise<ResponseDataEntity<PayWasherOrderRes>> =>
wmFetch({
url: `/dcxy/api/washer/order/${entity.orderCode}/pay`,
method: 'POST',
data: entity,
});
export const payWasherOrderCallback = (entity: PayWasherOrderParams) =>
wmFetch({
url: '/dcxy/api/washer/pay/callback?orderNum=' + entity.orderCode,
method: 'POST',
data: entity,
});
type BeginWasherParams = {
orderCode: string;
deviceCode: string;
};
export const beginWasher = (entity: BeginWasherParams) =>
wmFetch({
url: `/dcxy/api/washer/begin/${entity.deviceCode}?orderCode=${entity.orderCode}`,
method: 'POST',
data: entity,
});
......@@ -8,6 +8,10 @@ page,
.flex {
display: flex;
}
image {
width: 100%;
height: 100%;
}
button {
height: 82px;
line-height: 82px;
......
......@@ -49,6 +49,8 @@ class App extends Component {
'pages/Password/ChangeTelAccount',
'pages/AppLaunch/AppLaunch',
'pages/WashingMachine/WashingMachine',
'pages/WashingMachine/WasherStart',
'pages/WashingMachine/AppointmentPay',
'pages/stringPay/stringPay',
],
window: {
......
.NoDataIcon-EmptyBox {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
font-size: 28px;
color: #fff;
.NoDataIcon-EmptyIcon {
width: 292px;
height: 248px;
margin-bottom: 72px;
}
}
import './NoDataIcon.scss';
import { View, Image } from '@tarojs/components';
import EmptyIcon from '../../images/shower/empty_yuyue@2x.png';
const NoDataIcon = (props: { text: string }) => (
<View className='NoDataIcon-EmptyBox'>
<Image className='NoDataIcon-EmptyIcon' src={EmptyIcon} />
{props.text}
</View>
);
export default NoDataIcon;
......@@ -14,3 +14,6 @@ export const SHOWER_APP_URL = 'http://ex-shower-app-server.168cad.top';
export const SOCKET_URL = 'wss://dev-shower-wss1.168cad.top:9443/ws';
export const SMPRO_URL = 'https://ex-dev-dcxy-smapro-app.168cad.top';
export const GX_URL = 'http://ex-dev-gx-app-server.168cad.top';
export const WSAHING_MACHINE_URL =
'https://ex-test-other-device-app.168cad.top';
export const WM_SOCKET_URL = 'wss://test-other-wss1.168cad.top:9443/ws';
import { useState } from '@tarojs/taro';
const useArrayValue = <T>(initArr: T[]): [T[], (newValue: T) => void] => {
const [arr, setArr] = useState(initArr);
const getValueHandle = (newValue: T) => {
let index = arr.indexOf(newValue);
if (index === -1) {
setArr([...arr, newValue]);
} else {
setArr([...arr.slice(0, index), ...arr.slice(index + 1)]);
}
};
return [arr, getValueHandle];
};
export default useArrayValue;
import { useEffect, useState } from '@tarojs/taro';
const useCountDown = (countDownTime: number) => {
const [count, setCount] = useState(countDownTime);
let timer: NodeJS.Timeout | null = null;
useEffect(() => {
timer && clearInterval(timer);
if (countDownTime) {
setCount(countDownTime);
}
}, [countDownTime]);
useEffect(() => {
timer = setTimeout(() => {
// console.log(countDownTime, count);
let newCount = count - 1;
if (newCount < 0) {
timer && clearInterval(timer);
return;
}
setCount(newCount);
}, 1000);
return () => {
timer && clearInterval(timer);
};
}, [count]);
return count;
};
export default useCountDown;
import Actions from '@/types/Store/Actions';
import Taro, { useReducer, useEffect } from '@tarojs/taro';
import { str2ab, ab2str } from '@/utils/arrayBuffer';
type StoreState = {
socketState: boolean;
socketTask: Taro.SocketTask | null;
};
const StopCode = 1000;
let reConnectting: boolean = false;
let timer: NodeJS.Timeout | null = null;
const initState = {
socketState: false,
socketTask: null,
};
const reducer = (state: StoreState, action: Actions): StoreState => {
switch (action.type) {
case 'GET_SOCKET_TASK':
return { ...state, socketTask: action.payload };
case 'SOCKET_STATE_CHANGE':
return { ...state, socketState: action.payload };
default:
return state;
}
};
const useDeviceWS = ({
url,
getSocketData,
}: {
url: string;
getSocketData: (msg: string) => void;
}) => {
const [state, dispatch] = useReducer(reducer, initState);
const connectDeviceSocket = () => {
console.log('connectDeviceSocket: ', url);
Taro.connectSocket({ url: url }).then(task => {
dispatch({ type: 'GET_SOCKET_TASK', payload: task });
task.onOpen(() => {
console.log('onOpen');
if (reConnectting) {
// reConnectDeviceSocket(true);
timer && clearTimeout(timer);
timer = null;
reConnectting = false;
}
task.send({ data: str2ab('{}') });
dispatch({ type: 'SOCKET_STATE_CHANGE', payload: true });
// this.sendDeviceCode();
});
task.onMessage(res => {
const msg: string = ab2str(res.data);
console.log('socket onMessage: ', msg);
if (msg === '[0]') {
console.log('结束蓝牙以及socket: ', msg);
closeDeviceSocket();
} else if (msg === '[]') {
} else {
if (msg.length > 100) {
getSocketData('[]');
} else if (msg.length > 20) {
for (let index = 0; index <= Math.floor(msg.length / 20); index++) {
let str = msg.substring(index * 20, (index + 1) * 20);
getSocketData(str);
}
} else {
if (msg) {
getSocketData(msg);
}
}
}
});
task.onClose(e => {
console.log('socked关闭', e, reConnectting, timer);
dispatch({ type: 'SOCKET_STATE_CHANGE', payload: false });
dispatch({ type: 'GET_SOCKET_TASK', payload: null });
if (e.code === StopCode) {
console.log('正确结束socket连接');
} else {
console.log('开始重连socket');
reConnectDeviceSocket();
}
});
});
};
const closeDeviceSocket = () => {
const { socketTask } = state;
console.log('in close', socketTask, timer);
if (socketTask) {
socketTask.close({
code: StopCode,
complete: () => {
dispatch({ type: 'SOCKET_STATE_CHANGE', payload: false });
dispatch({ type: 'GET_SOCKET_TASK', payload: null });
},
});
}
if (timer) {
clearTimeout(timer);
}
};
const reConnectDeviceSocket = () => {
console.log(reConnectting, timer);
if (reConnectting) {
connectDeviceSocket();
} else if (timer) {
clearTimeout(timer);
reConnectting = false;
timer = null;
console.log('请保证网络正常');
Taro.showModal({
title: '警告',
content: '请保持网络畅通正常',
});
} else {
timer = setTimeout(() => {
reConnectting = false;
}, 10000);
reConnectting = true;
console.log(reConnectting, timer);
connectDeviceSocket();
}
};
const sendMessageToServer = (
msg: string,
successHandle: Taro.SocketTask.send.ParamPropSuccess,
failHandle: Taro.SocketTask.send.ParamPropFail,
) => {
const { socketTask } = state;
socketTask &&
socketTask.send({
data: str2ab(msg),
success: successHandle,
fail: failHandle,
});
};
// useEffect(() => {
// console.log(deviceCode);
// if (deviceCode) {
// connectDeviceSocket(url);
// }
// return closeDeviceSocket;
// }, [url, deviceCode]);
return { state, sendMessageToServer, connectDeviceSocket };
};
export default useDeviceWS;
......@@ -16,6 +16,7 @@ function useInputValue(initialValue: string) {
return {
value,
onChange,
setValue,
};
}
......
......@@ -21,6 +21,9 @@ import DryerIconBig from '../../images/menu/ic_chuifeng-2@2x.png';
import DispenserIconNormal from '../../images/menu/ic_bigua@2x.png';
import DispenserIconBlock from '../../images/menu/ic_bigua-1@2x.png';
import DispenserIconBig from '../../images/menu/ic_bigua-2@2x.png';
import WasherIconBig from '../../images/menu/ic_xiyi_1@2x.png';
import WasherIconBlock from '../../images/menu/ic_xiyi-1@2x.png';
import WasherIconNormal from '../../images/menu/ic_xiyi@2x.png';
import './Home.scss';
import { connect } from '@tarojs/redux';
......@@ -231,6 +234,12 @@ class Home extends Component {
});
}
goWashingMachine() {
Taro.navigateTo({
url: '/pages/WashingMachine/WashingMachine',
});
}
render() {
const { userinfo, serviceList } = this.props;
const { barMenuVisiable } = this.state;
......@@ -295,6 +304,7 @@ class Home extends Component {
</View>
</View>
<View className='Home-Service'>
{/* <Button onClick={this.goWashingMachine}>洗衣</Button> */}
{serviceList.length && serviceList.length <= 2 ? (
<View className='Home-Service-List big'>
{serviceList.map(service =>
......@@ -331,6 +341,16 @@ class Home extends Component {
side='left'
onClick={this.goDispenser}
/>
) : service.serviceId === 23 ||
service.serviceId === 24 ||
service.serviceId === 25 ? (
<MenuIconBig
key={service.serviceId}
icon={WasherIconBig}
text={service.serviceName}
side='right'
onClick={this.goWashingMachine}
/>
) : null,
)}
</View>
......@@ -365,6 +385,15 @@ class Home extends Component {
text={service.serviceName}
onClick={this.goDispenser}
/>
) : service.serviceId === 23 ||
service.serviceId === 24 ||
service.serviceId === 25 ? (
<MenuIconBlock
key={service.serviceId}
icon={WasherIconBlock}
text={''}
onClick={this.goWashingMachine}
/>
) : null,
)}
</View>
......@@ -399,6 +428,15 @@ class Home extends Component {
text={service.serviceName}
onClick={this.goDispenser}
/>
) : service.serviceId === 23 ||
service.serviceId === 24 ||
service.serviceId === 25 ? (
<MenuIconNormal
key={service.serviceId}
icon={WasherIconNormal}
text={service.serviceName}
onClick={this.goWashingMachine}
/>
) : null,
)}
</View>
......
.AppointmentPay {
height: 100%;
display: flex;
flex-direction: column;
.AppointmentPay-content {
flex: 1;
position: relative;
.AppointmentPay-contentItem {
border-top: 1px solid #707070;
padding: 48px 0;
margin: 0 40px;
&:nth-child(2) {
border: none;
}
.AppointmentPay-contentTitle {
display: flex;
justify-content: space-between;
}
.AppointmentPay-contentValue {
display: flex;
.AppointmentPay-contentValueItem {
margin-right: 40px;
&:last-child {
margin: 0;
}
}
}
.AppointmentPay-Price {
text-align: right;
margin: 40px 0 0;
font-size: 32px;
color: #f93d3d;
}
}
.AppointmentPay-Tips {
margin: 0 40px;
font-size: 24px;
color: #f93d3d;
}
.AppointmentPay-CountDown {
width: 100%;
position: absolute;
bottom: 16px;
text-align: center;
font-size: 24px;
color: #456beb;
}
}
.AppointmentPay-footer {
display: flex;
align-items: center;
justify-content: center;
height: 100px;
font-size: 32px;
color: #333;
box-shadow: 0 -4px 10px #f6f6f6;
&.disabled {
color: #999;
}
}
}
import './AppointmentPay.scss';
import { View, Image } from '@tarojs/components';
import WasherDevice from './components/WasherDevice';
import WxIcon from '../../images/washingmachine/pay_wechat_icon@2x.png';
import Taro, { useEffect, useState } from '@tarojs/taro';
import { useSelector } from '@tarojs/redux';
import { Customer } from '@/types/Customer/Customer';
import useCountDown from '@/hooks/useCountDown';
import { WasherStoreState } from '@/store/rootReducers/washer';
import { payWasherOrder } from '@/api/washingMachine';
import { formatePayString } from '@/utils/pay';
import useWasherStart from './hooks/useWasherStart';
const AppointmentPay = () => {
const orderData = useSelector(
(state: { washer: WasherStoreState }) => state.washer.orderInfo,
);
const userInfo = useSelector(
(state: { userinfo: Customer }) => state.userinfo,
);
const count = useCountDown(orderData.payTimeout);
const [deviceCode, setDeviceCode] = useState('');
const { wsState, bluetoothState } = useWasherStart(deviceCode);
const payStartHandle = () => {
if (orderData.orderType === 1) {
Taro.showLoading({
title: '开启中。。。',
mask: true,
});
setDeviceCode(orderData.deviceCode);
} else {
payHandle();
}
};
const payHandle = () => {
console.log('in payHandle');
Taro.showLoading();
payWasherOrder({
orderCode: orderData.orderCode,
})
.then(res => {
console.log(res.data);
Taro.hideLoading();
const { payStr } = res.data;
if (payStr) {
let payData = formatePayString(payStr, userInfo.customerId);
Taro.requestPayment({
timeStamp: payData.timeStamp.toString(),
nonceStr: payData.nonceStr,
package: payData.package,
signType: payData.signType,
paySign: payData.paySign,
})
.then(res => {
console.log(res);
goBackWasherHome();
})
.catch(err => {
Taro.showToast({
title: err.errMsg || '呼起支付失败',
icon: 'none',
});
});
} else {
goBackWasherHome();
}
})
.catch(err => {
Taro.hideLoading();
console.log(err);
Taro.showToast({
title: err.msg || '请求支付失败',
icon: 'none',
});
});
};
const goBackWasherHome = () => {
Taro.navigateBack({
delta: 2,
});
};
useEffect(() => {
console.log(wsState.socketState, bluetoothState);
if (wsState.socketState && bluetoothState) {
Taro.hideLoading();
payHandle();
}
}, [wsState.socketState, bluetoothState]);
return (
<View className='AppointmentPay'>
<View className='AppointmentPay-content'>
<WasherDevice
data={{
deviceCode: orderData.deviceCode,
postion: orderData.position,
}}
/>
<View className='AppointmentPay-contentItem'>
<View className='AppointmentPay-contentTitle'>
<View className='AppointmentPay-contentName'>洗衣功能</View>
<View className='AppointmentPay-contentValue'>
{orderData.services.map(service => (
<View
key={service.id}
className='AppointmentPay-contentValueItem'>
{service.name}-{service.duration}分钟
</View>
))}
</View>
</View>
<View className='AppointmentPay-Price'>
合计: {orderData.money.toFixed(2)}
</View>
</View>
<View className='AppointmentPay-contentItem'>
<View className='AppointmentPay-contentTitle'>
<View className='AppointmentPay-contentName'>支付方式</View>
<View className='AppointmentPay-contentValue'>
<View className='AppointmentPay-contentPayicon'>
<Image src={WxIcon} />
</View>
<View>微信支付</View>
</View>
</View>
</View>
{orderData.appointmentTimeout && (
<View className='AppointmentPay-Tips'>
注:本次支付为预约设备功能使用费用,预约时间为
{orderData.appointmentTimeout}
分钟,超出预约时间,费用不退还,请仔细阅读。
</View>
)}
<View className='AppointmentPay-CountDown'>
请在{count}秒后完成支付
</View>
</View>
{count ? (
<View className='AppointmentPay-footer' onClick={payStartHandle}>
预约支付
</View>
) : (
<View className='AppointmentPay-footer disabled'>预约支付</View>
)}
</View>
);
};
AppointmentPay.config = {
navigationBarBackgroundColor: '#f9faff',
navigationBarTitleText: '洗衣预约',
};
export default AppointmentPay;
.WasherStart {
height: 100%;
display: flex;
flex-direction: column;
.WasherStart-content {
flex: 1;
overflow-y: auto;
.WasherStart-serviceList {
padding: 40px 42px;
.WasherStart-serviceTitle {
font-size: 32px;
margin-bottom: 20px;
}
.WasherStart-serviceItem {
display: flex;
align-items: center;
height: 80px;
line-height: 80px;
&.checked {
color: #456beb;
.WasherStart-servicePrice {
color: #f93d3d;
}
}
.WasherStart-serviceName {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.WasherStart-servicePrice {
margin-left: 10px;
margin-right: 56px;
}
.WasherStart-serviceIcon {
width: 40px;
height: 40px;
line-height: 0;
}
}
.WasherStart-payItem {
display: flex;
height: 80px;
align-items: center;
.WasherStart-payIcon {
width: 56px;
height: 56px;
margin-right: 30px;
}
.WasherStart-serviceName {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.WasherStart-payBtn {
margin: 0 50px 0 10px;
width: 106px;
height: 52px;
line-height: 52px;
font-size: 26px;
text-align: center;
border-radius: 26px;
background-color: #d4deff;
color: #456ebe;
}
.WasherStart-serviceIcon {
width: 40px;
height: 40px;
}
}
}
}
.WasherStart-footer {
display: flex;
justify-content: center;
align-items: center;
height: 100px;
line-height: 100px;
font-size: 32px;
color: #333;
.WasherStart-footerItem {
flex: 1;
text-align: center;
}
.WasherStart-footerLine {
width: 2px;
height: 30px;
background-color: #d7d7d7;
}
}
}
import './WasherStart.scss';
import { View, Image } from '@tarojs/components';
import CheckIcon from '../../images/washingmachine/pc_nor_icon@2x.png';
import CheckedIcon from '../../images/washingmachine/pc_sel_icon@2x.png';
import AimiIcon from '../../images/order/pay_aimi_icon@2x.png';
import BeanIcon from '../../images/order/pay_chuifengdou_icon@2x.png';
import WxIcon from '../../images/washingmachine/pay_wechat_icon@2x.png';
import WasherDevice from './components/WasherDevice';
import Taro, { useState, useEffect } from '@tarojs/taro';
import useArrayValue from '@/hooks/useArrayValue';
import { useSelector, useDispatch } from '@tarojs/redux';
import { createWasherOrder } from '@/api/washingMachine';
import { Customer } from '@/types/Customer/Customer';
import {
WasherStoreState,
updateWasherOrderInfo,
} from '@/store/rootReducers/washer';
import { ITouchEvent } from '@tarojs/components/types/common';
const WasherStart = () => {
const dispatch = useDispatch();
const userInfo = useSelector(
(state: { userinfo: Customer }) => state.userinfo,
);
const data = useSelector(
(state: { washer: WasherStoreState }) => state.washer.deviceInfo,
);
console.log('WasherStart Props:', data);
const [programId, setProgramId] = useState(0);
const initOptionProgramIds: number[] = [];
const [optionProgramIds, setOptionProgramId] = useArrayValue(
initOptionProgramIds,
);
const [paywayId, setPaywayId] = useState(0);
const [orderType, setOrderType] = useState(0);
const goAppointmentPayHandle = (orderType: number) => {
if (!programId) {
return Taro.showToast({
title: '请选择洗衣功能',
icon: 'none',
});
}
if (!paywayId) {
return Taro.showToast({
title: '请选择支付方式',
icon: 'none',
});
}
setOrderType(orderType);
};
const goAccountPage = (e: ITouchEvent) => {
e.stopPropagation();
Taro.navigateTo({
url: '/pages/Account/Account',
});
};
useEffect(() => {
if (data.deviceCode && orderType) {
Taro.showLoading();
let modelServiceIds = programId.toString();
if (optionProgramIds && optionProgramIds.length) {
modelServiceIds += ',' + optionProgramIds.join(',');
}
let ids = modelServiceIds.split(',').map(id => Number(id));
createWasherOrder({
customerId: userInfo.customerId,
customerName: userInfo.customerName,
customerPhone: userInfo.customerPhone,
deviceCode: data.deviceCode,
modelServiceIds: ids,
orderType: orderType,
payType: paywayId,
})
.then(res => {
Taro.hideLoading();
console.log(res.data);
// setOrderData(res.data);
dispatch(updateWasherOrderInfo(res.data));
Taro.navigateTo({
url: '/pages/WashingMachine/AppointmentPay',
});
})
.catch(err => {
Taro.hideLoading();
console.log(err);
Taro.showToast({
title: err.msg || '创建订单失败!',
icon: 'none',
});
});
}
}, [data.deviceCode, orderType]);
return (
<View className='WasherStart'>
<View className='WasherStart-content'>
<WasherDevice
data={{
deviceCode: data.deviceCode,
postion: data.position,
}}
/>
<View className='WasherStart-serviceList'>
<View className='WasherStart-serviceTitle'>请选择洗衣功能</View>
{data.requiredPrograms &&
data.requiredPrograms.length &&
data.requiredPrograms.map(program => (
<View
key={program.id}
className={`WasherStart-serviceItem ${
programId === program.id ? 'checked' : ''
}`}
onClick={() => setProgramId(program.id)}>
<View className='WasherStart-serviceName'>
{program.name} - {program.duration}分钟
</View>
<View className='WasherStart-servicePrice'>
{program.price.toFixed(2)}
</View>
<View className='WasherStart-serviceIcon'>
{programId === program.id ? (
<Image src={CheckedIcon}></Image>
) : (
<Image src={CheckIcon}></Image>
)}
</View>
</View>
))}
</View>
{data.optionalPrograms && data.optionalPrograms.length && (
<View className='WasherStart-serviceList'>
<View className='WasherStart-serviceTitle'>可选功能</View>
{data.optionalPrograms.map(program => (
<View
key={program.id}
className={`WasherStart-serviceItem ${
optionProgramIds.indexOf(program.id) > -1 ? 'checked' : ''
}`}
onClick={() => setOptionProgramId(program.id)}>
<View className='WasherStart-serviceName'>
{program.name} - {program.duration}分钟
</View>
<View className='WasherStart-servicePrice'>
{program.price.toFixed(2)}
</View>
<View className='WasherStart-serviceIcon'>
{optionProgramIds.indexOf(program.id) > -1 ? (
<Image src={CheckedIcon}></Image>
) : (
<Image src={CheckIcon}></Image>
)}
</View>
</View>
))}
</View>
)}
<View className='WasherStart-serviceList'>
<View className='WasherStart-serviceTitle'>请选择支付方式</View>
{data.paymentWaysInner &&
data.paymentWaysInner.length &&
data.paymentWaysInner.map(payway => (
<View
key={payway.paymentWayId}
className='WasherStart-payItem'
onClick={() => setPaywayId(payway.paymentWayId)}>
<View className='WasherStart-payIcon'>
{payway.paymentWayId === 6 ? (
<Image src={AimiIcon}></Image>
) : (
<Image src={BeanIcon}></Image>
)}
</View>
<View className='WasherStart-serviceName'>
{payway.paymentWayName}
</View>
{payway.paymentWayId === 6 && (
<View className='WasherStart-payBtn' onClick={goAccountPage}>
去购买
</View>
)}
<View className='WasherStart-serviceIcon'>
{paywayId === payway.paymentWayId ? (
<Image src={CheckedIcon}></Image>
) : (
<Image src={CheckIcon}></Image>
)}
</View>
</View>
))}
{data.paymentWaysOuter &&
data.paymentWaysOuter.length &&
data.paymentWaysOuter.map(payway => (
<View
key={payway.paymentWayId}
className='WasherStart-payItem'
onClick={() => setPaywayId(payway.paymentWayId)}>
<View className='WasherStart-payIcon'>
<Image src={WxIcon}></Image>
</View>
<View className='WasherStart-serviceName'>
{payway.paymentWayName}
</View>
<View className='WasherStart-serviceIcon'>
{paywayId === payway.paymentWayId ? (
<Image src={CheckedIcon}></Image>
) : (
<Image src={CheckIcon}></Image>
)}
</View>
</View>
))}
</View>
</View>
<View className='WasherStart-footer'>
{data.enabledAppoint && (
<View
className='WasherStart-footerItem'
onClick={() => goAppointmentPayHandle(2)}>
我要预约
</View>
)}
{data.enabledAppoint && (
<View className='WasherStart-footerLine'></View>
)}
<View
className='WasherStart-footerItem'
onClick={() => goAppointmentPayHandle(1)}>
马上使用
</View>
</View>
</View>
);
};
WasherStart.config = {
navigationBarBackgroundColor: '#f9faff',
navigationBarTitleText: '洗衣',
};
export default WasherStart;
.WashingMachine {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
background: linear-gradient(180deg, #486dec, #b3b8fc);
.WashingMachine-main {
flex: 1;
overflow-y: hidden;
}
.WashingMachine-mask {
position: fixed;
width: 100%;
height: 100%;
left: 0;
right: 0;
top: 0;
bottom: 0;
background-color: rgba($color: #000000, $alpha: 0.3);
z-index: 10;
}
.WashingMachine-location {
.WashingMachine-locationMain {
position: fixed;
bottom: 0;
width: 100%;
height: 0;
background-color: #fff;
z-index: 12;
border-radius: 20px 20px 0 0;
transition: height 0.28s ease-out;
&.visibale {
height: 70%;
}
}
.WashingMachine-locationMainInner {
background-color: #f4f6fe;
}
}
.WashingMachine-appointmentMain {
position: fixed;
top: 140px;
left: 50%;
transform: translate(-50%, 0);
z-index: 12;
.WashingMachine-appointmentClose {
width: 72px;
height: 72px;
margin: 120px auto 0;
}
}
.WashingMachine-footer {
display: flex;
background-color: #fff;
height: 100px;
line-height: 100px;
justify-content: space-around;
align-items: center;
font-size: 32px;
color: #999;
box-shadow: 0 -4px 10px #f6f6f6;
.WashingMachine-footer-item {
width: 150px;
height: 100%;
text-align: center;
&.actived {
position: relative;
color: #456beb;
}
&.actived::before {
content: '';
position: absolute;
left: 50%;
top: 0;
height: 4px;
width: 44px;
background-color: #ffd506;
transform: translate(-50%, 0);
}
}
}
}
import { View } from '@tarojs/components';
import './WashingMachine.scss';
import { View, Image } from '@tarojs/components';
import Home from './views/Home';
import Location from './views/Location';
import Appoint from './views/Appoint';
import Taro, {
useState,
useCallback,
useEffect,
usePullDownRefresh,
useRef,
} from '@tarojs/taro';
import AppointmentCard from './components/AppointmentCard';
import CloseIcon from '../../images/washingmachine/ic_yuyueguanbi@2x.png';
import { useSelector } from '@tarojs/redux';
import { WasherStoreState } from '@/store/rootReducers/washer';
import useWasherStart from './hooks/useWasherStart';
import { beginWasher } from '@/api/washingMachine';
const WashingMachine = () => {
return <View>WashingMachine</View>;
const HomeRef = useRef(Promise.resolve);
const LocationRef = useRef(Promise.resolve);
const AppointRef = useRef(Promise.resolve);
const getRef = (name: string, fun) => {
if (name === 'Home') {
HomeRef.current = fun;
} else if (name === 'Location') {
LocationRef.current = fun;
} else if (name === 'Appoint') {
AppointRef.current = fun;
}
};
const [showState, setShowState] = useState('Home');
const [showLocation, setShowLocation] = useState(false);
const [showAppoint, setShowAppoint] = useState(false);
const orderDetail = useSelector(
(state: { washer: WasherStoreState }) => state.washer.orderDetail,
);
const showLocationHandle = useCallback(
visibale => setShowLocation(visibale),
[setShowLocation],
);
const [deviceCode, setDeviceCode] = useState('');
const startHandle = (code: string) => {
Taro.showLoading();
setDeviceCode(code);
};
const { wsState, bluetoothState } = useWasherStart(deviceCode);
console.log('orderDetail', orderDetail.deviceCode);
useEffect(() => {
console.log('orderDetail', orderDetail.deviceCode);
if (orderDetail.deviceCode) {
setShowAppoint(true);
}
}, [orderDetail.deviceCode]);
useEffect(() => {
console.log(wsState.socketState, bluetoothState);
if (wsState.socketState && bluetoothState) {
beginWasher({
deviceCode,
orderCode: orderDetail.orderCode,
})
.then(res => {
Taro.hideLoading();
setShowAppoint(false);
HomeRef.current();
Taro.showToast({
title: res.msg || '开启成功',
icon: 'none',
});
})
.catch(err => {
Taro.hideLoading();
Taro.showToast({
title: err.msg || '开启失败',
icon: 'none',
});
});
}
}, [wsState.socketState, bluetoothState]);
usePullDownRefresh(() => {
if (showState === 'Home') {
if (HomeRef.current) {
HomeRef.current().then(() => {
Taro.stopPullDownRefresh();
});
} else {
Taro.stopPullDownRefresh();
}
} else if (showState === 'Location') {
if (LocationRef.current) {
LocationRef.current().then(() => {
Taro.stopPullDownRefresh();
});
} else {
Taro.stopPullDownRefresh();
}
} else if (showState === 'Appoint') {
if (AppointRef.current) {
AppointRef.current(0).then(() => {
Taro.stopPullDownRefresh();
});
} else {
Taro.stopPullDownRefresh();
}
} else {
Taro.stopPullDownRefresh();
}
});
return (
<View className='WashingMachine'>
<View className='WashingMachine-main'>
{showState === 'Home' && (
<Home getRef={getRef} showLocationHandle={showLocationHandle} />
)}
{showState === 'Location' && <Location getRef={getRef} />}
{showState === 'Appoint' && (
<Appoint getRef={getRef} startHandle={startHandle} />
)}
</View>
<View className='WashingMachine-location'>
{showLocation && (
<View
className='WashingMachine-mask'
onClick={() => setShowLocation(false)}></View>
)}
<View
className={`WashingMachine-locationMain ${
showLocation ? 'visibale' : ''
}`}>
<Location
getRef={getRef}
input-bg='WashingMachine-locationMainInner'
/>
</View>
</View>
{showAppoint && (
<View className='WashingMachine-appointment'>
<View className='WashingMachine-mask'></View>
<View className='WashingMachine-appointmentMain'>
<AppointmentCard data={orderDetail} startHandle={startHandle} />
<View
className='WashingMachine-appointmentClose'
onClick={() => setShowAppoint(false)}>
<Image src={CloseIcon} />
</View>
</View>
</View>
)}
<View className='WashingMachine-footer'>
<View
className={`WashingMachine-footer-item ${
showState === 'Home' ? 'actived' : ''
}`}
onClick={() => setShowState('Home')}>
洗衣
</View>
<View
className={`WashingMachine-footer-item ${
showState === 'Location' ? 'actived' : ''
}`}
onClick={() => setShowState('Location')}>
常用位置
</View>
<View
className={`WashingMachine-footer-item ${
showState === 'Appoint' ? 'actived' : ''
}`}
onClick={() => setShowState('Appoint')}>
预约记录
</View>
</View>
</View>
);
};
WashingMachine.config = {
navigationBarBackgroundColor: '#486dec',
navigationBarTitleText: '洗衣',
navigationBarTextStyle: 'white',
backgroundColor: '#486dec',
enablePullDownRefresh: true,
};
export default WashingMachine;
.AppointmentCard {
box-sizing: border-box;
width: 674px;
margin: 0 auto 48px;
padding: 40px;
border-radius: 20px;
background-color: #fff;
.AppointmentCard-title {
display: flex;
justify-content: space-between;
font-size: 32px;
margin-bottom: 40px;
.AppointmentCard-titleText {
color: #333;
}
.AppointmentCard-titlePrice {
color: #f93d3d;
}
}
.AppointmentCard-deviceInfo {
font-size: 28px;
color: #333;
.AppointmentCard-deviceItem {
display: flex;
margin-bottom: 20px;
.AppointmentCard-deviceLabel {
margin-right: 20px;
}
.AppointmentCard-deviceValue {
flex: 1;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.AppointmentCard-service {
display: flex;
margin-bottom: 8px;
}
}
}
.AppointmentCard-time {
display: flex;
flex-direction: column;
width: 582px;
height: 160px;
border-radius: 20px;
background-color: #eaeffd;
justify-content: center;
align-items: center;
font-size: 28px;
color: #456beb;
line-height: 48px;
margin-bottom: 20px;
.AppointmentCard-timeItem {
display: flex;
width: 490px;
.AppointmentCard-timeLabel {
margin-right: 20px;
}
}
}
.AppointmentCard-start {
height: 90px;
line-height: 90px;
font-size: 32px;
color: #fff;
}
&.disabled {
position: relative;
.AppointmentCard-titleText,
.AppointmentCard-titlePrice,
.AppointmentCard-deviceInfo {
color: #999;
}
.AppointmentCard-time {
height: 100px;
background-color: #fff;
color: #d2d5f9;
align-items: flex-start;
}
}
.AppointmentCard-timeoutIcon {
position: absolute;
right: 0;
bottom: 0;
width: 262px;
height: 246px;
}
}
import './AppointmentCard.scss';
import { View, Button, Image } from '@tarojs/components';
import { AppointOrder, AppointOrderStatus } from '@/api/washingMachine';
import TimeoutIcon from '../../../images/shower/img_shixiao@2x.png';
const AppointmentCard = (props: {
data: AppointOrder;
startHandle: (code: string) => void;
}) => {
const {
money,
deviceCode,
position,
services,
startDate,
endDate,
status,
} = props.data;
const { startHandle } = props;
return (
<View
className={`AppointmentCard ${
status === AppointOrderStatus.outtime ? 'disabled' : ''
}`}>
<View className='AppointmentCard-title'>
<View className='AppointmentCard-titleText'>我的预约</View>
<View className='AppointmentCard-titlePrice'>{money.toFixed(2)}</View>
</View>
<View className='AppointmentCard-deviceInfo'>
<View className='AppointmentCard-deviceItem'>
<View className='AppointmentCard-deviceLabel'>设备编码:</View>
<View className='AppointmentCard-deviceValue'>{deviceCode}</View>
</View>
<View className='AppointmentCard-deviceItem'>
<View className='AppointmentCard-deviceLabel'>设备位置:</View>
<View className='AppointmentCard-deviceValue'>{position}</View>
</View>
<View className='AppointmentCard-deviceItem'>
<View className='AppointmentCard-deviceLabel'>洗衣功能:</View>
<View className='AppointmentCard-deviceValue'>
{services.map(service => (
<View key={service.id} className='AppointmentCard-service'>
<View className='AppointmentCard-serviceName'>
{service.name}
</View>
<View className='AppointmentCard-servicePrice'>
{service.price.toFixed(2)}
</View>
</View>
))}
</View>
</View>
<View className='AppointmentCard-time'>
<View className='AppointmentCard-timeItem'>
<View className='AppointmentCard-timeLabel'>预约生效时间:</View>
<View className='AppointmentCard-timeValue'>{startDate}</View>
</View>
<View className='AppointmentCard-timeItem'>
<View className='AppointmentCard-timeLabel'>预约失效时间:</View>
<View className='AppointmentCard-timeValue'>{endDate}</View>
</View>
</View>
{status === AppointOrderStatus.appointing && (
<Button
className='AppointmentCard-start'
onClick={() => startHandle(deviceCode)}>
开启洗衣
</Button>
)}
{status === AppointOrderStatus.outtime && (
<View className='AppointmentCard-timeoutIcon'>
<Image src={TimeoutIcon} />
</View>
)}
</View>
</View>
);
};
AppointmentCard.defaultProps = {
data: {
bluetoothMode: 0,
deviceCode: '',
deviceName: '',
endDate: '',
money: 0,
orderCode: '',
position: '',
services: [],
startDate: '',
status: AppointOrderStatus.appointing,
},
};
export default AppointmentCard;
.WasherCard {
display: flex;
box-sizing: border-box;
margin: 0 auto 20px;
width: 670px;
height: 160px;
padding: 16px 40px 16px 32px;
background-color: #fafafe;
border-radius: 20px;
font-size: 28px;
color: #333;
align-items: center;
&.disabled {
color: #999;
}
.WasherCard-info {
height: 100%;
flex: 1;
.WasherCard-item {
display: flex;
line-height: 46px;
.WasherCard-label {
margin-right: 30px;
}
.WasherCard-value {
flex: 1;
&.position {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
}
}
}
.WasherCard-btn {
display: flex;
width: 80px;
height: 80px;
justify-content: center;
align-items: center;
.WasherCard-start {
width: 70px;
height: 70px;
}
.WasherCard-isRunning {
width: 40px;
height: 40px;
}
}
}
import { View, Image } from '@tarojs/components';
import { WashingMachineDevice } from '@/api/washingMachine';
import './WasherCard.scss';
import StartIcon from '../../../images/washingmachine/ic_kaiqi@2x.png';
import RunningIcon from '../../../images/washingmachine/ic_fanmang@2x.png';
const WasherCard = (props: {
data: WashingMachineDevice;
goAppointmentOrder: (deviceCode: string) => void;
}) => {
const { data, goAppointmentOrder } = props;
return (
<View className={`WasherCard ${data.isRunning ? 'disabled' : ''}`}>
<View className='WasherCard-info'>
<View className='WasherCard-item'>
<View className='WasherCard-label'>设备编码</View>
<View className='WasherCard-value'>{data.deviceCode}</View>
</View>
<View className='WasherCard-item'>
<View className='WasherCard-label'>设备位置</View>
<View className='WasherCard-value position'>{data.position}</View>
</View>
</View>
<View className='WasherCard-btn'>
{data.isRunning ? (
<View className='WasherCard-isRunning'>
<Image src={RunningIcon} />
</View>
) : (
<View
className='WasherCard-start'
onClick={() => goAppointmentOrder(data.deviceCode)}>
<Image src={StartIcon} />
</View>
)}
</View>
</View>
);
};
export default WasherCard;
.WasherDevice-device {
background-color: #f9faff;
display: flex;
height: 160px;
padding: 0 48px 0 40px;
.WasherDevice-deviceInfo {
flex: 1;
font-size: 28px;
color: #333;
.WasherDevice-deviceItem {
display: flex;
margin-bottom: 10px;
.WasherDevice-deviceLabel {
margin-right: 18px;
}
.WasherDevice-deviceValue {
width: 340px;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
}
}
.WasherDevice-deviceIcon {
width: 120px;
height: 120px;
}
}
import './WasherDevice.scss';
import { View, Image } from '@tarojs/components';
import DeviceIcon from '../../../images/washingmachine/ic_xiyi@2x.png';
const WasherDevice = (props: {
data: { deviceCode: string; postion: string };
}) => {
return (
<View className='WasherDevice-device'>
<View className='WasherDevice-deviceInfo'>
<View className='WasherDevice-deviceItem'>
<View className='WasherDevice-deviceLabel'>设备编码:</View>
<View className='WasherDevice-deviceValue'>
{props.data.deviceCode}
</View>
</View>
<View className='WasherDevice-deviceItem'>
<View className='WasherDevice-deviceLabel'>设备位置:</View>
<View className='WasherDevice-deviceValue'>
{props.data.postion}
</View>
</View>
</View>
<View className='WasherDevice-deviceIcon'>
<Image src={DeviceIcon} />
</View>
</View>
);
};
export default WasherDevice;
import { useSelector } from '@tarojs/redux';
import { Customer } from '@/types/Customer/Customer';
import { useCallback, useEffect, useState } from '@tarojs/taro';
import { fetchAppointmentList, AppointOrder } from '@/api/washingMachine';
const initList: AppointOrder[] = [];
const useAppointList = (
pageNum: number,
): [AppointOrder[], (pageNum: number) => void] => {
const [list, setList] = useState(initList);
const userInfo = useSelector(
(state: { userinfo: Customer }) => state.userinfo,
);
const fetchHandle = useCallback(
(pageNum: number) =>
fetchAppointmentList({
customerId: userInfo.customerId,
pageNum,
})
.then(res => {
console.log(res);
if (pageNum) {
setList([...list, ...res.data]);
} else {
setList(res.data);
}
})
.catch(err => {
console.log(err);
}),
[userInfo.customerId],
);
useEffect(() => {
fetchHandle(pageNum);
}, [pageNum]);
return [list, fetchHandle];
};
export default useAppointList;
import { Customer } from '@/types/Customer/Customer';
import { useSelector } from '@tarojs/redux';
import {
fetchPositionList,
Position,
addUsedPosition,
delUsedPosition,
} from '@/api/washingMachine';
import Taro, { useCallback, useState, useEffect } from '@tarojs/taro';
const initList: Position[] = [];
const usePositionList = (
positionName: string,
): {
list: Position[];
usedList: Position[];
fetchList: (positionName: string) => void;
addPositionHandle: (id: string) => void;
delPositionHandle: (id: string) => void;
} => {
const userInfo = useSelector(
(state: { userinfo: Customer }) => state.userinfo,
);
const [list, setList] = useState(initList);
const [usedList, setUsedList] = useState(initList);
const fetchHandle = useCallback(
positionName => {
Taro.showLoading();
return fetchPositionList({
campusId: userInfo.areaId,
customerId: userInfo.customerId,
name: positionName,
})
.then(res => {
Taro.hideLoading();
console.log(res.data);
const { positions, usedPositions } = res.data;
setList(positions);
setUsedList(usedPositions);
})
.catch(err => {
Taro.hideLoading();
console.log(err);
Taro.showToast({
title: err.msg || '请求失败',
icon: 'none',
});
});
},
[positionName],
);
const addPositionHandle = useCallback(
(id: string) => {
addUsedPosition({
positionId: id,
customerId: userInfo.customerId,
})
.then(res => {
console.log(res);
fetchHandle('');
})
.catch(err => {
console.log(err);
Taro.showToast({
title: err.msg || '网络错误',
icon: 'none',
});
});
},
[addUsedPosition],
);
const delPositionHandle = useCallback(
(id: string) => {
delUsedPosition({
positionId: id,
customerId: userInfo.customerId,
})
.then(res => {
console.log(res);
fetchHandle('');
})
.catch(err => {
console.log(err);
Taro.showToast({
title: err.msg || '网络错误',
icon: 'none',
});
});
},
[addUsedPosition],
);
useEffect(() => {
fetchHandle(positionName);
}, [positionName, userInfo.customerId]);
return {
list,
usedList,
fetchList: fetchHandle,
addPositionHandle,
delPositionHandle,
};
};
export default usePositionList;
import Taro, { useCallback, useEffect } from '@tarojs/taro';
import {
fetchWasherDeviceInfo,
FetchWasherDeviceInfoRes,
} from '@/api/washingMachine';
import { useSelector, useDispatch } from '@tarojs/redux';
import { Customer } from '@/types/Customer/Customer';
import { updateWasherDeviceInfo } from '@/store/rootReducers/washer';
const initState: FetchWasherDeviceInfoRes = {
bluetoothMode: 0,
deviceCode: '',
deviceName: '',
enabledAppoint: false,
optionalPrograms: [],
paymentWaysInner: [],
paymentWaysOuter: [],
position: '',
requiredPrograms: [],
};
const useWasherDevice = (
deviceCode: string,
goNextPage?: () => void,
): FetchWasherDeviceInfoRes => {
const userInfo = useSelector(
(state: { userinfo: Customer }) => state.userinfo,
);
const data = useSelector(
(state: { washer: FetchWasherDeviceInfoRes }) => state.washer,
);
const dispatch = useDispatch();
const fetchHandle = useCallback(
(deviceCode: string) => {
if (deviceCode) {
Taro.showLoading();
fetchWasherDeviceInfo({
customerId: userInfo.customerId,
deviceCode: deviceCode,
})
.then(res => {
Taro.hideLoading();
console.log(res);
dispatch(updateWasherDeviceInfo(res.data));
goNextPage && goNextPage();
})
.catch(err => {
Taro.hideLoading();
Taro.showToast({
title: err.msg || '网络异常',
icon: 'none',
});
});
}
},
[fetchWasherDeviceInfo, userInfo.customerId, deviceCode],
);
useEffect(() => {
console.log('orderDetail', deviceCode);
if (deviceCode) {
fetchHandle(deviceCode);
} else {
updateWasherDeviceInfo(initState);
}
}, [deviceCode]);
return data;
};
export default useWasherDevice;
import {
FetchWasherStatus,
WashingMachineDevice,
fetchDeviceListByStatus,
AppointOrderStatus,
} from '@/api/washingMachine';
import Taro, { useEffect, useState, useCallback } from '@tarojs/taro';
import { useSelector, useDispatch } from '@tarojs/redux';
import { Customer } from '@/types/Customer/Customer';
import { updateWasherOrderDetail } from '@/store/rootReducers/washer';
const initList: WashingMachineDevice[] = [];
const useWasherList = (
state: FetchWasherStatus,
): [WashingMachineDevice[], (state: FetchWasherStatus) => void] => {
const dispatch = useDispatch();
const userInfo = useSelector(
(state: { userinfo: Customer }) => state.userinfo,
);
const [list, setList] = useState(initList);
const fetchHandle = useCallback(
(state: FetchWasherStatus) => {
Taro.showLoading();
return fetchDeviceListByStatus({
customerId: userInfo.customerId,
status: state,
campusId: userInfo.areaId,
})
.then(res => {
Taro.hideLoading();
console.log(res.data);
const { deviceList, appointOrder } = res.data;
setList(deviceList);
if (appointOrder) {
dispatch(
updateWasherOrderDetail({
...appointOrder,
money: appointOrder.money / 100,
status: AppointOrderStatus.appointing,
}),
);
} else {
dispatch(
updateWasherOrderDetail({
bluetoothMode: 0,
deviceCode: '',
deviceName: '',
endDate: '',
money: 0,
orderCode: '',
position: '',
services: [],
startDate: '',
status: AppointOrderStatus.appointing,
}),
);
}
})
.catch(err => {
Taro.hideLoading();
Taro.showToast({
title: err.msg || '网络异常',
icon: 'none',
});
});
},
[fetchDeviceListByStatus, userInfo.customerId, userInfo.areaId],
);
useEffect(() => {
fetchHandle(state);
}, [state]);
return [list, fetchHandle];
};
export default useWasherList;
import useBluetooth from '@/hooks/useBluetooth';
import useDeviceWS from '@/hooks/useDeviceWS';
import { WM_SOCKET_URL } from '@/constants';
import { useEffect } from '@tarojs/taro';
import { str2ab } from '@/utils/arrayBuffer';
const useWasherStart = (code: string) => {
const services = [
'6E401103-B5A3-F393-E0A9-E50E24DCCA9E',
'6E400002-B5A3-F393-E0A9-E50E24DCCA9E',
'6E400003-B5A3-F393-E0A9-E50E24DCCA9E',
];
const getBluetoothData = (data: string) => {
sendMessageToServer(
data,
res => {
console.log(res);
},
err => {
console.log(err);
},
);
};
const getSocketData = (data: string) => {
sendMessageToDevice(data);
};
const {
state: wsState,
sendMessageToServer,
connectDeviceSocket,
} = useDeviceWS({
url: WM_SOCKET_URL,
getSocketData,
});
const {
state: bluetoothState,
sendMessageToDevice,
devicesDiscoveryHandle,
} = useBluetooth({
getBluetoothData,
services,
});
useEffect(() => {
console.log('useWasherStart useEffect code', code);
if (code) {
connectDeviceSocket();
devicesDiscoveryHandle(code);
}
}, [code]);
useEffect(() => {
if (code) {
if (bluetoothState && wsState.socketState && wsState.socketTask) {
let deviceData = '{<' + code + '>}';
console.log('<---发送设备编号:', deviceData);
wsState.socketTask.send({
data: str2ab(deviceData),
success: msg => {
console.log('发送设备编号:', msg, '--->');
},
fail: err => {
console.log('发送设备编号:', err, '--->');
},
});
}
}
}, [bluetoothState, wsState.socketState, code]);
return {
wsState,
bluetoothState,
};
};
export default useWasherStart;
.WashingMachineAppoint {
height: 100%;
// display: flex;
// flex-direction: column;
overflow-y: scroll;
}
import './Appoint.scss';
import { View } from '@tarojs/components';
import AppointmentCard from '../components/AppointmentCard';
import useAppointList from '../hooks/useAppointList';
import NoDataIcon from '@/components/NoDataIcon/NoDataIcon';
import { useEffect } from '@tarojs/taro';
const WashingMachineAppoint = (props: {
getRef: (name: string, fun: Function) => void;
startHandle: (code: string) => void;
}) => {
const [list, refreshList] = useAppointList(0);
useEffect(() => {
props.getRef('Home', refreshList);
}, []);
return (
<View className='WashingMachineAppoint'>
{list && list.length ? (
list.map(order => (
<AppointmentCard
key={order.orderCode}
data={order}
startHandle={props.startHandle}
/>
))
) : (
<NoDataIcon text='暂无预约记录~' />
)}
</View>
);
};
export default WashingMachineAppoint;
.WashingMachineHome {
height: 100%;
display: flex;
flex-direction: column;
.WashingMachineHome-sreach {
display: flex;
justify-content: center;
padding: 48px 0;
.WashingMachineHome-sreach-inputbox {
display: flex;
align-items: center;
box-sizing: border-box;
width: 544px;
padding: 0 28px;
background-color: #fff;
border-radius: 20px;
margin-right: 34px;
font-size: 24px;
.WashingMachineHome-sreach-inputbox_input {
flex: 1;
}
.WashingMachineHome-sreach-inputbox_text {
color: #456beb;
}
}
.WashingMachineHome-scan {
width: 100px;
height: 100px;
}
}
.WashingMachineHome-list {
flex: 1;
background-color: #fff;
border-radius: 20px 20px 0 0;
display: flex;
flex-direction: column;
.WashingMachineHome-list_title {
display: flex;
padding: 40px 48px 20px;
justify-content: space-between;
.WashingMachineHome-list_titleItem {
font-size: 32px;
color: #333;
padding: 0 20px;
&:first-child {
border-right: 1px solid #d8d8d8;
}
&.actived {
color: #456beb;
}
}
.WashingMachineHome-list_titleLocation {
font-size: 28px;
color: #456beb;
}
}
.WashingMachineHome-list_content {
flex: 1;
overflow-y: auto;
.WashingMachineHome-list_nocontent {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.WashingMachineHome-list_nocontentImg {
width: 246px;
height: 242px;
margin: 160px 0 20px;
}
.WashingMachineHome-list_nocontentText {
font-size: 28px;
color: #999;
}
.WashingMachineHome-list_nocontentBtn {
width: 156px;
height: 52px;
line-height: 52px;
margin-top: 40px;
background-color: #d4deff;
border-radius: 26px;
font-size: 24px;
color: #456beb;
text-align: center;
}
}
}
}
}
import { View, Image, Input, Text } from '@tarojs/components';
import './Home.scss';
import ScanIcon from '../../../images/washingmachine/ic_saoma-e@2x.png';
import NoDataIcon from '../../../images/washingmachine/img_empty@2x.png';
import Taro, {
useState,
useCallback,
useDidShow,
useEffect,
} from '@tarojs/taro';
import { FetchWasherStatus } from '@/api/washingMachine';
import useWasherList from '../hooks/useWasherList';
import WasherCard from '../components/WasherCard';
import useInputValue from '@/hooks/useInputValue';
import useWasherDevice from '../hooks/useWasherDevice';
const WashingMachineHome = (props: {
showLocationHandle: (visibility: boolean) => void;
getRef: (name: string, fun: Function) => void;
}) => {
const [showState, setShowState] = useState(0); // 0:空闲;1:使用中。
const { value, onChange } = useInputValue('');
const [deviceList, fetchDeviceList] = useWasherList(showState);
const [deviceCode, setDeviceCode] = useState('');
useWasherDevice(deviceCode, () => {
setDeviceCode('');
Taro.navigateTo({
url: '/pages/WashingMachine/WasherStart',
});
});
const scanHandle = () => {
console.log('scanHandle');
Taro.scanCode()
.then(res => {
console.log(res);
const result: string = res.result;
onChange({
type: '',
timeStamp: 0,
target: { id: '', tagName: '', dataset: {} },
currentTarget: { id: '', tagName: '', dataset: {} },
detail: { value: result, cursor: 0, keyCode: 0 },
preventDefault: () => {},
stopPropagation: () => {},
});
})
.catch(err => {
console.log(err);
Taro.showToast({
title: err.errMsg,
icon: 'none',
});
});
};
const searchHandle = () => {
console.log('searchHandle', value);
if (value) {
setDeviceCode(value);
}
};
const refreshList = () => fetchDeviceList(showState);
const goAppointmentOrder = useCallback((value: string) => {
console.log('goAppointmentOrder', deviceCode, value);
if (deviceCode === value) {
Taro.navigateTo({
url: '/pages/WashingMachine/WasherStart',
});
} else {
setDeviceCode(value);
}
}, []);
useEffect(() => {
props.getRef('Home', refreshList);
}, []);
useDidShow(() => {
console.log('componentDidShow');
refreshList();
});
return (
<View className='WashingMachineHome'>
<View className='WashingMachineHome-sreach'>
<View className='WashingMachineHome-sreach-inputbox'>
<Input
className='WashingMachineHome-sreach-inputbox_input'
placeholder='输入洗衣机上的设备编码或点击扫一扫'
value={value}
onInput={onChange}
/>
<View
className='WashingMachineHome-sreach-inputbox_text'
onClick={searchHandle}>
搜索
</View>
</View>
<View className='WashingMachineHome-scan' onClick={scanHandle}>
<Image src={ScanIcon}></Image>
</View>
</View>
<View className='WashingMachineHome-list'>
<View className='WashingMachineHome-list_title'>
<View>
<Text
className={`WashingMachineHome-list_titleItem ${
showState === FetchWasherStatus.unusing ? 'actived' : ''
}`}
onClick={() => setShowState(0)}>
空闲中
</Text>
<Text
className={`WashingMachineHome-list_titleItem ${
showState === FetchWasherStatus.using ? 'actived' : ''
}`}
onClick={() => setShowState(1)}>
将结束
</Text>
</View>
<View
className='WashingMachineHome-list_titleLocation'
onClick={() => props.showLocationHandle(true)}>
位置筛选
</View>
</View>
<View className='WashingMachineHome-list_content'>
{deviceList && deviceList.length ? (
deviceList.map(data => (
<WasherCard
key={data.deviceCode}
data={data}
goAppointmentOrder={goAppointmentOrder}
/>
))
) : (
<View className='WashingMachineHome-list_nocontent'>
<View className='WashingMachineHome-list_nocontentImg'>
<Image src={NoDataIcon} />
</View>
<View className='WashingMachineHome-list_nocontentText'>
洗衣机突然暴走了~
</View>
<View
className='WashingMachineHome-list_nocontentBtn'
onClick={refreshList}>
刷新看看
</View>
</View>
)}
</View>
</View>
</View>
);
};
export default WashingMachineHome;
.WashingMachineLocation {
height: 100%;
display: flex;
flex-direction: column;
.WashingMachineLocation-search {
margin: 48px 36px;
height: 100px;
line-height: 100px;
background-color: #fff;
border-radius: 20px;
text-align: center;
input {
height: 100%;
}
}
.WashingMachineLocation-PostionList {
flex: 1;
background-color: #fff;
border-radius: 20px 20px 0 0;
padding: 40px 40px 0;
overflow-y: scroll;
.WashingMachineLocation-title {
font-size: 28px;
color: #999;
margin-bottom: 20px;
}
.WashingMachineLocation-usuallyItem {
display: flex;
align-items: center;
width: 100%;
height: 84px;
box-sizing: border-box;
background-color: #eaeffd;
border-radius: 20px;
padding: 0 28px;
margin-bottom: 20px;
.WashingMachineLocation-usuallyItem_postion {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.WashingMachineLocation-usuallyItem_closeIcon {
width: 40px;
height: 40px;
}
}
.WashingMachineLocation-listItem {
font-size: 32px;
color: #333;
margin-bottom: 20px;
}
}
}
import { View, Input, Image } from '@tarojs/components';
import './Location.scss';
import CloseIcon from '../../../images/washingmachine/ic_guanbi@2x.png';
import usePositionList from '../hooks/usePositionList';
import { useState, useCallback, useEffect } from '@tarojs/taro';
import useInputValue from '@/hooks/useInputValue';
const WashingMachineLocation = (props: {
getRef: (name: string, fun: Function) => void;
}) => {
const { value, onChange, setValue } = useInputValue('');
const [positionInput, setPositionInput] = useState('');
const inputHandle = e => {
console.log('in inputHandle', e);
setPositionInput(value);
setValue('');
};
const {
list,
usedList,
fetchList,
addPositionHandle,
delPositionHandle,
} = usePositionList(positionInput);
useEffect(() => {
props.getRef('Location', fetchList);
}, []);
return (
<View className='WashingMachineLocation'>
<View className='WashingMachineLocation-search input-bg'>
<Input
placeholder='输入要搜索的位置'
value={value}
onInput={onChange}
onConfirm={inputHandle}
/>
</View>
<View className='WashingMachineLocation-PostionList'>
{usedList && usedList.length && (
<View className='WashingMachineLocation-usually'>
<View className='WashingMachineLocation-title'>已选择常用位置</View>
{usedList.map(position => (
<View
key={position.id}
className='WashingMachineLocation-usuallyItem'>
<View
key={position.id}
className='WashingMachineLocation-usuallyItem_postion'>
{position.name}
</View>
<View
className='WashingMachineLocation-usuallyItem_closeIcon'
onClick={() => delPositionHandle(position.id)}>
<Image src={CloseIcon} />
</View>
</View>
))}
</View>
)}
<View className='WashingMachineLocation-list'>
<View className='WashingMachineLocation-title'>全部位置</View>
{list.map(position => (
<View
key={position.id}
className='WashingMachineLocation-listItem'
onClick={() => addPositionHandle(position.id)}>
{position.name}
</View>
))}
</View>
</View>
</View>
);
};
WashingMachineLocation.externalClasses = ['input-bg'];
export default WashingMachineLocation;
......@@ -81,6 +81,7 @@ class Index extends Component {
updateUserInfo(data);
Taro.redirectTo({
url: '/pages/Home/Home',
// url: '/pages/WashingMachine/WashingMachine',
});
})
.catch(err => {
......
......@@ -6,6 +6,7 @@ import serviceList from './rootReducers/service';
import orderState from './rootReducers/orderState';
import showerState from './rootReducers/shower';
import systemInfo from './rootReducers/systemInfo';
import washer from './rootReducers/washer';
export default combineReducers({
userinfo,
......@@ -15,4 +16,5 @@ export default combineReducers({
orderState,
showerState,
systemInfo,
washer,
});
import Actions from '@/types/Store/Actions';
import {
FetchWasherDeviceInfoRes,
CreateWasherOrderRes,
AppointOrder,
AppointOrderStatus,
} from '@/api/washingMachine';
const INITIAL_STATE = {
deviceInfo: {
bluetoothMode: 0,
deviceCode: '',
deviceName: '',
enabledAppoint: false,
optionalPrograms: [],
paymentWaysInner: [],
paymentWaysOuter: [],
position: '',
requiredPrograms: [],
},
orderInfo: {
appointmentTimeout: 0,
deviceCode: '',
deviceName: '',
money: 0,
orderCode: '',
orderType: 0,
payTimeout: 0,
paymentWayId: 0,
position: '',
serviceId: 0,
services: [],
},
orderDetail: {
bluetoothMode: 0,
deviceCode: '',
deviceName: '',
endDate: '',
money: 0,
orderCode: '',
position: '',
services: [],
startDate: '',
status: AppointOrderStatus.appointing,
},
};
export type WasherStoreState = {
deviceInfo: FetchWasherDeviceInfoRes;
orderInfo: CreateWasherOrderRes;
orderDetail: AppointOrder;
};
export const updateWasherDeviceInfo = (
entity: StoreState<FetchWasherDeviceInfoRes>,
): Actions => ({
type: 'UPDATE_WASHER_DEVICE',
payload: entity,
});
export const updateWasherOrderInfo = (
entity: StoreState<CreateWasherOrderRes>,
): Actions => ({
type: 'UPDATE_WASHER_ORDER',
payload: entity,
});
export const updateWasherOrderDetail = (
entity: StoreState<AppointOrder>,
): Actions => ({
type: 'UPDATE_WASHER_DETAIL',
payload: entity,
});
export default function washer(
state: WasherStoreState = INITIAL_STATE,
actions: Actions,
): WasherStoreState {
switch (actions.type) {
case 'UPDATE_WASHER_DEVICE':
return {
...state,
deviceInfo: {
...state.deviceInfo,
...actions.payload,
},
};
case 'UPDATE_WASHER_ORDER':
return {
...state,
orderInfo: {
...state.orderInfo,
...actions.payload,
},
};
case 'UPDATE_WASHER_DETAIL':
return {
...state,
orderDetail: {
...state.orderDetail,
...actions.payload,
},
};
default:
return state;
}
}
import AES from 'crypto-js/aes';
import Utf8 from 'crypto-js/enc-utf8';
import ECBmode from 'crypto-js/mode-ecb';
import PaddingPkcs7 from 'crypto-js/pad-pkcs7';
export const formatePayString = (
payStr: string,
customerId: number,
): {
timeStamp: number;
nonceStr: string;
package: string;
signType: string;
paySign: string;
} => {
const key = customerId.toString().padEnd(16, '0');
const payData = JSON.parse(
AES.decrypt(payStr, Utf8.parse(key), {
mode: ECBmode,
padding: PaddingPkcs7,
}).toString(Utf8),
);
return payData.msg;
};
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment