提交 bf78ef33 作者: ZhangLingKun

功能:商城商品下单页面

上级 3f6b08cc
流水线 #7601 已失败 于阶段
in 4 分 18 秒
import { CommonAPI } from './modules/common';
import { HomeAPI } from './modules/home';
import { UserAPI } from './modules/user';
import { WalletAPI } from './modules/wallet';
export { CommonAPI, HomeAPI };
export { CommonAPI, HomeAPI, UserAPI, WalletAPI };
import { InterFunction } from '@/api/interface';
// 查询用户地址列表-条件查询
export type UserAddressSelectList = InterFunction<
{
userAccountId?: number;
},
{
id: number;
takeAddress: string;
takeName: string;
takePhone: string;
takeRegion: string;
type: number;
}[]
>;
import { InterFunction } from '@/api/interface';
// 获取用户钱包(新)
export type UserPayWalletInfoType = InterFunction<
{},
{
cashAmt: number;
cashFreeze: number;
cashPaid: number;
id: number;
rebateWdl: number;
salaryAmt: number;
salaryFreeze: number;
salaryPaid: number;
totalAmount: number;
totalCash: number;
totalFreeze: number;
totalSalary: number;
userAccountId: number;
userName: string;
}
>;
import { UserAddressSelectList } from '@/api/interface/user';
import request from '../request';
export class UserAPI {
// 查询用户地址列表-条件查询
static userAddressSelectList: UserAddressSelectList = (params) =>
request.post('/oms/user-address/selectList', params);
}
import { UserPayWalletInfoType } from '@/api/interface/wallet';
import request from '@/api/request';
export class WalletAPI {
// 获取用户钱包(新)
static getUserPayWalletInfo: UserPayWalletInfoType = () =>
request.get('/userapp/pay/getCurrentUserPayWalletInfo');
}
......@@ -28,6 +28,8 @@ const BreadcrumbView: React.FC = () => {
{ name: '执照培训', path: 'train' },
{ name: '飞手约单', path: 'flyer' },
{ name: '商品详情', path: 'product' },
{ name: '购物车', path: 'cart' },
{ name: '提交订单', path: 'submit' },
];
// 转换路由
const getCurrentRouter = () => {
......
......@@ -3,7 +3,7 @@ import styled from 'styled-components';
export const ContentWrap = styled.div`
position: relative;
width: 100%;
min-height: 100vh;
min-height: 78vh;
box-sizing: border-box;
border-radius: 0;
opacity: 1;
......
......@@ -56,12 +56,14 @@ const HeaderView: React.FC<{
getLocationByIP().then((res) => {
dispatch(setAddress(res));
});
// 当前是否登录
if (!token) {
dispatch(setSystem({ token: undefined }));
dispatch(setUserInfo(null));
} else {
getAccountInfo().then();
}
}, []);
// 当前是否登录
useEffect(() => {
if (!system?.token || !token) return;
getAccountInfo().then();
}, [system?.token]);
// 右上角按钮
const items: MenuProps['items'] = [
{
......
......@@ -164,6 +164,8 @@ const LoginModalView = ({
// 设置token
Cookies.set('SHAREFLY-WEB-TOKEN', res.result.token);
message.success('登录成功');
// 刷新当前页面
window.location.reload();
}
};
// 设置定时器轮询获取信息
......
import React, { useEffect, useState } from 'react';
import { MinusOutlined, PlusOutlined } from '@ant-design/icons';
import { Button, InputNumber, Space } from 'antd';
import { InputNumberProps } from 'antd/es/input-number';
const NumberBox: React.FC<InputNumberProps> = (props) => {
// 使用state hooks来定义value和setValue
const [value, setValue] = useState<number>(1);
// 定义增加和减少的函数
const handleIncrement = () => {
// 不能大于最大
if (props?.max && value >= Number(props?.max)) return;
props?.onChange?.(value + 1);
setValue(value + 1);
};
const handleDecrement = () => {
// 不能低于最小
if (props?.min && value <= Number(props?.min)) return;
props?.onChange?.(value - 1);
setValue(value - 1);
};
// 监听value的变化
useEffect(() => {
if (!props?.value) return;
setValue(Number(props?.value) || 1);
}, [props?.value]);
return (
<Space.Compact block style={{ width: 'auto' }}>
<Button icon={<MinusOutlined />} onClick={handleDecrement} />
<InputNumber
{...props}
controls={false}
style={{ width: '42px' }}
value={value}
/>
<Button icon={<PlusOutlined />} onClick={handleIncrement} />
</Space.Compact>
);
};
export default NumberBox;
import React, { useEffect, useState } from 'react';
import { Checkbox } from 'antd';
import { CheckboxValueType } from 'antd/es/checkbox/Group';
import Big from 'big.js';
import { useSelector } from 'react-redux';
import styled from 'styled-components';
import { InterDataType } from '@/api/interface';
import { UserPayWalletInfoType } from '@/api/interface/wallet';
import { SystemState } from '@/store/module/system';
import { isNone } from '@/utils';
import getCurrentUserPayWalletInfo from '@/utils/getCurrentUserPayWalletInfo';
import { formatMoney } from '@/utils/money';
// 钱包类型
type WalletType = InterDataType<UserPayWalletInfoType>;
const PaymentCheckout: React.FC<{ cost: number }> = ({ cost }) => {
// system
const system = useSelector((state: any) => state.system) as SystemState;
// 选择的支付方式
const [paymentTypeValue, setPaymentTypeValue] = useState<CheckboxValueType[]>(
[],
);
// 钱包详情
const [walletInfo, setWalletInfo] = useState<WalletType>();
// 获取钱包详情
const getUserPayWalletInfo = async () => {
const res = await getCurrentUserPayWalletInfo();
if (res) setWalletInfo(res);
};
// 获取用户最多可抵扣的云享金余额
const getOrderPriceCashMax = () => {
// 当前的云享金余额
const cash = walletInfo?.cashAmt || 0;
// 如果用户的钱包金额大于等于订单金额 则可抵扣金额为订单金额
return cash >= (cost || 0) ? cost || 0 : cash;
};
// 获取用户最多可抵扣的薪水余额
const getOrderPriceSalaryMax = () => {
// 当前的薪水余额
const salary = walletInfo?.salaryAmt || 0;
// 如果用户的钱包金额大于等于订单金额 则可抵扣金额为订单金额
return salary >= (cost || 0) ? cost || 0 : salary;
};
// 获取用户最多可抵扣的总的金额
const getOrderPriceTotalMax = () => {
const cash = Big(getOrderPriceCashMax());
const salary = Big(getOrderPriceSalaryMax());
return cash.add(salary).toNumber();
};
// 获取用户的云享金余额
const getOrderPriceCash = () => {
// 云享金是否选中
const selectCash = paymentTypeValue?.includes(1);
// 余额是否选中
const selectSalary = paymentTypeValue?.includes(2);
// 总的抵扣金额
const total =
getOrderPriceTotalMax() > cost ? cost : getOrderPriceTotalMax();
// 只选其中一个的情况
if (!selectCash && selectSalary) {
return Big(total).minus(Big(getOrderPriceSalaryMax())).toNumber();
}
// 返回结果
return getOrderPriceCashMax();
};
// 获取用户的薪水余额
const getOrderPriceSalary = () => {
// 云享金是否选中
const selectCash = paymentTypeValue?.includes(1);
// 余额是否选中
const selectSalary = paymentTypeValue?.includes(2);
// 总的抵扣金额
const total =
getOrderPriceTotalMax() > cost ? cost : getOrderPriceTotalMax();
// 只选其中一个的情况
if (!selectCash && selectSalary) {
return getOrderPriceSalaryMax();
}
// 返回结果
return Big(total).minus(Big(getOrderPriceCashMax())).toNumber();
};
// 根据用户钱包和需要支付的金额设置默认选中
const setDefaultSelect = () => {
// 必须有金额时才继续执行
if (isNone(walletInfo?.cashAmt) || isNone(walletInfo?.salaryAmt)) {
return;
}
// 云享金
const cash = Big(walletInfo?.cashAmt || 0).toNumber();
// 余额
const salary = Big(walletInfo?.salaryAmt || 0).toNumber();
// 如果需要支付金额为零 则后面的判断都不执行
if (!cost || cost <= 0) {
setPaymentTypeValue([]);
return;
}
// 如果云享金大于等于订单金额 则默认选中云享金
if (cash >= cost) {
setPaymentTypeValue([1]);
return;
}
// 如果余额大于等于订单金额 则默认选中余额
if (salary >= cost) {
setPaymentTypeValue([2]);
return;
}
// 如果云享金有钱
if (cash > 0 && salary === 0) {
setPaymentTypeValue([1]);
return;
}
// 如果余额有钱
if (cash === 0 && salary > 0) {
setPaymentTypeValue([2]);
return;
}
// 如果两者都有余额
if (cash > 0 && salary > 0) {
setPaymentTypeValue([1, 2]);
}
};
// 组件挂载
useEffect(() => {
if (!system?.token) return;
getUserPayWalletInfo().then();
if (!cost) return;
setDefaultSelect();
}, [cost]);
return (
<PaymentCheckoutWrap>
<Checkbox.Group value={paymentTypeValue} onChange={setPaymentTypeValue}>
<div className="payment-item flex-start">
<img
src="https://file.iuav.com/file/checkout_icon_01.png"
alt="云享金"
className="icon"
/>
<div className="title">云享金</div>
<div className="num">-可抵扣¥{formatMoney(getOrderPriceCash())}</div>
<div className="checkbox">
<Checkbox value={1} />
</div>
</div>
<div className="payment-item flex-start">
<img
src="https://file.iuav.com/file/checkout_icon_02.png"
alt="余额"
className="icon"
/>
<div className="title">余额</div>
<div className="num">
-可抵扣¥{formatMoney(getOrderPriceSalary())}
</div>
<div className="checkbox">
<Checkbox value={2} />
</div>
</div>
</Checkbox.Group>
</PaymentCheckoutWrap>
);
};
export default PaymentCheckout;
// 样式
const PaymentCheckoutWrap = styled.div`
position: relative;
width: 18.6rem;
.payment-item {
position: relative;
width: 100%;
&:not(:last-child) {
margin-bottom: 0.66rem;
}
.icon {
width: 1.25rem;
height: 1.25rem;
}
.title,
.num {
margin-left: 0.5rem;
}
.num {
color: #ff4600;
}
.checkbox {
position: absolute;
top: 0;
right: 0;
}
}
`;
import React, { useEffect, useState } from 'react';
import { EnvironmentOutlined } from '@ant-design/icons';
import { InputNumber } from 'antd';
import { message } from 'antd';
import Big from 'big.js';
import dayjs from 'dayjs';
import { useSelector } from 'react-redux';
import { useRouter } from 'next/router';
import { useDispatch, useSelector } from 'react-redux';
import styled from 'styled-components';
import { InterDataType } from '@/api/interface';
import { AppMallGoodsDetails } from '@/api/interface/mall';
import NumberBox from '@/components/numberBox';
import ProductSwiperView from '@/components/product-swiper';
import { RootState } from '@/store';
import { AddressState } from '@/store/module/address';
import { setGlobalData } from '@/store/module/globalData';
import { UserInfoState } from '@/store/module/userInfo';
// 商品详情类型
type DetailType = InterDataType<AppMallGoodsDetails>;
const ProductHeadView: React.FC<{ detail: DetailType }> = ({ detail }) => {
// 当前的路由数据
const router = useRouter();
// store
const dispatch = useDispatch();
// address
const address = useSelector(
(state: RootState) => state.address,
) as AddressState;
const address = useSelector((state: any) => state.address) as AddressState;
// userInfo
const userInfo = useSelector((state: any) => state.userInfo) as UserInfoState;
// 当前选中的规格
const [selectSpecList, setSelectSpecList] = useState<
{ row: number; index: number }[]
......@@ -88,6 +95,57 @@ const ProductHeadView: React.FC<{ detail: DetailType }> = ({ detail }) => {
const money = Big(price).mul(Big(productNum)).toNumber().toLocaleString();
return money.endsWith('.00') ? money : `${money}.00`;
};
// 获取选中规格列表的补充数据
const getSelectPriceStock = () => {
const arr =
selectSpecList.map((i) => [
detail?.specAttrList?.[i.row]?.specName,
detail?.specAttrList?.[i.row]?.specValuesList?.[i.index]?.specName,
]) || [];
return Object.fromEntries(arr);
};
// 获取规格的选项
const getSelectPriceStockItem = () => {
const item = getSelectPriceStock();
return detail?.priceStock?.find(
(i) => i.productSpec === JSON.stringify(item),
);
};
// 提交事件
const handleSubmit = async (type: number) => {
// 判断是否选择了规格
if (selectSpecList.length === 0) {
message.warning('请选择规格');
return;
}
// 去重掉同分组的之后,再判断必选项是否已选
if (detail?.specAttrList?.length !== selectSpecList.length) {
const arr =
detail?.specAttrList
?.filter((i, j) => !selectSpecList.map((n) => n.row).includes(j))
.map((i) => i.specName) || [];
message.warning(`请选择必选项 ${arr.join(' ')}`);
return;
}
// 判断是否登录
if (!userInfo?.id) {
dispatch(
setGlobalData({
loginModalVisible: true,
}),
);
return;
}
// 当前的规格数据
const specItem = getSelectPriceStockItem();
// 立即购买
if (type !== 3) {
await router.push(
`/cart/submit/${detail?.id}?num=${productNum}&specId=${specItem?.id}&type=${type}`,
);
}
};
// 组件挂载
useEffect(() => {
if (!detail) return;
getLowerSpec();
......@@ -159,19 +217,25 @@ const ProductHeadView: React.FC<{ detail: DetailType }> = ({ detail }) => {
<div className="content-item flex-start">
<div className="item-label">数量</div>
<div className="item-content">
<InputNumber
<NumberBox
min={1}
max={9999}
precision={0}
value={productNum}
onChange={(e) => setProductNum(e || 1)}
onChange={(e) => setProductNum(Number(e))}
/>
</div>
</div>
<div className="content-action flex-start select-none">
<div className="action-item">加入购物车</div>
<div className="action-item">提交意向</div>
<div className="action-item">确认购买</div>
<div className="action-item" onClick={() => handleSubmit(3)}>
加入购物车
</div>
<div className="action-item" onClick={() => handleSubmit(2)}>
提交意向
</div>
<div className="action-item" onClick={() => handleSubmit(1)}>
确认购买
</div>
</div>
</div>
</ProductHeadWrap>
......
import React, { useState } from 'react';
import { Button, message, Popover } from 'antd';
import { CommonAPI } from '@/api';
const QrcodePopover: React.FC<{
children?: React.ReactNode;
text?: string;
path?: string;
}> = ({ children, text, path }) => {
QrcodePopover.defaultProps = {
path: 'pages/welcome/index',
};
// 登录二维码的地址
const [qrCodeData, setQrCodeData] = useState<string>();
// 获取登录二维码
const getQrcodeLogin = async () => {
// 获取二维码
const res = await CommonAPI.getAppletQRCode({
page: path || 'pages/welcome/index',
scene: 'type=share',
});
if (res && res.code === '200') {
if (!res.result) {
message.warning('获取登录二维码失败');
return;
}
// 设置当前登录的二维码
setQrCodeData(`data:image/png;base64,${res.result}`);
}
};
// 开启回调
const handleOpenChange = async (e: boolean) => {
if (e) {
// 如果获取成功就不再获取
if (qrCodeData) return;
// 如果开启,就获取二维码
await getQrcodeLogin();
}
};
const content = (
<div className="flex-center" style={{ flexDirection: 'column' }}>
<div style={{ width: '12.5rem', height: '12.5rem' }}>
{qrCodeData && (
<img
className="animate__animated animate__faster animate__fadeIn"
style={{ width: '12.5rem', height: '12.5rem' }}
src={qrCodeData}
alt="云享飞小程序"
/>
)}
</div>
<div style={{ marginTop: '1rem' }}>请前往小程序进行继续操作</div>
</div>
);
return (
<Popover
content={content}
trigger="focus"
placement="bottomRight"
onOpenChange={handleOpenChange}
>
{children && children}
{text && (
<Button type="link" style={{ color: '#3366cc', padding: 0, margin: 0 }}>
{text}
</Button>
)}
</Popover>
);
};
export default QrcodePopover;
import React, { useEffect, useState } from 'react';
import { EnvironmentOutlined, ReloadOutlined } from '@ant-design/icons';
import { Button, Radio, RadioChangeEvent, Space } from 'antd';
import { useDispatch, useSelector } from 'react-redux';
import styled from 'styled-components';
import { UserAPI } from '@/api';
import { InterDataType } from '@/api/interface';
import { UserAddressSelectList } from '@/api/interface/user';
import QrcodePopover from '@/components/qrcodePopover';
import { setGlobalData } from '@/store/module/globalData';
import { SystemState } from '@/store/module/system';
import { UserInfoState } from '@/store/module/userInfo';
// 列表类型
type ListType = InterDataType<UserAddressSelectList>;
const SubmitAddressView = () => {
// store
const dispatch = useDispatch();
// system
const system = useSelector((state: any) => state.system) as SystemState;
// userInfo
const userInfo = useSelector((state: any) => state.userInfo) as UserInfoState;
// 收货地址列表
const [addressList, setAddressList] = useState<ListType>();
// 当前选中的地址
const [currentValue, setCurrentValue] = useState<number>();
// 获取用户的收货地址
const getUserAddressList = async () => {
const res = await UserAPI.userAddressSelectList({
userAccountId: userInfo?.id,
});
if (res && res.code === '200') {
setAddressList(res.result || []);
// 设置默认地址
setCurrentValue(res.result?.find((i) => i.type === 0)?.id);
// 将地址列表存入store
dispatch(
setGlobalData({
userAddressList: res.result,
userAddressSelectId: res.result?.find((i) => i.type === 0)?.id,
}),
);
}
};
// 转换地址
const transformAddress = (address: string) => {
return address?.split('/');
};
// 选择地址事件
const handleSelect = (e: RadioChangeEvent) => {
setCurrentValue(e?.target?.value);
// 将地址列表存入store
dispatch(
setGlobalData({
userAddressSelectId: e?.target?.value,
}),
);
};
// 组件挂载
useEffect(() => {
if (!system?.token) {
// 请先完成登录
dispatch(
setGlobalData({
loginModalVisible: true,
}),
);
} else {
getUserAddressList().then();
}
}, [system?.token]);
return (
<SubmitAddressWrap>
<div className="address-title flex-between">
<div className="title flex-baseline">
<span>确认收货地址</span>
<Button
type="link"
icon={<ReloadOutlined />}
onClick={getUserAddressList}
>
刷新地址
</Button>
</div>
<QrcodePopover
text="管理收货地址"
path="page-mine/address-management/index"
/>
</div>
{!addressList?.length && (
<div className="address-none flex-start">
<EnvironmentOutlined style={{ color: '#ff6700', fontSize: '13px' }} />
<div className="text">
暂无地址,请打开手机端【云享飞】微信小程序,【我的】-【个人设置】-【地址管理】添加
</div>
</div>
)}
<Radio.Group value={currentValue} onChange={(e) => handleSelect(e)}>
<Space direction="vertical">
{addressList?.map((i, j) => (
<div
className={`address-item ${
i?.id === currentValue && 'item-active'
} flex-start`}
key={j}
>
<div className="label">
{i?.id === currentValue && (
<EnvironmentOutlined
style={{ color: '#ff6700', fontSize: '13px' }}
/>
)}
</div>
<Radio value={i.id}>
{transformAddress(i?.takeRegion)?.map((n, m) => (
<span className="text" key={m}>
{n}
</span>
))}
<span className="text">{i?.takeAddress}</span>
<span className="text">{i?.takeName} 收)</span>
<span className="text">{i?.takePhone}</span>
{i?.type === 0 && <span className="text">默认地址</span>}
</Radio>
</div>
))}
</Space>
</Radio.Group>
</SubmitAddressWrap>
);
};
export default SubmitAddressView;
// 样式
const SubmitAddressWrap = styled.div`
position: relative;
width: 100%;
box-sizing: border-box;
padding-bottom: 2rem;
.address-title {
position: relative;
width: 100%;
border-bottom: 2px solid #f1f1f1;
box-sizing: border-box;
padding: 1rem 0 0.5rem 0;
margin-bottom: 1rem;
.title {
font-size: 18px;
font-weight: bold;
color: #333333;
}
.action {
font-weight: 400;
color: #3366cc;
cursor: pointer;
}
}
.address-none {
position: relative;
width: 100%;
height: 2.5rem;
line-height: 2.5rem;
background: #fff1e8;
border: 0.04rem solid #ff552d;
box-sizing: border-box;
padding: 0 0.58rem;
.text {
margin-left: 0.5rem;
}
}
.address-item {
position: relative;
width: 100%;
height: 2.5rem;
box-sizing: border-box;
padding: 0 0.5rem;
.text {
margin-right: 0.3rem;
}
.ant-radio + span {
color: #666666;
}
.ant-wave-target {
margin-right: 0.3rem;
}
.label {
position: relative;
width: 4rem;
}
}
.item-active {
background: #fff1e8;
border: 0.04rem solid #ff552d;
.ant-radio + span {
color: #000;
}
.label::after {
position: absolute;
top: -0.3rem;
right: 0.5rem;
content: '寄送至';
font-size: 13px;
color: #ff552d;
}
}
.ant-radio-group,
.ant-space {
width: 100%;
}
`;
import React, { useEffect, useState } from 'react';
import Big from 'big.js';
import { useRouter } from 'next/router';
import { useDispatch } from 'react-redux';
import styled from 'styled-components';
import { InterDataType } from '@/api/interface';
import { AppMallGoodsDetails } from '@/api/interface/mall';
import NumberBox from '@/components/numberBox';
import { setGlobalData } from '@/store/module/globalData';
import { formatMoney } from '@/utils/money';
// 商品详情类型
type DetailType = InterDataType<AppMallGoodsDetails>;
const SubmitProductView: React.FC<{ detail: DetailType }> = ({ detail }) => {
// 路由钩子
const router = useRouter();
// store
const dispatch = useDispatch();
// 商品的购买数量
const [productNum, setProductNum] = useState<number>(1);
// 获取当前选择的规格
const getCurrentSpec = () => {
const { specId } = router.query;
const item = detail?.priceStock?.find((i) => i?.id === Number(specId));
// 找不到就用默认的
return item || detail?.priceStock?.at(0);
};
// 转换规格展示
const getCurrentSpecText = () => {
const productSpec = getCurrentSpec()?.productSpec || undefined;
if (!productSpec) return '请选择规格';
return Object.entries(JSON.parse(productSpec))
?.map((i) => `【${i[0]}${i[1]}`)
?.join(' +');
};
// 获取购买的价格
const getCurrentPrice = () => {
return Big(getCurrentSpec()?.salePrice || 1)
.mul(Big(productNum))
?.toFixed(2)
?.toLocaleString();
};
// 设置购买数量
const handleProductNum = (value: any) => {
setProductNum(value);
// 将购买数量存入store
dispatch(
setGlobalData({
productSpecNum: value,
}),
);
};
// 组件挂载
useEffect(() => {
// 设置购买数量
if (router?.query?.num) {
setProductNum(Number(router?.query?.num));
// 将购买数量存入store
dispatch(
setGlobalData({
productSpecNum: Number(router?.query?.num),
}),
);
}
}, [router?.query]);
return (
<SubmitProductWrap>
<div className="submit-title flex-between">
<div className="title">确认订单信息</div>
</div>
<div className="submit-td flex-start">
<div className="item">宝贝</div>
<div className="item">单价</div>
<div className="item">数量</div>
<div className="item">合计</div>
</div>
<div className="submit-tr flex-start">
<div className="item submit-product flex-start">
<img
src={detail?.resourcesList?.at(0)?.url}
alt={detail?.tradeName}
className="product-img"
/>
<div className="product-content">
<div className="title two-line-ellipsis">{detail?.tradeName}</div>
<div className="desc">已选:{getCurrentSpecText()}</div>
</div>
</div>
<div className="item">{formatMoney(getCurrentSpec()?.salePrice)}</div>
<div className="item">
<NumberBox
min={1}
max={9999}
precision={0}
value={productNum}
onChange={handleProductNum}
/>
</div>
<div className="item submit-money">
<span className="label"></span>
<span className="num">{getCurrentPrice()}</span>
</div>
</div>
</SubmitProductWrap>
);
};
export default SubmitProductView;
// 样式
const SubmitProductWrap = styled.div`
position: relative;
width: 100%;
box-sizing: border-box;
padding-bottom: 1rem;
.submit-title {
position: relative;
width: 100%;
//border-bottom: 2px solid #f1f1f1;
box-sizing: border-box;
padding: 1rem 0 0.5rem 0;
.title {
font-size: 18px;
font-weight: bold;
color: #333333;
}
.action {
font-weight: 400;
color: #3366cc;
cursor: pointer;
}
}
.submit-td,
.submit-tr {
position: relative;
width: 100%;
min-height: 2rem;
line-height: 2rem;
border-bottom: 0.02rem solid #1f8afe;
flex-wrap: nowrap;
.item {
text-align: center;
color: #666666;
font-weight: bold;
&:nth-child(1) {
width: 40%;
}
&:not(:nth-child(1)) {
width: calc((100% - 40%) / 3);
}
}
}
.submit-tr {
border: none;
.item {
display: flex;
align-items: center;
justify-content: center;
min-height: 3.33rem;
font-weight: normal;
}
.submit-product {
width: 100%;
justify-content: flex-start;
box-sizing: border-box;
padding: 0.67rem 0.5rem;
.product-img {
width: 3.33rem;
height: 3.33rem;
}
.product-content {
padding-left: 0.5rem;
text-align: left;
.title {
font-weight: bold;
color: #333333;
}
.desc {
font-size: 12px;
font-weight: 400;
color: #666666;
line-height: normal;
}
}
}
.submit-money {
font-size: 18px;
//line-height: 1;
color: #ff6700;
.label {
font-size: 14px;
}
}
}
`;
import React from 'react';
import { Input } from 'antd';
import Big from 'big.js';
import { GetServerSidePropsContext } from 'next';
import { useRouter } from 'next/router';
import { useSelector } from 'react-redux';
import styled from 'styled-components';
import { InterDataType } from '@/api/interface';
import { AppMallGoodsDetails } from '@/api/interface/mall';
import { MallAPI } from '@/api/modules/mall';
import BreadcrumbView from '@/components/breadcrumb';
import LayoutView from '@/components/layout';
import PaymentCheckout from '@/components/payment-checkout';
import SubmitAddressView from '@/components/submit-comp/submit-address';
import SubmitProductView from '@/components/submit-comp/submit-product';
import { GlobalDataState } from '@/store/module/globalData';
import { formatMoney } from '@/utils/money';
// 商品详情类型
type DetailType = InterDataType<AppMallGoodsDetails>;
// 每次加载页面都会执行
export async function getServerSideProps(context: GetServerSidePropsContext) {
// 商品id
const id: number = Number(context.params?.id);
// 商品详情
let productDetail: DetailType | undefined;
// 获取商品详情
const getMallGoodsDetails = async () => {
const res = await MallAPI.appMallGoodsDetails({ id });
if (res && res.code === '200') {
productDetail = res.result;
// console.log('获取商品详情 --->', res);
}
};
// 依次获取接口数据
await (async () => {
await getMallGoodsDetails();
})();
return { props: { id, productDetail } };
}
// 客户端组件
const MallCartSubmitView: React.FC<{
productDetail: DetailType;
}> = ({ productDetail }) => {
// 路由钩子
const router = useRouter();
// globalData
const globalData = useSelector(
(state: any) => state.globalData,
) as GlobalDataState;
// 获取当前用户选择的地址
const getUserAddressCurrent = () => {
const item = globalData?.userAddressList?.find(
(i) => i.id === globalData?.userAddressSelectId,
);
return {
...item,
takeRegion: item?.takeRegion?.split('/')?.join(' '),
};
};
// 获取商品的结算价格
const getCheckoutPrice = () => {
const item = router?.query?.specId
? productDetail?.priceStock?.find(
(i) => i.id === Number(router?.query?.specId),
)
: productDetail?.priceStock?.at(0);
const salePrice = Big(item?.salePrice || 0);
const productSpecNum = Big(globalData?.productSpecNum || 1);
return salePrice.mul(productSpecNum)?.toNumber();
};
return (
<LayoutView>
<MallCartSubmitWrap>
<BreadcrumbView />
<SubmitAddressView />
<SubmitProductView detail={productDetail} />
<div className="submit-remark flex-start">
<div className="remark-view flex-start">
<div className="label">备注:</div>
<div className="input">
<Input.TextArea
placeholder={'请输入备注'}
maxLength={50}
showCount
/>
</div>
</div>
</div>
<div className="submit-checkout flex-start">
<div className="checkout-view">
<div className="checkout-item checkout-price flex-end">
<div className="label">实付款:</div>
<div className="unit"></div>
<div className="price">{formatMoney(getCheckoutPrice())}</div>
</div>
<div className="checkout-item flex-end">
<div className="label">寄送至:</div>
<div className="text">
{getUserAddressCurrent()?.takeRegion}{' '}
{getUserAddressCurrent()?.takeAddress}
</div>
</div>
<div className="checkout-item item-last flex-end">
<div className="label">收货人:</div>
<div className="text">
{getUserAddressCurrent()?.takeName}{' '}
{getUserAddressCurrent()?.takePhone}
</div>
</div>
<PaymentCheckout cost={getCheckoutPrice()} />
</div>
<div className="checkout-submit select-none cursor-pointer">
提交订单
</div>
</div>
</MallCartSubmitWrap>
</LayoutView>
);
};
export default MallCartSubmitView;
// 样式
const MallCartSubmitWrap = styled.div`
position: relative;
max-width: 1190px;
box-sizing: border-box;
padding: 2rem 0 0 0;
margin: 0 auto;
.submit-remark {
position: relative;
width: 100%;
min-height: 4.5rem;
background: #f2f7ff;
border: 0.04rem solid #d0eaf5;
padding: 0.67rem;
margin-bottom: 1rem;
.remark-view {
position: relative;
height: 100%;
align-items: flex-start;
.label {
font-weight: bold;
color: #333333;
}
.input {
min-height: 4rem;
width: 25rem;
}
}
}
.submit-checkout {
position: relative;
width: 100%;
padding-bottom: 2rem;
box-sizing: border-box;
align-items: flex-end;
flex-direction: column;
.checkout-view {
position: relative;
width: 25rem;
min-height: 8rem;
border: 1px solid #ff5001;
box-sizing: border-box;
display: flex;
align-items: flex-end;
justify-content: flex-start;
flex-direction: column;
padding: 0.5rem 0.67rem 1rem 0.67rem;
.checkout-item {
align-items: baseline;
margin-bottom: 0.33rem;
.label {
font-weight: bold;
color: #333333;
}
.unit {
color: #ff6700;
font-size: 16px;
}
.price {
color: #ff6700;
font-size: 24px;
font-weight: bold;
}
.text {
max-width: 86%;
}
}
.item-last {
margin-bottom: 0.66rem;
}
}
.checkout-submit {
position: relative;
width: 8.58rem;
height: 2.63rem;
background: #ff552d;
line-height: 2.63rem;
border: 0.04rem solid #ff5001;
text-align: center;
font-size: 16px;
color: #ffffff;
box-sizing: border-box;
&:active {
filter: brightness(0.9);
}
}
}
`;
// user.ts
import { createSlice } from '@reduxjs/toolkit';
import { HYDRATE } from 'next-redux-wrapper';
import { InterDataType } from '@/api/interface';
import { UserAddressSelectList } from '@/api/interface/user';
export type GlobalDataState = {
loginModalVisible: boolean;
userAddressList?: InterDataType<UserAddressSelectList>;
userAddressSelectId?: number;
productSpecNum?: number;
};
const initialState: GlobalDataState = {
loginModalVisible: false,
userAddressList: undefined,
userAddressSelectId: undefined,
productSpecNum: undefined,
};
const globalDataSlice = createSlice({
......@@ -15,13 +23,14 @@ const globalDataSlice = createSlice({
initialState,
reducers: {
setGlobalData: (state, action) => {
return action.payload;
return { ...state, ...action.payload };
},
},
extraReducers: {
// hydrated 用于获取服务端注入的state并选择更新
[HYDRATE]: (state, action) => {
return {
...state,
...action.payload.globalData,
};
},
......
......@@ -7,9 +7,9 @@ const themeConfig: ThemeConfig = {
},
components: {
Button: {
colorLink: '#ffffff',
colorLink: 'rgba(255,85,45,0.8)',
colorLinkActive: '#ff552d',
colorLinkHover: '#ff552d',
colorLinkHover: '#e1251b',
contentFontSize: 12,
},
},
......
import { WalletAPI } from '@/api';
import { InterDataType } from '@/api/interface';
import { UserPayWalletInfoType } from '@/api/interface/wallet';
// 钱包数据类型
export type CurrentUserPayWalletInfoType = InterDataType<UserPayWalletInfoType>;
// 获取当前用户的钱包信息
const getCurrentUserPayWalletInfo = (): Promise<CurrentUserPayWalletInfoType> =>
new Promise((resolve, reject) => {
WalletAPI.getUserPayWalletInfo({})
.then((res) => {
if (res && res.code === '200') {
resolve(res.result);
} else {
reject(res.result);
}
})
.catch((err) => {
reject(err);
});
});
export default getCurrentUserPayWalletInfo;
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论