forked from wangziqi/gongxue-base
feat: add student checkout pages
This commit is contained in:
3
apps/taro/src/pages/student/checkout/index.config.ts
Normal file
3
apps/taro/src/pages/student/checkout/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '会员收银台',
|
||||
});
|
||||
269
apps/taro/src/pages/student/checkout/index.tsx
Normal file
269
apps/taro/src/pages/student/checkout/index.tsx
Normal file
@@ -0,0 +1,269 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import Taro, { useRouter } from '@tarojs/taro';
|
||||
import { Button, Input, Text, View } from '@tarojs/components';
|
||||
import {
|
||||
claimCoupon,
|
||||
createOrder,
|
||||
createPayment,
|
||||
loadOrderStatus,
|
||||
loadSvipPlans,
|
||||
type CouponClaimResult,
|
||||
type OrderDetail,
|
||||
type PaymentCreateResult,
|
||||
type SvipPlan,
|
||||
} from '@/services/commerce';
|
||||
import { loadProfile, type StudentProfile } from '@/services/profile';
|
||||
import '../student.css';
|
||||
|
||||
type PayProvider = 'alipay' | 'wechat_pay' | 'manual';
|
||||
|
||||
const providerLabels: Record<PayProvider, string> = {
|
||||
alipay: '支付宝',
|
||||
wechat_pay: '微信支付',
|
||||
manual: '线下支付',
|
||||
};
|
||||
|
||||
function centsToYuan(value?: number | null) {
|
||||
return ((value || 0) / 100).toFixed(2);
|
||||
}
|
||||
|
||||
function planPriceCents(plan?: SvipPlan | null) {
|
||||
if (!plan) return 0;
|
||||
if (typeof plan.priceCents === 'number') return plan.priceCents;
|
||||
if (typeof plan.price === 'number') return Math.round(plan.price * 100);
|
||||
return 0;
|
||||
}
|
||||
|
||||
function encodeQuery(input: Record<string, string | undefined>) {
|
||||
return Object.entries(input)
|
||||
.filter(([, value]) => value)
|
||||
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value || '')}`)
|
||||
.join('&');
|
||||
}
|
||||
|
||||
function buildPaymentUrl(result?: PaymentCreateResult) {
|
||||
const params = result?.paymentParams || {};
|
||||
const directUrl = params.url || params.payUrl || params.paymentUrl || params.qrCodeUrl;
|
||||
if (typeof directUrl === 'string' && directUrl) return directUrl;
|
||||
const gateway = params.gateway;
|
||||
const query = params.query;
|
||||
if (typeof gateway === 'string' && query && typeof query === 'object' && !Array.isArray(query)) {
|
||||
const queryString = encodeQuery(query as Record<string, string | undefined>);
|
||||
return queryString ? `${gateway}?${queryString}` : gateway;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function tryOpenPaymentUrl(url: string) {
|
||||
if (!url) return false;
|
||||
if (process.env.TARO_ENV === 'h5' && typeof window !== 'undefined') {
|
||||
window.location.href = url;
|
||||
return true;
|
||||
}
|
||||
Taro.setClipboardData({ data: url });
|
||||
return true;
|
||||
}
|
||||
|
||||
export default function StudentCheckoutPage() {
|
||||
const router = useRouter();
|
||||
const params = router.params || {};
|
||||
const [profile, setProfile] = useState<StudentProfile | null>(null);
|
||||
const [plans, setPlans] = useState<SvipPlan[]>([]);
|
||||
const [selectedPlanId, setSelectedPlanId] = useState(params.planId || '');
|
||||
const [provider, setProvider] = useState<PayProvider>(params.provider === 'wechat_pay' ? 'wechat_pay' : params.provider === 'manual' ? 'manual' : 'alipay');
|
||||
const [couponCode, setCouponCode] = useState(String(params.couponCode || ''));
|
||||
const [coupon, setCoupon] = useState<CouponClaimResult | null>(null);
|
||||
const [order, setOrder] = useState<OrderDetail | null>(null);
|
||||
const [payment, setPayment] = useState<PaymentCreateResult | null>(null);
|
||||
const [busy, setBusy] = useState('');
|
||||
const [message, setMessage] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
loadProfile().catch(() => ({ item: null })),
|
||||
loadSvipPlans(String(params.regionId || '') || undefined).catch(() => ({ items: [] })),
|
||||
])
|
||||
.then(([profilePayload, planPayload]) => {
|
||||
const nextPlans = planPayload.items || [];
|
||||
setProfile(profilePayload.item || null);
|
||||
setPlans(nextPlans);
|
||||
if (!selectedPlanId && nextPlans[0]) setSelectedPlanId(nextPlans[0].id);
|
||||
})
|
||||
.catch(nextError => setError(nextError instanceof Error ? nextError.message : '收银台加载失败'));
|
||||
}, []);
|
||||
|
||||
const selectedPlan = useMemo(() => plans.find(item => item.id === selectedPlanId) || null, [plans, selectedPlanId]);
|
||||
const regionId = String(params.regionId || profile?.target?.regionId || selectedPlan?.regionId || '');
|
||||
const discountCents = coupon?.coupon?.discountCents || 0;
|
||||
const estimateAmountCents = Math.max(0, planPriceCents(selectedPlan) - discountCents);
|
||||
const paymentUrl = buildPaymentUrl(payment || undefined);
|
||||
|
||||
async function handleClaimCoupon() {
|
||||
if (!couponCode.trim()) {
|
||||
setError('请先输入优惠券码。');
|
||||
return;
|
||||
}
|
||||
if (!selectedPlanId) {
|
||||
setError('请先选择会员套餐。');
|
||||
return;
|
||||
}
|
||||
setBusy('coupon');
|
||||
setError('');
|
||||
setMessage('');
|
||||
try {
|
||||
const payload = await claimCoupon({ code: couponCode.trim(), planId: selectedPlanId, regionId: regionId || undefined });
|
||||
setCoupon(payload);
|
||||
setMessage(`优惠券已领取,可抵扣 ¥${centsToYuan(payload.coupon?.discountCents || 0)}`);
|
||||
} catch (nextError) {
|
||||
setError(nextError instanceof Error ? nextError.message : '优惠券领取失败');
|
||||
} finally {
|
||||
setBusy('');
|
||||
}
|
||||
}
|
||||
|
||||
async function pollStatus(orderNo: string) {
|
||||
const payload = await loadOrderStatus(orderNo);
|
||||
if (payload.item?.status === 'paid') {
|
||||
setOrder(prev => prev ? { ...prev, status: 'paid', paidAt: payload.item?.paidAt || prev.paidAt } : prev);
|
||||
setMessage('订单已支付,会员权益已由后端开通。');
|
||||
return;
|
||||
}
|
||||
setMessage(`当前订单状态:${payload.item?.status || '未知'},如已完成支付请稍后刷新。`);
|
||||
}
|
||||
|
||||
async function handleCreateOrder() {
|
||||
if (!selectedPlan) {
|
||||
setError('请选择会员套餐。');
|
||||
return;
|
||||
}
|
||||
setBusy('order');
|
||||
setError('');
|
||||
setMessage('');
|
||||
setPayment(null);
|
||||
try {
|
||||
const orderPayload = await createOrder({
|
||||
planId: selectedPlan.id,
|
||||
regionId: regionId || undefined,
|
||||
payProvider: provider,
|
||||
payMethod: provider === 'alipay' ? 'wap' : provider === 'wechat_pay' ? 'jsapi' : 'manual',
|
||||
couponCode: couponCode.trim() || undefined,
|
||||
couponRedemptionId: coupon?.redemption?.id,
|
||||
});
|
||||
const nextOrder = orderPayload.item || null;
|
||||
setOrder(nextOrder);
|
||||
if (!nextOrder?.orderNo) {
|
||||
setError('订单创建结果缺少订单号。');
|
||||
return;
|
||||
}
|
||||
if (nextOrder.status === 'paid') {
|
||||
setMessage('订单已支付,会员权益已开通。');
|
||||
return;
|
||||
}
|
||||
if (nextOrder.amountCents === 0) {
|
||||
await pollStatus(nextOrder.orderNo);
|
||||
return;
|
||||
}
|
||||
const paymentPayload = await createPayment({
|
||||
orderNo: nextOrder.orderNo,
|
||||
provider,
|
||||
returnUrl: process.env.TARO_ENV === 'h5' && typeof window !== 'undefined' ? window.location.href : undefined,
|
||||
quitUrl: process.env.TARO_ENV === 'h5' && typeof window !== 'undefined' ? window.location.href : undefined,
|
||||
});
|
||||
setPayment(paymentPayload.item || null);
|
||||
setMessage(provider === 'manual' ? '已生成线下支付记录,请联系教务或客服确认。' : '支付参数已生成,请继续完成支付。');
|
||||
} catch (nextError) {
|
||||
setError(nextError instanceof Error ? nextError.message : '下单失败');
|
||||
} finally {
|
||||
setBusy('');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleOpenPayment() {
|
||||
if (!payment) return;
|
||||
if (payment.provider === 'wechat_pay' && process.env.TARO_ENV === 'weapp') {
|
||||
const paramsForWeapp = payment.paymentParams || {};
|
||||
Taro.requestPayment(paramsForWeapp as unknown as Taro.requestPayment.Option)
|
||||
.then(() => order?.orderNo ? pollStatus(order.orderNo) : undefined)
|
||||
.catch(nextError => setError(nextError instanceof Error ? nextError.message : '微信支付未完成'));
|
||||
return;
|
||||
}
|
||||
if (!paymentUrl) {
|
||||
await Taro.setClipboardData({ data: JSON.stringify(payment.paymentParams || {}) });
|
||||
setMessage('支付参数已复制,请交给支付容器或客服处理。');
|
||||
return;
|
||||
}
|
||||
tryOpenPaymentUrl(paymentUrl);
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='student-page'>
|
||||
<View className='student-topbar'>
|
||||
<View className='student-title-block'>
|
||||
<Text className='student-kicker'>Checkout</Text>
|
||||
<Text className='student-title'>会员收银台</Text>
|
||||
<Text className='student-subtitle'>金额、优惠、权益和支付状态全部由后端计算与确认。</Text>
|
||||
</View>
|
||||
<Button className='secondary-button' onClick={() => Taro.navigateBack()}>返回</Button>
|
||||
</View>
|
||||
|
||||
<View className='section-block'>
|
||||
<Text className='section-heading'>选择套餐</Text>
|
||||
<View className='list-stack'>
|
||||
{plans.map(item => (
|
||||
<View className={`list-row ${item.id === selectedPlanId ? 'active' : ''}`} key={item.id} onClick={() => setSelectedPlanId(item.id)}>
|
||||
<Text className='row-main'>{item.name} · ¥{centsToYuan(planPriceCents(item))}</Text>
|
||||
<Text className='row-meta'>{item.days === -1 ? '永久会员' : `${item.days || 0} 天`} {item.badge ? ` · ${item.badge}` : ''}</Text>
|
||||
{item.desc ? <Text className='row-meta'>{item.desc}</Text> : null}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
{!plans.length ? <View className='empty-state'>暂无可购买套餐,请联系租户后台配置 SVIP 套餐。</View> : null}
|
||||
</View>
|
||||
|
||||
<View className='section-block'>
|
||||
<Text className='section-heading'>优惠券</Text>
|
||||
<View className='toolbar checkout-toolbar'>
|
||||
<Input className='input' placeholder='输入优惠券码' value={couponCode} onInput={event => setCouponCode(String(event.detail.value || ''))} />
|
||||
<Button className='secondary-button' loading={busy === 'coupon'} onClick={handleClaimCoupon}>领取</Button>
|
||||
</View>
|
||||
{coupon?.coupon ? <Text className='success-text'>优惠券 {coupon.coupon.code || couponCode}:抵扣 ¥{centsToYuan(coupon.coupon.discountCents || 0)}</Text> : null}
|
||||
</View>
|
||||
|
||||
<View className='section-block'>
|
||||
<Text className='section-heading'>支付方式</Text>
|
||||
<View className='toolbar wrap'>
|
||||
{(['alipay', 'wechat_pay', 'manual'] as PayProvider[]).map(item => (
|
||||
<Button key={item} className={`pill-button ${provider === item ? 'active' : ''}`} onClick={() => setProvider(item)}>
|
||||
{providerLabels[item]}
|
||||
</Button>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className='section-block'>
|
||||
<View className='quiet-panel'>
|
||||
<View className='amount-row'>
|
||||
<Text className='row-meta'>预估应付</Text>
|
||||
<Text className='amount-text'>¥{centsToYuan(order?.amountCents ?? estimateAmountCents)}</Text>
|
||||
</View>
|
||||
<Text className='row-meta'>地区:{profile?.target?.regionName || regionId || '跟随套餐'} · 支付:{providerLabels[provider]}</Text>
|
||||
{order?.orderNo ? <Text className='row-meta'>订单号:{order.orderNo} · {order.status}</Text> : null}
|
||||
<View className='toolbar wrap checkout-actions'>
|
||||
<Button className='primary-button' loading={busy === 'order'} onClick={handleCreateOrder}>
|
||||
{order?.orderNo ? '重新发起支付' : '提交订单'}
|
||||
</Button>
|
||||
{payment ? <Button className='primary-button' onClick={handleOpenPayment}>前往支付</Button> : null}
|
||||
{order?.orderNo ? <Button className='secondary-button' onClick={() => pollStatus(order.orderNo)}>刷新状态</Button> : null}
|
||||
{order?.orderNo ? <Button className='secondary-button' onClick={() => Taro.navigateTo({ url: `/pages/student/order-detail/index?orderNo=${encodeURIComponent(order.orderNo)}` })}>订单详情</Button> : null}
|
||||
</View>
|
||||
{payment?.provider === 'manual' ? <Text className='row-meta'>线下支付不会由学生端确认,需租户后台具备支付权限的人员确认后才会开通权益。</Text> : null}
|
||||
{paymentUrl ? <Text className='row-meta break-text'>支付链接:{paymentUrl}</Text> : null}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{message ? <Text className='success-text'>{message}</Text> : null}
|
||||
{error ? <Text className='error-text'>{error}</Text> : null}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
3
apps/taro/src/pages/student/order-detail/index.config.ts
Normal file
3
apps/taro/src/pages/student/order-detail/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '订单详情',
|
||||
});
|
||||
200
apps/taro/src/pages/student/order-detail/index.tsx
Normal file
200
apps/taro/src/pages/student/order-detail/index.tsx
Normal file
@@ -0,0 +1,200 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import Taro, { useRouter } from '@tarojs/taro';
|
||||
import { Button, Text, View } from '@tarojs/components';
|
||||
import {
|
||||
createPayment,
|
||||
loadOrderDetail,
|
||||
loadOrderStatus,
|
||||
type OrderDetail,
|
||||
type OrderStatus,
|
||||
type PaymentCreateResult,
|
||||
} from '@/services/commerce';
|
||||
import { getTenantContext } from '@/services/api';
|
||||
import '../student.css';
|
||||
|
||||
function centsToYuan(value?: number | null) {
|
||||
return ((value || 0) / 100).toFixed(2);
|
||||
}
|
||||
|
||||
function recordText(record: Record<string, unknown>, keys: string[], fallback = '-') {
|
||||
for (const key of keys) {
|
||||
const value = record[key];
|
||||
if (typeof value === 'string' && value) return value;
|
||||
if (typeof value === 'number') return String(value);
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function encodeQuery(input: Record<string, string | undefined>) {
|
||||
return Object.entries(input)
|
||||
.filter(([, value]) => value)
|
||||
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value || '')}`)
|
||||
.join('&');
|
||||
}
|
||||
|
||||
function paymentUrl(result?: PaymentCreateResult | null) {
|
||||
const params = result?.paymentParams || {};
|
||||
const direct = params.url || params.payUrl || params.paymentUrl || params.qrCodeUrl;
|
||||
if (typeof direct === 'string' && direct) return direct;
|
||||
const gateway = params.gateway;
|
||||
const query = params.query;
|
||||
if (typeof gateway === 'string' && query && typeof query === 'object' && !Array.isArray(query)) {
|
||||
const queryString = encodeQuery(query as Record<string, string | undefined>);
|
||||
return queryString ? `${gateway}?${queryString}` : gateway;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
export default function StudentOrderDetailPage() {
|
||||
const router = useRouter();
|
||||
const orderNo = router.params?.orderNo || '';
|
||||
const tenant = getTenantContext();
|
||||
const [detail, setDetail] = useState<OrderDetail | null>(null);
|
||||
const [status, setStatus] = useState<OrderStatus | null>(null);
|
||||
const [payment, setPayment] = useState<PaymentCreateResult | null>(null);
|
||||
const [busy, setBusy] = useState('');
|
||||
const [message, setMessage] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const items = useMemo(() => detail?.items || [], [detail]);
|
||||
const payments = useMemo(() => detail?.payments || [], [detail]);
|
||||
const redemptions = useMemo(() => detail?.couponRedemptions || [], [detail]);
|
||||
const payUrl = paymentUrl(payment);
|
||||
const canContinuePay = detail?.status === 'pending' || status?.status === 'pending';
|
||||
|
||||
function reload() {
|
||||
if (!orderNo) {
|
||||
setError('缺少订单号。');
|
||||
return;
|
||||
}
|
||||
setBusy('load');
|
||||
setError('');
|
||||
Promise.all([
|
||||
loadOrderDetail(orderNo),
|
||||
loadOrderStatus(orderNo),
|
||||
])
|
||||
.then(([detailPayload, statusPayload]) => {
|
||||
setDetail(detailPayload.item || null);
|
||||
setStatus(statusPayload.item || null);
|
||||
})
|
||||
.catch(nextError => setError(nextError instanceof Error ? nextError.message : '订单加载失败'))
|
||||
.finally(() => setBusy(''));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
reload();
|
||||
}, [orderNo]);
|
||||
|
||||
async function handleContinuePay(provider?: string | null) {
|
||||
if (!orderNo) return;
|
||||
setBusy('pay');
|
||||
setError('');
|
||||
setMessage('');
|
||||
try {
|
||||
const payload = await createPayment({
|
||||
orderNo,
|
||||
provider: provider || detail?.payProvider || status?.payProvider || 'alipay',
|
||||
returnUrl: process.env.TARO_ENV === 'h5' && typeof window !== 'undefined' ? window.location.href : undefined,
|
||||
quitUrl: process.env.TARO_ENV === 'h5' && typeof window !== 'undefined' ? window.location.href : undefined,
|
||||
});
|
||||
const nextPayment = payload.item || null;
|
||||
setPayment(nextPayment);
|
||||
const url = paymentUrl(nextPayment);
|
||||
if (url && process.env.TARO_ENV === 'h5' && typeof window !== 'undefined') {
|
||||
window.location.href = url;
|
||||
} else if (url) {
|
||||
await Taro.setClipboardData({ data: url });
|
||||
setMessage('支付链接已复制。');
|
||||
} else {
|
||||
setMessage(nextPayment?.provider === 'manual' ? '线下支付订单已生成,请联系教务或客服确认。' : '支付参数已生成。');
|
||||
}
|
||||
} catch (nextError) {
|
||||
setError(nextError instanceof Error ? nextError.message : '继续支付失败');
|
||||
} finally {
|
||||
setBusy('');
|
||||
}
|
||||
}
|
||||
|
||||
async function copyAfterSalesInfo() {
|
||||
const serviceText = tenant?.branding?.slogan || tenant?.branding?.brandName || '请联系当前租户客服处理售后';
|
||||
await Taro.setClipboardData({
|
||||
data: `订单号:${orderNo}\n售后说明:${serviceText}`,
|
||||
});
|
||||
setMessage('订单售后信息已复制。');
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='student-page'>
|
||||
<View className='student-topbar'>
|
||||
<View className='student-title-block'>
|
||||
<Text className='student-kicker'>Order</Text>
|
||||
<Text className='student-title'>订单详情</Text>
|
||||
<Text className='student-subtitle'>学生端只查看订单和售后入口,退款审核与确认在租户后台完成。</Text>
|
||||
</View>
|
||||
<Button className='secondary-button' onClick={() => Taro.navigateBack()}>返回</Button>
|
||||
</View>
|
||||
|
||||
<View className='section-block'>
|
||||
<View className='quiet-panel'>
|
||||
<View className='amount-row'>
|
||||
<Text className='row-meta'>{detail?.orderNo || orderNo}</Text>
|
||||
<Text className='status-badge'>{status?.status || detail?.status || '加载中'}</Text>
|
||||
</View>
|
||||
<Text className='row-main'>{detail?.productName || '会员订单'}</Text>
|
||||
<Text className='amount-text'>¥{centsToYuan(detail?.amountCents ?? status?.amountCents)}</Text>
|
||||
<Text className='row-meta'>已退金额 ¥{centsToYuan(detail?.refundedAmountCents ?? status?.refundedAmountCents)} · 支付方式 {detail?.payProvider || status?.payProvider || '-'}</Text>
|
||||
<Text className='row-meta'>创建时间 {detail?.createdAt || '-'} · 支付时间 {detail?.paidAt || status?.paidAt || '-'}</Text>
|
||||
<View className='toolbar wrap checkout-actions'>
|
||||
<Button className='secondary-button' loading={busy === 'load'} onClick={reload}>刷新</Button>
|
||||
{canContinuePay ? <Button className='primary-button' loading={busy === 'pay'} onClick={() => handleContinuePay(detail?.payProvider)}>继续支付</Button> : null}
|
||||
<Button className='secondary-button' onClick={copyAfterSalesInfo}>售后入口</Button>
|
||||
</View>
|
||||
{payUrl ? <Text className='row-meta break-text'>支付链接:{payUrl}</Text> : null}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className='section-block'>
|
||||
<Text className='section-heading'>订单项目</Text>
|
||||
<View className='list-stack'>
|
||||
{items.map((item, index) => (
|
||||
<View className='list-row' key={recordText(item, ['id', 'itemId'], `item-${index}`)}>
|
||||
<Text className='row-main'>{recordText(item, ['name', 'itemType'])}</Text>
|
||||
<Text className='row-meta'>数量 {recordText(item, ['quantity'])} · 小计 ¥{centsToYuan(Number(item.totalAmountCents || 0))}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
{!items.length ? <View className='empty-state'>暂无订单项目。</View> : null}
|
||||
</View>
|
||||
|
||||
<View className='section-block'>
|
||||
<Text className='section-heading'>支付记录</Text>
|
||||
<View className='list-stack'>
|
||||
{payments.map((item, index) => (
|
||||
<View className='list-row' key={recordText(item, ['id', 'providerTradeNo'], `payment-${index}`)}>
|
||||
<Text className='row-main'>{recordText(item, ['provider'])} · {recordText(item, ['status'])}</Text>
|
||||
<Text className='row-meta'>{recordText(item, ['method'])} · ¥{centsToYuan(Number(item.amountCents || 0))}</Text>
|
||||
<Text className='row-meta'>交易号 {recordText(item, ['providerTradeNo'])}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
{!payments.length ? <View className='empty-state'>暂无支付记录。</View> : null}
|
||||
</View>
|
||||
|
||||
<View className='section-block'>
|
||||
<Text className='section-heading'>优惠记录</Text>
|
||||
<View className='list-stack'>
|
||||
{redemptions.map((item, index) => (
|
||||
<View className='list-row' key={recordText(item, ['id', 'couponCode'], `coupon-${index}`)}>
|
||||
<Text className='row-main'>{recordText(item, ['couponCode'])}</Text>
|
||||
<Text className='row-meta'>{recordText(item, ['status'])} · 抵扣 ¥{centsToYuan(Number(item.discountAppliedCents || 0))}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
{!redemptions.length ? <View className='empty-state'>暂无优惠记录。</View> : null}
|
||||
</View>
|
||||
|
||||
{message ? <Text className='success-text'>{message}</Text> : null}
|
||||
{error ? <Text className='error-text'>{error}</Text> : null}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -110,12 +110,15 @@ export default function StudentProfilePage() {
|
||||
<Text className='section-heading'>会员套餐</Text>
|
||||
<View className='list-stack'>
|
||||
{plans.slice(0, 4).map(item => (
|
||||
<View className='list-row' key={item.id}>
|
||||
<View className='list-row' key={item.id} onClick={() => Taro.navigateTo({ url: `/pages/student/checkout/index?planId=${encodeURIComponent(item.id)}${profile?.target?.regionId ? `®ionId=${encodeURIComponent(profile.target.regionId)}` : ''}` })}>
|
||||
<Text className='row-main'>{item.name} · ¥{String(item.price ?? 0)}</Text>
|
||||
<Text className='row-meta'>{item.days === -1 ? '永久' : `${item.days || 0} 天`} {item.badge ? ` · ${item.badge}` : ''}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
<View className='toolbar wrap'>
|
||||
<Button className='primary-button' onClick={() => Taro.navigateTo({ url: `/pages/student/checkout/index${profile?.target?.regionId ? `?regionId=${encodeURIComponent(profile.target.regionId)}` : ''}` })}>开通会员</Button>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className='section-block'>
|
||||
@@ -130,7 +133,7 @@ export default function StudentProfilePage() {
|
||||
<Text className='section-heading'>订单</Text>
|
||||
<View className='list-stack'>
|
||||
{orders.slice(0, 5).map(item => (
|
||||
<View className='list-row' key={item.id}>
|
||||
<View className='list-row' key={item.id} onClick={() => Taro.navigateTo({ url: `/pages/student/order-detail/index?orderNo=${encodeURIComponent(item.orderNo)}` })}>
|
||||
<Text className='row-main'>{item.productName || item.orderNo}</Text>
|
||||
<Text className='row-meta'>{item.status} · ¥{((item.amountCents || 0) / 100).toFixed(2)}</Text>
|
||||
</View>
|
||||
|
||||
@@ -242,3 +242,51 @@
|
||||
border-radius: 8px;
|
||||
background: #0f172a;
|
||||
}
|
||||
|
||||
.checkout-toolbar {
|
||||
align-items: center;
|
||||
overflow-x: visible;
|
||||
}
|
||||
|
||||
.checkout-toolbar .input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.checkout-actions {
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.amount-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.amount-text {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
color: #0f172a;
|
||||
font-size: 40px;
|
||||
font-weight: 850;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 40px;
|
||||
padding: 0 14px;
|
||||
border-radius: 8px;
|
||||
background: #ecfdf5;
|
||||
color: #047857;
|
||||
font-size: 22px;
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.break-text {
|
||||
word-break: break-all;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user