更新:课程确认订单和订单支付功能初步完成

This commit is contained in:
2025-11-24 17:15:28 +08:00
parent b357225703
commit bfe0c09242
16 changed files with 2022 additions and 63 deletions

View File

@@ -4,7 +4,11 @@ import type { IApiResponse } from '@/api/types'
import type {
ICreateOrderParams,
IGooglePayVerifyParams,
ICreateOrderResponse
ICreateOrderResponse,
IOrderGoods,
ICoupon,
ICourseOrderCreateParams,
IOrderInitData
} from '@/types/order'
import type { IUserInfo } from '@/types/user'
@@ -59,5 +63,96 @@ export const orderApi = {
method: 'POST'
})
return res
},
/**
* 初始化订单准备数据
* @param params 初始化参数
*/
async initPrepareOrder(params: { uid: number; productList: Array<{ productId: number; quantity: number }> }) {
const res = await mainClient.request<IApiResponse<IOrderInitData>>({
url: 'common/buyOrder/initPrepareOrder',
method: 'POST',
data: params
})
return res
},
/**
* 根据商品ID获取商品详情列表
* @param productIds 商品ID字符串逗号分隔
*/
async getShopProductListByIds(productIds: string) {
const res = await mainClient.request<IApiResponse<{ shopProductList: IOrderGoods[] }>>({
url: 'book/buyOrder/getShopProductListByIds',
method: 'POST',
data: { productIds }
})
return res
},
/**
* 获取VIP优惠金额
* @param productList 商品列表
*/
async getVipDiscountAmount(productList: Array<{ productId: number; quantity: number }>) {
const res = await mainClient.request<IApiResponse<{ discountAmount: number }>>({
url: 'book/buyOrder/getVipDiscountAmount',
method: 'POST',
data: { productList }
})
return res
},
/**
* 获取地区优惠金额
* @param productList 商品列表
*/
async getDistrictAmount(productList: Array<{ productId: number; quantity: number }>) {
const res = await mainClient.request<IApiResponse<{ districtAmount: number }>>({
url: 'book/buyOrder/getDistrictAmount',
method: 'POST',
data: { productList }
})
return res
},
/**
* 获取可用优惠券列表
* @param shopProductInfos 商品信息字符串格式productId:price:quantity,productId:price:quantity
*/
async getCouponListPayment(shopProductInfos: string) {
const res = await mainClient.request<IApiResponse<{ couponHistoryList: ICoupon[] }>>({
url: 'common/coupon/getCouponListPayment',
method: 'POST',
data: { shopProductInfos }
})
return res
},
/**
* 更新购物车商品
* @param data 商品数据
*/
async updateCart(data: { productId: number; productAmount: number; price: number }) {
const res = await mainClient.request<IApiResponse>({
url: 'book/ordercart/update',
method: 'POST',
data
})
return res
},
/**
* 创建课程订单
* @param data 订单数据
*/
async placeCourseOrder(data: ICourseOrderCreateParams) {
const res = await mainClient.request<IApiResponse<{ orderSn: string; money: number }>>({
url: 'book/buyOrder/placeOrder',
method: 'POST',
data
})
return res
}
}

View File

