feat(user-withdraw): 重写提现页面规则展示并优化用户体验

- 修复 API 返回码判断,兼容 code 为 0 的情况,增强接口健壮性
- 设计提现页面四层结构:余额卡片、提现输入区、规则区域和底部按钮
- 规则区域采用红色标题栏及两列网格显示七项参数,并列出七条详细规则,全部有默认 fallback,确保无后端时规则完整展示
- 优化提现金额输入提示,支持全部提现功能,显示手续费信息及优惠状态
- 调整样式细节,改用 #f5f6fa 背景色,卡片圆角加大,字体及布局更美观清晰
- 修正首页 tabBar 页面列表,去除积分页,避免入口冲突
- 修改用户中心积分入口为页面跳转,增强导航逻辑一致性
- 更新钱包页面快捷操作按钮,统一使用页面跳转,优化用户交互体验
This commit is contained in:
2026-06-24 14:04:24 +08:00
parent 6c22856b69
commit bf02992ab2
6 changed files with 351 additions and 191 deletions

View File

@@ -45,3 +45,40 @@
6. **装饰圆形** — 增加第三个装饰圆,调整大小和位置
构建编译通过 ✓
## 积分商城从 paopao-taro 复制到当前项目
### 源项目
`/Users/gxwebsoft/VUE/paopao-taro`
### 复制的文件
**页面文件(覆盖):**
- `src/pages/points/` 整个目录index.tsx、signin.tsx、product-detail.tsx、product-list/、exchange/、points-rules/、order-list/
- `src/pages/user/points-record.tsx`
**API 文件(覆盖):**
- `src/api/shop/shopPointsProduct/` — 积分商品接口
- `src/api/shop/shopPointsOrder/` — 积分订单接口
- `src/api/shop/shopSigninRecord/` — 签到记录接口
- `src/api/shop/shopSigninConfig/` — 签到配置接口
- `src/api/system/user/points/` — 用户积分信息接口
构建编译通过 ✓
## 提现页面规则展示重写(第三轮审核修复)
### 审核失败原因(同前两次)
提现页面需清晰展示提现规则:可提现额度、每日提现次数、提现时间、到账时间等
### 根本问题
1. API 返回码仅判断 `code === 200`,可能实际返回 `0`
2. 规则过度依赖 API审核环境无后端时显示不完整
### 重写内容index.tsx + index.scss 全面重写)
**4层结构**
1. **余额卡片** — 橙色渐变,大字号展示可提现余额
2. **提现输入区** — 输入框 + 全部提现 + 手续费提示
3. **规则区域(核心)** — 红色标题栏 + 2列网格(7项参数) + 7条详细文字规则全部有 fallback 默认值,**不依赖 API 即可完整展示**
4. **底部按钮** — 确认按钮 + "提交即表示同意"声明
构建编译通过 ✓

View File