@@ -39,11 +39,11 @@ export async function getVipInfo() {
* @param current 当前页码
* @param limit 每页数量
*/
export async function getOrderList(current: number, limit: number, orderStatus: string) {
export async function getOrderList(page: number, limit: number, orderStatus: string) {
const res = await mainClient.request<IApiResponse<{ orders: IPageData<IOrder> }>>({
url: 'common/buyOrder/commonBuyOrderList',
method: 'POST',
data: { current, limit, orderStatus, come: '10', userId: uni.getStorageSync('userInfo').id }
data: { page, limit, orderStatus, come: '10', userId: uni.getStorageSync('userInfo').id }
})
return res
}

View File

@@ -31,7 +31,7 @@
<!-- VIP优惠价 -->
<view v-if="item.isVipPrice === 1 && item.vipPrice" class="price-info">
<text class="vip-price">{{ parseFloat(item.vipPrice).toFixed(2) }} 天医币</text>
<text class="vip-label">VIP到手</text>
<text class="vip-label">VIP优惠</text>
<text class="original-price">{{ parseFloat(item.price).toFixed(2) }} 天医币</text>
</view>

View File

229
components/order/Price.vue Normal file
View File

@@ -0,0 +1,229 @@
<template>
<view class="">
<!-- 商品总价 -->
<!-- <view class="price-item">
<text class="label">{{ $t('order.totalPrice') }}</text>
<text class="value">{{ totalPrice.toFixed(2) }}</text>
</view> -->
<!-- 优惠券 -->
<!-- <view class="price-item" @click="showCouponPopup = true">
<view class="label-row">
<text class="label">{{ $t('order.coupon') }}</text>
</view>
<view class="value-row">
<template v-if="!selectedCoupon">
<view v-if="availableCouponCount > 0" class="coupon-badge">
<text>{{ $t('order.couponCount', { count: availableCouponCount }) }}</text>
</view>
<text v-else-if="couponList.length === 0" class="unavailable-text">
{{ $t('order.noCoupon') }}
</text>
<text v-else class="unavailable-text">
{{ $t('order.unavailable') }}
</text>
</template>
<template v-else>
<text class="discount-value">-{{ selectedCoupon.couponEntity.couponAmount }}</text>
<text class="reselect-btn">{{ $t('order.reselect') }}</text>
</template>
<image
v-if="availableCouponCount > 0 || selectedCoupon"
src="/static/icon/icon_right.png"
class="arrow-icon"
/>
</view>
</view> -->
<!-- 活动立减 -->
<view v-if="hasActivityDiscount" class="price-item">
<text class="label">{{ $t('order.activityDiscount') }}</text>
<text class="discount-value">-{{ activityDiscountAmount.toFixed(2) }}</text>
</view>
<!-- VIP专享立减 -->
<view v-if="vipPrice > 0" class="price-item">
<view class="label-row">
<text class="vip-icon">VIP</text>
<text class="label">{{ $t('order.vipDiscount') }}</text>
</view>
<text class="discount-value">-{{ vipPrice.toFixed(2) }}</text>
</view>
<!-- 地区优惠 -->
<view v-if="districtAmount > 0" class="price-item">
<text class="label">{{ $t('order.districtDiscount') }}</text>
<text class="discount-value">-{{ districtAmount.toFixed(2) }}</text>
</view>
<!-- 积分 -->
<view v-if="initData && initData.user.jf > 0" class="price-item">
<view class="label-row">
<image src="/static/icon/jifen.png" class="icon-img" />
<text class="label">{{ $t('order.points') }}</text>
<text class="points-total">
({{ $t('order.allPoints') }}{{ initData.user.jf }})
</text>
</view>
<text class="discount-value">-{{ jfNumberShow }}</text>
</view>
<!-- 积分输入 -->
<view v-if="initData && initData.user.jf > 0" class="points-input-section">
<text class="points-label">
{{ $t('order.maxPoints', { max: jfNumberMax }) }}
</text>
<view class="points-input-box">
<wd-input
v-model="jfNumber"
type="number"
:placeholder="$t('order.pointsPlaceholder')"
@input="handlePointsInput"
@clear="handlePointsClear"
clearable
/>
</view>
</view>
<wd-cell-group border class="p-0!">
<wd-cell title="商品总价" :value="`${totalPrice.toFixed(2)}天医币`" />
<wd-cell title="优惠券" :value="`-${selectedCoupon?.couponEntity?.couponAmount || 0}天医币`" />
<wd-cell title="活动立减" :value="`-${selectedCoupon?.couponEntity?.couponAmount || 0}天医币`" />
<wd-cell :value="`-${vipPrice.toFixed(2)}天医币`">
<template #title><text class="text-[#f94f04] font-bold">VIP</text>专项立减</template>
</wd-cell>
<wd-cell title="地区优惠" :value="`-${selectedCoupon?.couponEntity?.couponAmount || 0}天医币`" />
</wd-cell-group>
</view>
</template>
<script lang="ts" setup>
const props = defineProps({
value: {
type: Number,
default: 0
},
totalPrice: {
type: Number,
default: 0
},
vipPrice: {
type: Number,
default: 0
}
})
</script>
<style lang="scss" scoped>
:deep(.wd-cell__wrapper) {
padding: 5px 0;
.wd-cell__title {
color: #666;
}
}
.price-section {
.price-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15rpx 0;
font-size: 28rpx;
.label-row {
display: flex;
align-items: center;
gap: 10rpx;
.label {
color: #666;
}
.vip-icon {
padding: 2rpx 8rpx;
background-color: #f94f04;
color: #fff;
font-size: 20rpx;
border-radius: 4rpx;
font-weight: bold;
}
.icon-img {
width: 40rpx;
height: 40rpx;
}
.points-total {
font-size: 24rpx;
color: #aaa;
}
}
.label {
color: #666;
}
.value {
color: #333;
font-weight: 500;
}
.value-row {
display: flex;
align-items: center;
gap: 10rpx;
.coupon-badge {
padding: 4rpx 20rpx;
background-color: #fceeeb;
color: #ec4729;
font-size: 24rpx;
border-radius: 10rpx;
}
.unavailable-text {
font-size: 24rpx;
color: #999;
}
.reselect-btn {
padding: 4rpx 12rpx;
background-color: #fe6035;
color: #fff;
font-size: 24rpx;
border-radius: 30rpx;
}
.arrow-icon {
width: 24rpx;
height: 24rpx;
}
}
.discount-value {
color: #fe6035;
font-weight: 500;
}
}
.points-input-section {
display: flex;
align-items: center;
justify-content: space-between;
padding: 15rpx 0;
margin-top: 10rpx;
background-color: #f5f5f5;
border-radius: 20rpx;
padding: 20rpx;
.points-label {
font-size: 28rpx;
color: #7dc1f0;
font-weight: 600;
}
.points-input-box {
width: 320rpx;
}
}
}
</style>

View File

@@ -412,6 +412,66 @@
},
"order": {
"selectFuduScheme": "Select Fudu Scheme",
"selectPurchaseScheme": "Select Purchase Scheme"
"selectPurchaseScheme": "Select Purchase Scheme",
"confirmTitle": "Confirm Order",
"goodsInfo": "Product Information",
"priceDetail": "Price Details",
"totalPrice": "Subtotal",
"coupon": "Coupon",
"points": "Points",
"vipDiscount": "VIP Exclusive Discount",
"activityDiscount": "Activity Discount",
"districtDiscount": "Regional Discount",
"actualPayment": "Total",
"paymentMethod": "Payment Method",
"virtualCoin": "Virtual Coin",
"balance": "Balance",
"recharge": "Recharge Now",
"remark": "Order Remark",
"remarkPlaceholder": "Optional: Leave a message",
"remarkTitle": "Order Remark",
"submit": "Pay Now",
"submitting": "Submitting...",
"total": "Total",
"noCoupon": "No coupons available",
"unavailable": "Unavailable",
"selectCoupon": "Please select a coupon",
"notUseCoupon": "Don't use coupon",
"reselect": "Reselect",
"selected": "Confirm",
"maxPoints": "Available Points ({max} pts)",
"pointsPlaceholder": "Enter points",
"allPoints": "Total Points",
"insufficientBalance": "Insufficient virtual coin balance",
"orderCreating": "Creating order",
"orderSuccess": "Purchase successful",
"orderFailed": "Failed, please try again",
"tooFrequent": "Too frequent, please wait",
"duplicateOrder": "You have a similar order recently. Continue?",
"duplicateConfirm": "Continue",
"duplicateCancel": "Cancel",
"customerService": "Customer Service",
"paymentTip": "1 Virtual Coin = 1 CNY",
"paymentTipTitle": "Notes",
"paymentTip1": "1. 1 Virtual Coin = 1 CNY",
"paymentTip2": "2. For questions, please call customer service",
"paymentTip3": "3. Non-mainland China users can pay by credit card. Simple and fast, recommended! Credit cards with Visa or MasterCard logos are accepted. Please send payment request to email",
"paymentTip3_1": "(click to copy) with course name, amount, registered name and phone number, or add WeChat customer service (",
"paymentTip3_2": ") (click to copy). We will send payment link within 24 hours.",
"ensureBalance": "Ensure sufficient virtual coin balance",
"vipLabel": "VIP Discount",
"activityLabel": "Activity Price",
"vipPriceLabel": "VIP Price",
"quantity": "Quantity",
"couponAmount": "¥",
"couponUseLevel": "Min. {level} CNY",
"couponExpiry": "Valid until",
"couponForever": "Permanent",
"couponReason": "Reason",
"couponUsage": "Usage",
"couponType0": "All Products",
"couponType1": "Specific Courses",
"couponType2": "Course Categories",
"couponCount": "{count} coupons"
}
}

View File

@@ -413,6 +413,66 @@
},
"order": {
"selectFuduScheme": "选择复读方案",
"selectPurchaseScheme": "选择购买方案"
"selectPurchaseScheme": "选择购买方案",
"confirmTitle": "确认订单",
"goodsInfo": "商品信息",
"priceDetail": "价格明细",
"totalPrice": "商品总价",
"coupon": "优惠券",
"points": "积分",
"vipDiscount": "VIP专享立减",
"activityDiscount": "活动立减",
"districtDiscount": "地区优惠",
"actualPayment": "实付款",
"paymentMethod": "支付方式",
"virtualCoin": "天医币",
"balance": "余额",
"recharge": "立即充值",
"remark": "订单备注",
"remarkPlaceholder": "选填:给商家留言",
"remarkTitle": "订单备注",
"submit": "立即支付",
"submitting": "提交中...",
"total": "合计",
"noCoupon": "暂无可用优惠券",
"unavailable": "不可用",
"selectCoupon": "请选择优惠券",
"notUseCoupon": "不使用优惠券",
"reselect": "重新选择",
"selected": "选好了",
"maxPoints": "可用积分({max}分)",
"pointsPlaceholder": "请输入积分",
"allPoints": "全部积分",
"insufficientBalance": "天医币余额不足",
"orderCreating": "正在请求订单",
"orderSuccess": "购买成功",
"orderFailed": "失败,请重新下单",
"tooFrequent": "操作太频繁了,休息下吧",
"duplicateOrder": "您短时间内有一笔相同金额的订单,是否确定继续下单?",
"duplicateConfirm": "继续操作",
"duplicateCancel": "点错了",
"customerService": "客服",
"paymentTip": "1天医币 = 1元人民币",
"paymentTipTitle": "说明",
"paymentTip1": "1. 1天医币 = 1元人民币",
"paymentTip2": "2.若有疑问或意见请致电客服",
"paymentTip3": "3.非中国大陆用户可以信用卡支付。简单快捷推荐使用支付时使用的信用卡需要带有Visa或MasterCard的标识。请向邮箱",
"paymentTip3_1": "点击复制发送支付请求内容需包含拟购买的课程名称、支付金额、APP注册姓名及手机号码或者加一路健康客服微信",
"paymentTip3_2": "点击复制联系我们我们将在24小时内向您的邮箱或者微信发送支付链接根据提示即可完成信用卡支付无需兑换外币。",
"ensureBalance": "确保您的天医币足够支付",
"vipLabel": "VIP优惠",
"activityLabel": "活动价",
"vipPriceLabel": "VIP到手价",
"quantity": "数量",
"couponAmount": "¥",
"couponUseLevel": "满{level}元可用",
"couponExpiry": "有效期至",
"couponForever": "永久有效",
"couponReason": "不可用原因",
"couponUsage": "使用说明",
"couponType0": "全场通用",
"couponType1": "指定课程可用",
"couponType2": "指定课程品类可用",
"couponCount": "共 {count} 张"
}
}