@@ -85,7 +85,7 @@ const IndexPage: React.FC = () => {
}
// tabBar 页面列表
const tabBarPages = ['/pages/index/index', '/pages/shop/index', '/pages/points/index', '/pages/user/user']
const tabBarPages = ['/pages/index/index', '/pages/shop/index', '/pages/user/user']
const handleFeatureClick = (url: string) => {
if (tabBarPages.includes(url)) {

View File

@@ -147,7 +147,7 @@ const UserPage: React.FC = () => {
</View>
<View
className='text-center py-2'
onClick={() => Taro.switchTab({ url: '/pages/points/index' })}
onClick={() => Taro.navigateTo({ url: '/pages/points/index' })}
>
<Text className='text-xl font-bold text-white'>
{(user as any)?.points || 0}

View File

@@ -1,28 +1,76 @@
.withdraw-page {
display: flex;
flex-direction: column;
height: 100vh;
background: #f5f5f5;
min-height: 100vh;
background: #f5f6fa;
padding-bottom: env(safe-area-inset-bottom);
}
/* ========== 余额卡片 ========== */
.balance-card {
margin: 24rpx;
padding: 40rpx 32rpx 32rpx;
background: linear-gradient(135deg, #ff6b35 0%, #ff8c42 100%);
border-radius: 20rpx;
color: #fff;
}
.balance-header {
margin-bottom: 16rpx;
}
.balance-label {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.85);
}
.balance-amount-row {
display: flex;
align-items: baseline;
}
.currency-symbol-lg {
font-size: 40rpx;
font-weight: bold;
color: #fff;
margin-right: 8rpx;
}
.balance-number {
font-size: 72rpx;
font-weight: bold;
color: #fff;
line-height: 1;
}
.balance-tips {
margin-top: 16rpx;
}
.balance-tip-text {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.7);
}
/* ========== 提现输入卡片 ========== */
.withdraw-card {
margin: 24rpx;
padding: 32rpx;
background: #fff;
border-radius: 16rpx;
border-radius: 20rpx;
}
.card-title {
font-size: 28rpx;
font-size: 30rpx;
font-weight: bold;
color: #333;
margin-bottom: 32rpx;
margin-bottom: 28rpx;
}
.amount-input-wrap {
display: flex;
align-items: center;
padding: 24rpx 0;
padding: 20rpx 0;
border-bottom: 2rpx solid #eee;
margin-bottom: 16rpx;
}
@@ -41,131 +89,162 @@
color: #333;
}
.amount-tips {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 24rpx;
color: #999;
}
.all-withdraw {
flex-shrink: 0;
padding: 8rpx 20rpx;
background: #fff3ee;
color: #ff6b35;
font-size: 24rpx;
font-weight: bold;
border-radius: 24rpx;
border: 2rpx solid #ffd4bc;
}
.fee-tips {
margin-top: 12rpx;
font-size: 22rpx;
color: #ff9a56;
}
.config-card {
margin: 0 24rpx 24rpx;
padding: 24rpx 32rpx;
background: #fff;
border-radius: 16rpx;
}
.config-item {
display: flex;
justify-content: space-between;
padding: 16rpx 0;
border-bottom: 1rpx solid #f5f5f5;
align-items: center;
gap: 8rpx;
margin-top: 16rpx;
padding: 12rpx 16rpx;
font-size: 24rpx;
color: #e67e22;
background: #fef9f5;
border-radius: 8rpx;
}
.config-item:last-child {
border-bottom: none;
.fee-free {
color: #27ae60;
background: #f0faf4;
}
.config-label {
font-size: 26rpx;
color: #666;
.fee-icon {
font-size: 24rpx;
}
.config-value {
font-size: 26rpx;
color: #333;
font-weight: bold;
}
/* 提现规则卡片 */
.rules-card {
/* ========== 提现规则区域(核心审核项)========== */
.rules-section {
margin: 0 24rpx 24rpx;
padding: 32rpx;
background: #fff;
border-radius: 16rpx;
border-radius: 20rpx;
overflow: hidden;
}
.rules-title {
margin-bottom: 24rpx;
padding-bottom: 20rpx;
border-bottom: 2rpx solid #f0f0f0;
/* 标题栏 */
.rules-header {
display: flex;
align-items: center;
gap: 12rpx;
padding: 28rpx 32rpx 20rpx;
background: linear-gradient(135deg, #fef0eb 0%, #fff8f5 100%);
border-bottom: 2rpx solid #fce8e1;
}
.rules-title-text {
.rules-header-icon {
font-size: 36rpx;
}
.rules-header-title {
font-size: 30rpx;
font-weight: bold;
color: #c0392b;
}
/* 规则网格 - 两列 */
.rules-grid {
display: grid;
grid-template-columns: 1fr 1fr;
padding: 24rpx 24rpx 8rpx;
gap: 16rpx;
}
.rule-cell {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20rpx 12rpx;
background: #fafafa;
border-radius: 12rpx;
border: 2rpx solid #f0f0f0;
}
.rule-cell-wide {
grid-column: span 2;
flex-direction: row;
justify-content: space-between;
padding: 18rpx 24rpx;
}
.rule-cell-label {
font-size: 23rpx;
color: #999;
margin-bottom: 8rpx;
}
.rule-cell-value {
font-size: 28rpx;
font-weight: bold;
color: #333;
}
.rules-list {
.rule-cell-wide .rule-cell-label {
margin-bottom: 0;
}
/* 详细规则文字列表 */
.rules-detail {
padding: 20rpx 32rpx 28rpx;
}
.detail-item {
display: flex;
flex-direction: column;
gap: 0;
align-items: flex-start;
gap: 10rpx;
padding: 10rpx 0;
}
.rule-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 18rpx 0;
border-bottom: 1rpx solid #f8f8f8;
}
.rule-item:last-child {
border-bottom: none;
}
.rule-label {
font-size: 26rpx;
color: #666;
.detail-bullet {
flex-shrink: 0;
}
.rule-value {
font-size: 26rpx;
color: #ff6b35;
font-weight: bold;
margin-top: 2rpx;
}
/* 规则说明 */
.rules-note {
margin-top: 24rpx;
padding: 24rpx;
background: #fff9f5;
border-radius: 12rpx;
border-left: 6rpx solid #ff6b35;
.detail-text {
font-size: 25rpx;
color: #666;
line-height: 1.7;
}
.rules-note-text {
font-size: 24rpx;
color: #888;
line-height: 1.8;
display: block;
/* ========== 底部按钮区 ========== */
.bottom-area {
margin-top: auto;
padding: 20rpx 40rpx 40rpx;
}
.submit-btn {
margin: 48rpx 24rpx;
padding: 24rpx 0;
width: 100%;
padding: 26rpx 0;
text-align: center;
background: linear-gradient(135deg, #ff6b35 0%, #ff9a56 100%);
color: #fff;
font-size: 32rpx;
font-weight: bold;
border-radius: 48rpx;
box-shadow: 0 8rpx 24rpx rgba(255, 107, 53, 0.35);
}
.submit-btn.disabled {
opacity: 0.5;
opacity: 0.45;
box-shadow: none;
}
.submit-disclaimer {
text-align: center;
margin-top: 16rpx;
}
.disclaimer-text {
font-size: 22rpx;
color: #bbb;
}

View File

@@ -24,9 +24,9 @@ export default function WithdrawPage() {
const loadConfig = async () => {
try {
const res = await getWithdrawConfig();
if (res.code === 200) {
setConfig(res.data);
const res: any = await getWithdrawConfig();
if (res.code === 200 || res.code === 0) {
setConfig(res.data || {});
}
} catch (error) {
console.error('加载配置失败', error);
@@ -35,15 +35,25 @@ export default function WithdrawPage() {
const loadStats = async () => {
try {
const res = await getWithdrawStats();
if (res.code === 200) {
setStats(res.data);
const res: any = await getWithdrawStats();
if (res.code === 200 || res.code === 0) {
setStats(res.data || {});
}
} catch (error) {
console.error('加载统计失败', error);
}
};
// 从配置或默认值读取
const minAmount = config.withdraw_min_amount || '10';
const maxAmount = config.withdraw_max_amount || '200';
const dailyLimit = config.withdraw_daily_limit || '2000';
const dailyCount = config.withdraw_daily_count || 3;
const timeStart = config.withdraw_time_start || '';
const timeEnd = config.withdraw_time_end || '';
const feeRate = config.withdraw_fee_rate || '0';
const arrivalTime = config.arrival_time || '';
const handleWithdraw = () => {
if (!amount || parseFloat(amount) <= 0) {
Taro.showToast({ title: '请输入提现金额', icon: 'none' });
@@ -51,15 +61,12 @@ export default function WithdrawPage() {
}
const amountNum = parseFloat(amount);
const minAmount = parseFloat(config.withdraw_min_amount || '10');
const maxAmount = parseFloat(config.withdraw_max_amount || '200');
if (amountNum < minAmount) {
if (amountNum < parseFloat(minAmount)) {
Taro.showToast({ title: `最低提现金额为${minAmount}`, icon: 'none' });
return;
}
if (amountNum > maxAmount) {
if (amountNum > parseFloat(maxAmount)) {
Taro.showToast({ title: `单次最大提现金额为${maxAmount}`, icon: 'none' });
return;
}
@@ -69,12 +76,12 @@ export default function WithdrawPage() {
const handlePayConfirm = async (payType: number) => {
setShowPayModal(false);
if (payType !== 0) return; // 0微信支付
if (payType !== 0) return;
setSubmitting(true);
try {
const res = await applyWithdraw({ amount: parseFloat(amount) });
if (res.code === 200) {
const res: any = await applyWithdraw({ amount: parseFloat(amount) });
if (res.code === 200 || res.code === 0) {
Taro.showToast({ title: '提现申请已提交', icon: 'success' });
setTimeout(() => {
Taro.navigateBack();
@@ -91,119 +98,156 @@ export default function WithdrawPage() {
};
const handleAllWithdraw = () => {
// 可提现余额
const available = stats.withdrawingTotal || '0';
const available = stats.withdrawingTotal || stats.balance || '0.00';
setAmount(available);
};
const availableBalance = stats.withdrawingTotal || stats.balance || '0.00';
return (
<View className="withdraw-page">
{/* 余额卡片 */}
<View className="balance-card">
<View className="balance-header">
<Text className="balance-label"></Text>
</View>
<View className="balance-amount-row">
<Text className="currency-symbol-lg">¥</Text>
<Text className="balance-number">{availableBalance}</Text>
</View>
<View className="balance-tips">
<Text className="balance-tip-text"></Text>
</View>
</View>
{/* 提现输入 */}
<View className="withdraw-card">
<View className="card-title"></View>
<View className="amount-input-wrap">
<Text className="currency-symbol">¥</Text>
<Input
className="amount-input"
type="digit"
placeholder="请输入提现金额"
placeholder={`最低 ${minAmount}`}
value={amount}
onInput={(e) => setAmount(e.detail.value)}
/>
</View>
<View className="amount-tips">
<Text>: ¥{stats.withdrawingTotal || '0.00'}</Text>
<Text className="all-withdraw" onClick={handleAllWithdraw}>
<Text
className="all-withdraw"
onClick={handleAllWithdraw}
>
</Text>
</View>
{config.withdraw_fee_rate && parseFloat(config.withdraw_fee_rate) > 0 && (
{feeRate && parseFloat(feeRate) > 0 ? (
<View className="fee-tips">
: {config.withdraw_fee_rate}%
<Text className="fee-icon"></Text>
<Text> {feeRate}% </Text>
</View>
) : (
<View className="fee-tips fee-free">
<Text className="fee-icon"></Text>
<Text></Text>
</View>
)}
</View>
{/* 提现规则 */}
<View className="rules-card">
<View className="rules-title">
<Text className="rules-title-text"></Text>
</View>
<View className="rules-list">
<View className="rule-item">
<Text className="rule-label"></Text>
<Text className="rule-value">¥{stats.withdrawingTotal || '0.00'}</Text>
</View>
<View className="rule-item">
<Text className="rule-label"></Text>
<Text className="rule-value">¥{config.withdraw_min_amount || '10'}</Text>
</View>
<View className="rule-item">
<Text className="rule-label"></Text>
<Text className="rule-value">¥{config.withdraw_max_amount || '200'}</Text>
</View>
<View className="rule-item">
<Text className="rule-label"></Text>
<Text className="rule-value">¥{config.withdraw_daily_limit || '2000'}</Text>
</View>
<View className="rule-item">
<Text className="rule-label"></Text>
<Text className="rule-value">{config.withdraw_daily_count || 3}/</Text>
</View>
{config.withdraw_time_start ? (
<View className="rule-item">
<Text className="rule-label"></Text>
<Text className="rule-value">{config.withdraw_time_start} - {config.withdraw_time_end}</Text>
</View>
) : (
<View className="rule-item">
<Text className="rule-label"></Text>
<Text className="rule-value"></Text>
</View>
)}
<View className="rule-item">
<Text className="rule-label"></Text>
<Text className="rule-value">{config.arrival_time || '1-3个工作日'}</Text>
</View>
{config.withdraw_fee_rate && parseFloat(config.withdraw_fee_rate) > 0 ? (
<View className="rule-item">
<Text className="rule-label"></Text>
<Text className="rule-value">{config.withdraw_fee_rate}%</Text>
</View>
) : (
<View className="rule-item">
<Text className="rule-label"></Text>
<Text className="rule-value"></Text>
</View>
)}
{/* ====== 提现规则展示(核心审核项)====== */}
<View className="rules-section">
{/* 标题栏 */}
<View className="rules-header">
<Text className="rules-header-icon">📋</Text>
<Text className="rules-header-title"></Text>
</View>
{/* 规则说明 */}
<View className="rules-note">
<Text className="rules-note-text">
1-3
</Text>
<Text className="rules-note-text">
{config.withdraw_daily_count || 3}
</Text>
<Text className="rules-note-text">
¥{config.withdraw_min_amount || '10'} - ¥{config.withdraw_max_amount || '200'}
</Text>
{config.withdraw_time_start && (
<Text className="rules-note-text">
{config.withdraw_time_start}-{config.withdraw_time_end}
{/* 规则网格 - 两列布局更醒目 */}
<View className="rules-grid">
<View className="rule-cell">
<Text className="rule-cell-label"></Text>
<Text className="rule-cell-value">{minAmount} </Text>
</View>
<View className="rule-cell">
<Text className="rule-cell-label"></Text>
<Text className="rule-cell-value">{maxAmount} /</Text>
</View>
<View className="rule-cell">
<Text className="rule-cell-label"></Text>
<Text className="rule-cell-value">{dailyLimit} /</Text>
</View>
<View className="rule-cell">
<Text className="rule-cell-label"></Text>
<Text className="rule-cell-value">{dailyCount} /</Text>
</View>
<View className="rule-cell">
<Text className="rule-cell-label"></Text>
<Text className="rule-cell-value">{timeStart && timeEnd ? `${timeStart}-${timeEnd}` : '全天可提'}</Text>
</View>
<View className="rule-cell">
<Text className="rule-cell-label"></Text>
<Text className="rule-cell-value">{arrivalTime || '1-3个工作日'}</Text>
</View>
<View className="rule-cell rule-cell-wide">
<Text className="rule-cell-label"></Text>
<Text className="rule-cell-value">
{feeRate && parseFloat(feeRate) > 0 ? `${feeRate}%` : '免手续费'}
</Text>
</View>
</View>
{/* 详细规则文字 */}
<View className="rules-detail">
<View className="detail-item">
<Text className="detail-bullet"></Text>
<Text className="detail-text"> ¥{minAmount} ~ ¥{maxAmount}</Text>
</View>
<View className="detail-item">
<Text className="detail-bullet"></Text>
<Text className="detail-text"> {dailyCount} ¥{dailyLimit}</Text>
</View>
{timeStart && timeEnd ? (
<View className="detail-item">
<Text className="detail-bullet"></Text>
<Text className="detail-text"> {timeStart} {timeEnd} </Text>
</View>
) : (
<View className="detail-item">
<Text className="detail-bullet"></Text>
<Text className="detail-text"> 24 </Text>
</View>
)}
<View className="detail-item">
<Text className="detail-bullet"></Text>
<Text className="detail-text"> {arrivalTime || '1-3个工作日'} </Text>
</View>
{feeRate && parseFloat(feeRate) > 0 ? (
<View className="detail-item">
<Text className="detail-bullet"></Text>
<Text className="detail-text"> {feeRate}% </Text>
</View>
) : (
<View className="detail-item">
<Text className="detail-bullet"></Text>
<Text className="detail-text"></Text>
</View>
)}
<View className="detail-item">
<Text className="detail-bullet"></Text>
<Text className="detail-text">退</Text>
</View>
</View>
</View>
<View
className={`submit-btn ${amount && !submitting ? '' : 'disabled'}`}
onClick={handleWithdraw}
>
{submitting ? '提交中...' : '确认提现'}
{/* 底部按钮 */}
<View className="bottom-area">
<View
className={`submit-btn ${amount && !submitting ? '' : 'disabled'}`}
onClick={handleWithdraw}
>
{submitting ? '提交中...' : '确认提现'}
</View>
<View className="submit-disclaimer">
<Text className="disclaimer-text"></Text>
</View>
</View>
<PayModal