View File

@@ -152,10 +152,10 @@
"navigationBarTitleText": "%courseDetails.chapter%"
}
}, {
"path": "pages/course/order",
"path": "pages/order/confirmOrder",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "%courseOrder.orderTitle%"
"navigationBarTitleText": "%order.confirmTitle%"
}
}
],

View File

@@ -449,8 +449,6 @@ const closeGoodsSelector = () => {
*/
const confirmPurchase = () => {
showProtocol.value = false
uni.showToast({ icon: 'none', title: '订单及支付功能开发中' })
return false
if (!selectedGoods.value) return
showProtocol.value = false
@@ -458,15 +456,16 @@ const confirmPurchase = () => {
// 跳转到确认订单页
const orderData = {
goods: [{ ...selectedGoods.value, productAmount: 1 }],
typeId: 0,
navTitle: courseDetail.value?.title,
title: courseDetail.value?.title,
isFudu: isFudu.value,
fuduId: isFudu.value ? fuduCatalogueId.value : undefined
// typeId: 0,
// navTitle: courseDetail.value?.title,
// title: courseDetail.value?.title,
// isFudu: isFudu.value,
// fuduId: isFudu.value ? fuduCatalogueId.value : undefined
}
uni.navigateTo({
url: `/pages/course/order?data=${encodeURIComponent(JSON.stringify(orderData))}`
// url: `/pages/order/confirmOrder?data=${encodeURIComponent(JSON.stringify(orderData))}`
url: `/pages/order/confirmOrder?goods=${selectedGoods.value.productId}`
})
}

File diff suppressed because it is too large Load Diff

BIN
static/icon/jifen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

BIN
static/icon/pay_3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -10,6 +10,7 @@
--color-red-500: oklch(63.7% 0.237 25.331);
--color-white: #fff;
--spacing: 0.25rem;
--font-weight-bold: 700;
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
--default-transition-duration: 150ms;
--default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
@@ -293,15 +294,40 @@
.bg-red-500 {
background-color: var(--color-red-500);
}
.p-0 {
padding: calc(var(--spacing) * 0);
}
.p-0\! {
padding: calc(var(--spacing) * 0) !important;
}
.pt-\[40px\] {
padding-top: 40px;
}
.pb-0 {
padding-bottom: calc(var(--spacing) * 0);
}
.pb-0\! {
padding-bottom: calc(var(--spacing) * 0) !important;
}
.text-center {
text-align: center;
}
.text-right {
text-align: right;
}
.font-bold {
--tw-font-weight: var(--font-weight-bold);
font-weight: var(--font-weight-bold);
}
.text-\[\#000\] {
color: #000;
}
.text-\[\#7dc1f0\] {
color: #7dc1f0;
}
.text-\[\#f94f04\] {
color: #f94f04;
}
.text-\[\#fff\] {
color: #fff;
}
@@ -393,6 +419,10 @@
inherits: false;
initial-value: solid;
}
@property --tw-font-weight {
syntax: "*";
inherits: false;
}
@property --tw-ordinal {
syntax: "*";
inherits: false;
@@ -585,6 +615,7 @@
--tw-skew-x: initial;
--tw-skew-y: initial;
--tw-border-style: solid;
--tw-font-weight: initial;
--tw-ordinal: initial;
--tw-slashed-zero: initial;
--tw-numeric-figure: initial;

View File

@@ -1,10 +1,10 @@
:root,
page {
/* 文字字号 */
--wot-fs-title: 18px; // 标题字号/重要正文字号
--wot-fs-content: 16px; // 普通正文
--wot-fs-secondary: 14px; // 次要信息,注释/补充/正文
--wot-fs-tertiary: 12px; // 次次要信息,注释/补充/正文
--wot-fs-title: 36rpx; // 标题字号/重要正文字号
--wot-fs-content: 28rpx; // 普通正文
--wot-fs-secondary: 24rpx; // 次要信息,注释/补充/正文
--wot-fs-tertiary: 20rpx; // 次次要信息,注释/补充/正文
// 导航栏
--wot-navbar-background: #fff;
@@ -16,7 +16,7 @@ page {
// --wot-tabbar-item-title-font-size: 16px;
// cell
--wot-cell-title-fs: 16px;
--wot-cell-title-fs: 28rpx;
// dropdown
// --wot-drop-menu-selected-color: #5355C8;

148
types/order.d.ts vendored
View File

@@ -26,61 +26,127 @@ export interface IOrder {
}
/**
* 订单创建参数接口
* 课程订单商品信息
*/
export interface ICreateOrderParams {
paymentMethod: '4' | '5' // 支付方式: 4-虚拟币, 5-Google Pay
orderMoney: number | string // 订单金额
abroadBookId: number // 图书ID
orderType: 'abroadBook' // 订单类型
export interface IOrderGoods {
productId: number
productName: string
productImages: string
price: number
vipPrice: number | null
activityPrice: number | null
isVipPrice: number // 是否有VIP优惠 0-否 1-是
productAmount: number // 购买数量
goodsType: string // 商品类型 "05" for course
}
/**
* Google Pay 验证参数接口
* 优惠券实体信息
*/
export interface IGooglePayVerifyParams {
purchaseToken: string // 购买凭证
orderSn: string // 订单号
productId: string // 产品ID
export interface ICouponEntity {
id: number
couponName: string
couponAmount: number
useLevel: number // 满多少可用
couponRange: number // 0-全场 1-指定课程 2-指定品类
remark?: string
}
/**
* 订单创建响应接口
* 优惠券信息
*/
export interface ICreateOrderResponse {
code: number
orderSn: string // 订单号
msg?: string
export interface ICoupon {
id: number
couponId: number
canUse: number // 0-不可用 1-可用
canUseReason?: string
effectType: number // 0-永久有效
endTime?: string
couponEntity: ICouponEntity
}
/**
* Google Pay SKU 信息接口
* 课程订单创建参数
*/
export interface IGooglePaySku {
productId: string
type: string
price: string
price_amount_micros: number
price_currency_code: string
title: string
description: string
}
/**
* Google Pay 支付结果接口
*/
export interface IGooglePayResult {
code: number
data?: Array<{
original: {
purchaseToken: string
orderId: string
packageName: string
productId: string
purchaseTime: number
purchaseState: number
}
export interface ICourseOrderCreateParams {
buyType: number // 0-商品页直接下单 1-购物车结算
userId: number
paymentMethod: number // 4-天医币
orderMoney: number // 订单金额
realMoney: number // 实收金额
jfDeduction: number // 积分抵扣
couponId?: number // 优惠券ID
couponName?: string // 优惠券名称
vipDiscountAmount: number // VIP折扣金额
districtMoney: number // 地区优惠金额
remark?: string // 备注
productList: Array<{
productId: number
quantity: number
}>
orderType: string // "order"
addressId: number // 0 for course products
appName: string // "wumen"
come: number // 2
}
/**
* 订单初始化数据
*/
export interface IOrderInitData {
user: {
id: number
jf: number // 积分
peanutCoin: number // 天医币余额
vip?: number // VIP状态
}
is_course: boolean
}
/**
* 订单路由参数
*/
export interface IOrderRouteParams {
goods: IOrderGoods[]
typeId: number
sourceType?: string
navTitle?: string
title?: string
}
/**
* 价格明细项
*/
export interface IPriceBreakdownItem {
type: number // 1-商品总价 2-运费 3-优惠券 4-积分 5-活动立减 6-VIP立减
text: string
imgUrl?: string
icon?: string
}
/**
* 订单状态
*/
export interface IOrderState {
orderData: IOrderRouteParams
goodsList: IOrderGoods[]
initData: IOrderInitData | null
totalPrice: number
vipPrice: number
districtAmount: number
actualPayment: number
jfNumber: number
jfNumberMax: number
jfNumberShow: string
couponList: ICoupon[]
selectedCoupon: ICoupon | null
showCouponPopup: boolean
remark: string
showRemarkPopup: boolean
payType: number
loading: boolean
submitting: boolean
buyingFlag: boolean
}
/**

View File

@@ -18,7 +18,7 @@ export const onPageBack = () => {
/**
* 拨打电话
*/
export const makePhoneCall = (phoneNumber: string, title: string) => {
export const makePhoneCall = (phoneNumber: string, title: string = '') => {
uni.showModal({
title: title,
content: phoneNumber,