forked from gxwebsoft/mp-10550
Compare commits
32 Commits
049b2396c3
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 81c63e0e65 | |||
| 86f7506422 | |||
| fae144549e | |||
| 718eddff63 | |||
| a4a0a922fc | |||
| ca2436a2e8 | |||
| 83ba49d860 | |||
| 7375a3b1ce | |||
| 756b548bf9 | |||
| 76e76c62ef | |||
| 546d90cc28 | |||
| d4fd61376c | |||
| b27421fd6e | |||
| b929b8d35e | |||
| 23af704c68 | |||
| ab61aa9ee0 | |||
| 64d30e1b62 | |||
| a8eb9e11be | |||
| 338dc421db | |||
| 6f1e0a6a2b | |||
| 8b5609255a | |||
| 31d47f0a0b | |||
| 68d5848d3d | |||
| e40120138b | |||
| ef26a207b0 | |||
| 78ac461ef9 | |||
| f9dcaa9ce9 | |||
| d86cdad470 | |||
| 3d94125c5e | |||
| 63d0d64a1f | |||
| 5840bea66b | |||
| 929f173b95 |
@@ -9,6 +9,6 @@ export const BaseUrl = API_BASE_URL;
|
|||||||
// 当前版本
|
// 当前版本
|
||||||
export const Version = 'v3.0.8';
|
export const Version = 'v3.0.8';
|
||||||
// 版权信息
|
// 版权信息
|
||||||
export const Copyright = 'WebSoft Inc.';
|
export const Copyright = '桂乐淘·购享无界 乐惠万家';
|
||||||
|
|
||||||
// java -jar CertificateDownloader.jar -k 0kF5OlPr482EZwtn9zGufUcqa7ovgxRL -m 1723321338 -f ./apiclient_key.pem -s 2B933F7C35014A1C363642623E4A62364B34C4EB -o ./
|
// java -jar CertificateDownloader.jar -k 0kF5OlPr482EZwtn9zGufUcqa7ovgxRL -m 1723321338 -f ./apiclient_key.pem -s 2B933F7C35014A1C363642623E4A62364B34C4EB -o ./
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
#### 新增功能
|
#### 新增功能
|
||||||
- 用户头像和基本信息展示
|
- 用户头像和基本信息展示
|
||||||
- 佣金统计(可提现、冻结中、累计收益)
|
- 佣金统计(可提现、待使用、累计收益)
|
||||||
- 团队统计(一级、二级、三级成员)
|
- 团队统计(一级、二级、三级成员)
|
||||||
- 功能导航网格(分销订单、提现申请、我的团队、推广二维码)
|
- 功能导航网格(分销订单、提现申请、我的团队、推广二维码)
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ dealer: {
|
|||||||
// 金额相关
|
// 金额相关
|
||||||
money: {
|
money: {
|
||||||
available: 'linear-gradient(135deg, #10b981 0%, #059669 100%)', // 可提现 - 绿色
|
available: 'linear-gradient(135deg, #10b981 0%, #059669 100%)', // 可提现 - 绿色
|
||||||
frozen: 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)', // 冻结中 - 蓝色
|
frozen: 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)', // 待使用 - 蓝色
|
||||||
total: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)' // 累计 - 橙色
|
total: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)' // 累计 - 橙色
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -31,6 +31,10 @@ export interface ShopDealerOrder {
|
|||||||
isInvalid?: number;
|
isInvalid?: number;
|
||||||
// 佣金结算(0未结算 1已结算)
|
// 佣金结算(0未结算 1已结算)
|
||||||
isSettled?: number;
|
isSettled?: number;
|
||||||
|
// 佣金解冻(0未解冻 1已解冻)
|
||||||
|
isUnfreeze?: number;
|
||||||
|
// 订单状态
|
||||||
|
orderStatus?: number;
|
||||||
// 结算时间
|
// 结算时间
|
||||||
settleTime?: number;
|
settleTime?: number;
|
||||||
// 商城ID
|
// 商城ID
|
||||||
|
|||||||
@@ -38,6 +38,10 @@ export interface ShopStoreRider {
|
|||||||
otherGoodsCommissionValue?: string;
|
otherGoodsCommissionValue?: string;
|
||||||
// 用户ID
|
// 用户ID
|
||||||
userId?: number;
|
userId?: number;
|
||||||
|
// 经度(配送员当前位置)
|
||||||
|
longitude?: string;
|
||||||
|
// 纬度(配送员当前位置)
|
||||||
|
latitude?: string;
|
||||||
// 备注
|
// 备注
|
||||||
comments?: string;
|
comments?: string;
|
||||||
// 排序号
|
// 排序号
|
||||||
|
|||||||
@@ -87,6 +87,10 @@ button[open-type="chooseAvatar"] {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
height: 80px;
|
height: 80px;
|
||||||
}
|
}
|
||||||
|
.cart-buy-only{
|
||||||
|
border-radius: 20px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
image {
|
image {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export default definePageConfig({
|
export default definePageConfig({
|
||||||
navigationBarTitleText: '成为经销商',
|
navigationBarTitleText: '注册成为会员',
|
||||||
navigationBarTextStyle: 'black'
|
navigationBarTextStyle: 'black'
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export default definePageConfig({
|
export default definePageConfig({
|
||||||
navigationBarTitleText: '桂乐淘分享中心'
|
navigationBarTitleText: '账户管理中心'
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ const DealerIndex: React.FC = () => {
|
|||||||
<View className="grid grid-cols-3 gap-3">
|
<View className="grid grid-cols-3 gap-3">
|
||||||
<View className="text-center p-3 rounded-lg flex flex-col" style={{
|
<View className="text-center p-3 rounded-lg flex flex-col" style={{
|
||||||
background: businessGradients.money.available
|
background: businessGradients.money.available
|
||||||
}}>
|
}} onClick={() => navigateToPage('/dealer/withdraw/index')}>
|
||||||
<Text className="text-lg font-bold mb-1 text-white">
|
<Text className="text-lg font-bold mb-1 text-white">
|
||||||
{formatMoney(dealerUser.money)}
|
{formatMoney(dealerUser.money)}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -146,7 +146,7 @@ const DealerIndex: React.FC = () => {
|
|||||||
<Text className="text-lg font-bold mb-1 text-white">
|
<Text className="text-lg font-bold mb-1 text-white">
|
||||||
{formatMoney(dealerUser.freezeMoney)}
|
{formatMoney(dealerUser.freezeMoney)}
|
||||||
</Text>
|
</Text>
|
||||||
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}>冻结中</Text>
|
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}>待使用</Text>
|
||||||
</View>
|
</View>
|
||||||
<View className="text-center p-3 rounded-lg flex flex-col" style={{
|
<View className="text-center p-3 rounded-lg flex flex-col" style={{
|
||||||
background: businessGradients.money.total
|
background: businessGradients.money.total
|
||||||
|
|||||||
@@ -94,15 +94,19 @@ const DealerOrders: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [fetchOrders])
|
}, [fetchOrders])
|
||||||
|
|
||||||
const getStatusText = (isSettled?: number, isInvalid?: number) => {
|
const getStatusText = (isSettled?: number, isInvalid?: number, isUnfreeze?: number, orderStatus?: number) => {
|
||||||
|
if (orderStatus === 2 || orderStatus === 5 || orderStatus === 6) return '已取消'
|
||||||
if (isInvalid === 1) return '已失效'
|
if (isInvalid === 1) return '已失效'
|
||||||
|
if (isUnfreeze === 1) return '已解冻'
|
||||||
if (isSettled === 1) return '已结算'
|
if (isSettled === 1) return '已结算'
|
||||||
return '待结算'
|
return '待结算'
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStatusColor = (isSettled?: number, isInvalid?: number) => {
|
const getStatusColor = (isSettled?: number, isInvalid?: number, isUnfreeze?: number, orderStatus?: number) => {
|
||||||
|
if (orderStatus === 2 || orderStatus === 5 || orderStatus === 6) return 'default'
|
||||||
if (isInvalid === 1) return 'danger'
|
if (isInvalid === 1) return 'danger'
|
||||||
if (isSettled === 1) return 'success'
|
if (isUnfreeze === 1) return 'success'
|
||||||
|
if (isSettled === 1) return 'info'
|
||||||
return 'warning'
|
return 'warning'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,8 +124,8 @@ const DealerOrders: React.FC = () => {
|
|||||||
<Text className="font-semibold text-gray-800">
|
<Text className="font-semibold text-gray-800">
|
||||||
订单号:{order.orderNo || '-'}
|
订单号:{order.orderNo || '-'}
|
||||||
</Text>
|
</Text>
|
||||||
<Tag type={getStatusColor(order.isSettled, order.isInvalid)}>
|
<Tag type={getStatusColor(order.isSettled, order.isInvalid, order.isUnfreeze,order.orderStatus)}>
|
||||||
{getStatusText(order.isSettled, order.isInvalid)}
|
{getStatusText(order.isSettled, order.isInvalid, order.isUnfreeze,order.orderStatus)}
|
||||||
</Tag>
|
</Tag>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
export default definePageConfig({
|
export default definePageConfig({
|
||||||
navigationBarTitleText: '桂乐淘分享中心'
|
navigationBarTitleText: '账户管理中心',
|
||||||
|
// Enable "Share to friends" and "Share to Moments" (timeline) for this page.
|
||||||
|
enableShareAppMessage: true,
|
||||||
|
enableShareTimeline: true
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React, {useState, useEffect} from 'react'
|
|||||||
import {View, Text, Image} from '@tarojs/components'
|
import {View, Text, Image} from '@tarojs/components'
|
||||||
import {Button, Loading} from '@nutui/nutui-react-taro'
|
import {Button, Loading} from '@nutui/nutui-react-taro'
|
||||||
import {Download, QrCode} from '@nutui/icons-react-taro'
|
import {Download, QrCode} from '@nutui/icons-react-taro'
|
||||||
import Taro from '@tarojs/taro'
|
import Taro, {useShareAppMessage} from '@tarojs/taro'
|
||||||
import {useDealerUser} from '@/hooks/useDealerUser'
|
import {useDealerUser} from '@/hooks/useDealerUser'
|
||||||
import {generateInviteCode} from '@/api/invite'
|
import {generateInviteCode} from '@/api/invite'
|
||||||
// import type {InviteStats} from '@/api/invite'
|
// import type {InviteStats} from '@/api/invite'
|
||||||
@@ -16,6 +16,39 @@ const DealerQrcode: React.FC = () => {
|
|||||||
// const [statsLoading, setStatsLoading] = useState<boolean>(false)
|
// const [statsLoading, setStatsLoading] = useState<boolean>(false)
|
||||||
const {dealerUser, loading: dealerLoading, error, refresh} = useDealerUser()
|
const {dealerUser, loading: dealerLoading, error, refresh} = useDealerUser()
|
||||||
|
|
||||||
|
// Enable "转发给朋友" + "分享到朋友圈" items in the share panel/menu.
|
||||||
|
useEffect(() => {
|
||||||
|
// Some clients require explicit call to show both share entries.
|
||||||
|
Taro.showShareMenu({
|
||||||
|
withShareTicket: true,
|
||||||
|
showShareItems: ['shareAppMessage', 'shareTimeline']
|
||||||
|
}).catch(() => {})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// 转发给朋友(分享小程序链接)
|
||||||
|
useShareAppMessage(() => {
|
||||||
|
const inviterRaw = dealerUser?.userId ?? Taro.getStorageSync('UserId')
|
||||||
|
const inviter = Number(inviterRaw)
|
||||||
|
const hasInviter = Number.isFinite(inviter) && inviter > 0
|
||||||
|
|
||||||
|
const user = Taro.getStorageSync('User') || {}
|
||||||
|
const nickname = (user && (user.nickname || user.realName || user.username)) || ''
|
||||||
|
const title = hasInviter ? `${nickname || '我'}邀请你加入桂乐淘伙伴计划` : '桂乐淘伙伴计划'
|
||||||
|
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
path: hasInviter
|
||||||
|
? `/pages/index/index?inviter=${inviter}&source=dealer_qrcode&t=${Date.now()}`
|
||||||
|
: `/pages/index/index`,
|
||||||
|
success: function () {
|
||||||
|
Taro.showToast({title: '分享成功', icon: 'success', duration: 2000})
|
||||||
|
},
|
||||||
|
fail: function () {
|
||||||
|
Taro.showToast({title: '分享失败', icon: 'none', duration: 2000})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 生成小程序码
|
// 生成小程序码
|
||||||
const generateMiniProgramCode = async () => {
|
const generateMiniProgramCode = async () => {
|
||||||
if (!dealerUser?.userId) {
|
if (!dealerUser?.userId) {
|
||||||
@@ -376,29 +409,7 @@ const DealerQrcode: React.FC = () => {
|
|||||||
保存小程序码到相册
|
保存小程序码到相册
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
{/*<View className={'my-2 bg-white'}>*/}
|
|
||||||
{/* <Button*/}
|
|
||||||
{/* size="large"*/}
|
|
||||||
{/* block*/}
|
|
||||||
{/* icon={<Copy/>}*/}
|
|
||||||
{/* onClick={copyInviteInfo}*/}
|
|
||||||
{/* disabled={!dealerUser?.userId || codeLoading}*/}
|
|
||||||
{/* >*/}
|
|
||||||
{/* 复制邀请信息*/}
|
|
||||||
{/* </Button>*/}
|
|
||||||
{/*</View>*/}
|
|
||||||
{/*<View className={'my-2 bg-white'}>*/}
|
|
||||||
{/* <Button*/}
|
|
||||||
{/* size="large"*/}
|
|
||||||
{/* block*/}
|
|
||||||
{/* fill="outline"*/}
|
|
||||||
{/* icon={<Share/>}*/}
|
|
||||||
{/* onClick={shareMiniProgramCode}*/}
|
|
||||||
{/* disabled={!dealerUser?.userId || codeLoading}*/}
|
|
||||||
{/* >*/}
|
|
||||||
{/* 分享给好友*/}
|
|
||||||
{/* </Button>*/}
|
|
||||||
{/*</View>*/}
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 推广说明 */}
|
{/* 推广说明 */}
|
||||||
|
|||||||
@@ -325,7 +325,7 @@ const DealerTeam: React.FC = () => {
|
|||||||
</View>
|
</View>
|
||||||
{/* 显示手机号(仅本级可见) */}
|
{/* 显示手机号(仅本级可见) */}
|
||||||
{showPhone && member.phone && (
|
{showPhone && member.phone && (
|
||||||
<Text className="text-sm text-gray-500" onClick={(e) => {
|
<Text className="text-sm text-gray-500 hidden" onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
makePhoneCall(member.phone || '');
|
makePhoneCall(member.phone || '');
|
||||||
}}>
|
}}>
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ const normalizeMoneyString = (money: unknown) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const DealerWithdraw: React.FC = () => {
|
const DealerWithdraw: React.FC = () => {
|
||||||
const [activeTab, setActiveTab] = useState<string | number>('0')
|
const [activeTab, setActiveTab] = useState<string>('0')
|
||||||
const [loading, setLoading] = useState<boolean>(false)
|
const [loading, setLoading] = useState<boolean>(false)
|
||||||
const [refreshing, setRefreshing] = useState<boolean>(false)
|
const [refreshing, setRefreshing] = useState<boolean>(false)
|
||||||
const [submitting, setSubmitting] = useState<boolean>(false)
|
const [submitting, setSubmitting] = useState<boolean>(false)
|
||||||
@@ -114,10 +114,11 @@ const DealerWithdraw: React.FC = () => {
|
|||||||
// Tab 切换处理函数
|
// Tab 切换处理函数
|
||||||
const handleTabChange = (value: string | number) => {
|
const handleTabChange = (value: string | number) => {
|
||||||
console.log('Tab切换到:', value)
|
console.log('Tab切换到:', value)
|
||||||
setActiveTab(value)
|
const next = String(value)
|
||||||
|
setActiveTab(next)
|
||||||
|
|
||||||
// 如果切换到提现记录页面,刷新数据
|
// 如果切换到提现记录页面,刷新数据
|
||||||
if (String(value) === '1') {
|
if (next === '1') {
|
||||||
fetchWithdrawRecords()
|
fetchWithdrawRecords()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -310,7 +311,7 @@ const DealerWithdraw: React.FC = () => {
|
|||||||
if (amount > available) {
|
if (amount > available) {
|
||||||
Taro.showToast({
|
Taro.showToast({
|
||||||
title: '提现金额超过可用余额',
|
title: '提现金额超过可用余额',
|
||||||
icon: 'error'
|
icon: 'none'
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -323,7 +324,6 @@ const DealerWithdraw: React.FC = () => {
|
|||||||
money: values.amount,
|
money: values.amount,
|
||||||
// Only support WeChat wallet withdrawals.
|
// Only support WeChat wallet withdrawals.
|
||||||
payType: 10,
|
payType: 10,
|
||||||
applyStatus: 10, // 待审核
|
|
||||||
platform: 'MiniProgram'
|
platform: 'MiniProgram'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -488,7 +488,7 @@ const DealerWithdraw: React.FC = () => {
|
|||||||
labelPosition="top"
|
labelPosition="top"
|
||||||
>
|
>
|
||||||
<CellGroup>
|
<CellGroup>
|
||||||
<Form.Item name="amount" label="提现金额" required>
|
<Form.Item name="amount" label="提现金额">
|
||||||
<Input
|
<Input
|
||||||
placeholder="请输入提现金额"
|
placeholder="请输入提现金额"
|
||||||
type="number"
|
type="number"
|
||||||
@@ -523,7 +523,7 @@ const DealerWithdraw: React.FC = () => {
|
|||||||
<Text className="text-sm text-red-500">
|
<Text className="text-sm text-red-500">
|
||||||
注意事项:
|
注意事项:
|
||||||
1. 提取佣金必须完成实名认证。
|
1. 提取佣金必须完成实名认证。
|
||||||
2. 佣金非自动到账,再您提取佣金申请通过后,请手动到我的申请记录点击领取。
|
2. 佣金非自动到账,在您提取佣金申请通过后,请手动到我的申请记录点击领取。
|
||||||
3. 桂乐淘温馨提示,请您依法依规申报所得,缴税相关税费。
|
3. 桂乐淘温馨提示,请您依法依规申报所得,缴税相关税费。
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
@@ -629,13 +629,12 @@ const DealerWithdraw: React.FC = () => {
|
|||||||
<View className="bg-gray-50 min-h-screen">
|
<View className="bg-gray-50 min-h-screen">
|
||||||
<Tabs value={activeTab} onChange={handleTabChange}>
|
<Tabs value={activeTab} onChange={handleTabChange}>
|
||||||
<Tabs.TabPane title="申请提现" value="0">
|
<Tabs.TabPane title="申请提现" value="0">
|
||||||
{renderWithdrawForm()}
|
|
||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
|
|
||||||
<Tabs.TabPane title="提现记录" value="1">
|
<Tabs.TabPane title="提现记录" value="1">
|
||||||
{renderWithdrawRecords()}
|
|
||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
{activeTab === '0' ? renderWithdrawForm() : renderWithdrawRecords()}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import Taro from '@tarojs/taro';
|
|||||||
import { User } from '@/api/system/user/model';
|
import { User } from '@/api/system/user/model';
|
||||||
import { getUserInfo, updateUserInfo, loginByOpenId } from '@/api/layout';
|
import { getUserInfo, updateUserInfo, loginByOpenId } from '@/api/layout';
|
||||||
import { TenantId } from '@/config/app';
|
import { TenantId } from '@/config/app';
|
||||||
import {getStoredInviteParams, handleInviteRelation} from '@/utils/invite';
|
import { handleInviteRelation } from '@/utils/invite';
|
||||||
|
|
||||||
// 用户Hook
|
// 用户Hook
|
||||||
export const useUser = () => {
|
export const useUser = () => {
|
||||||
@@ -44,15 +44,10 @@ export const useUser = () => {
|
|||||||
reject(new Error('自动登录失败'));
|
reject(new Error('自动登录失败'));
|
||||||
}
|
}
|
||||||
}).catch(_ => {
|
}).catch(_ => {
|
||||||
// 首次注册,跳转到邀请注册页面
|
// 登录失败(通常是新用户尚未注册/未绑定手机号等)。
|
||||||
const pages = Taro.getCurrentPages();
|
// 这里不做任何“自动跳转”,避免用户点击「我的」时被强制带到分销/申请页,体验割裂。
|
||||||
const currentPage = pages[pages.length - 1];
|
// 需要登录的页面请使用 utils/auth 的 ensureLoggedIn / goToRegister 做显式跳转。
|
||||||
const inviteParams = getStoredInviteParams()
|
reject(new Error('autoLoginByOpenId failed'));
|
||||||
if (currentPage?.route !== 'dealer/apply/add' && inviteParams?.inviter) {
|
|
||||||
return Taro.navigateTo({
|
|
||||||
url: '/dealer/apply/add'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
fail: reject
|
fail: reject
|
||||||
@@ -60,7 +55,11 @@ export const useUser = () => {
|
|||||||
});
|
});
|
||||||
return res;
|
return res;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('自动登录失败:', error);
|
const msg = error instanceof Error ? error.message : String(error);
|
||||||
|
// 新用户首次进入、未绑定手机号等场景属于“预期失败”,避免刷屏报错。
|
||||||
|
if (msg !== 'autoLoginByOpenId failed') {
|
||||||
|
console.error('自动登录失败:', error);
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import Header from './Header'
|
// import Header from './Header'
|
||||||
import Banner from './Banner'
|
import Banner from './Banner'
|
||||||
import Taro, { useDidShow, useShareAppMessage } from '@tarojs/taro'
|
import Taro, { useDidShow, useShareAppMessage } from '@tarojs/taro'
|
||||||
import { View, Text, Image, ScrollView } from '@tarojs/components'
|
import { View, Text, Image, ScrollView } from '@tarojs/components'
|
||||||
import { useEffect, useMemo, useState, type ReactNode } from 'react'
|
import { useEffect, useMemo, useState, type ReactNode } from 'react'
|
||||||
import { Cart, Gift, Ticket } from '@nutui/icons-react-taro'
|
import { Cart, Gift, Ticket, Agenda, ArrowRight } from '@nutui/icons-react-taro'
|
||||||
import { getShopInfo } from '@/api/layout'
|
import { getShopInfo } from '@/api/layout'
|
||||||
import { checkAndHandleInviteRelation, hasPendingInvite } from '@/utils/invite'
|
import { checkAndHandleInviteRelation, hasPendingInvite } from '@/utils/invite'
|
||||||
import { pageShopGoods } from '@/api/shop/shopGoods'
|
import { pageShopGoods } from '@/api/shop/shopGoods'
|
||||||
@@ -11,6 +11,7 @@ import type { ShopGoods, ShopGoodsParam } from '@/api/shop/shopGoods/model'
|
|||||||
import { getMyGltUserTicketTotal } from '@/api/glt/gltUserTicket'
|
import { getMyGltUserTicketTotal } from '@/api/glt/gltUserTicket'
|
||||||
import { ensureLoggedIn } from '@/utils/auth'
|
import { ensureLoggedIn } from '@/utils/auth'
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
// import navTo from "@/utils/common";
|
||||||
|
|
||||||
function Home() {
|
function Home() {
|
||||||
const [activeTabKey, setActiveTabKey] = useState('recommend')
|
const [activeTabKey, setActiveTabKey] = useState('recommend')
|
||||||
@@ -20,9 +21,12 @@ function Home() {
|
|||||||
useShareAppMessage(() => {
|
useShareAppMessage(() => {
|
||||||
// 获取当前用户ID,用于生成邀请链接
|
// 获取当前用户ID,用于生成邀请链接
|
||||||
const userId = Taro.getStorageSync('UserId');
|
const userId = Taro.getStorageSync('UserId');
|
||||||
|
const user = Taro.getStorageSync('User') || {};
|
||||||
|
const nickname =
|
||||||
|
(user && (user.nickname || user.realName || user.username)) || '';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: userId + '超值推荐',
|
title: (nickname || '') + '超值推荐',
|
||||||
path: userId ? `/pages/index/index?inviter=${userId}&source=share&t=${Date.now()}` : `/pages/index/index`,
|
path: userId ? `/pages/index/index?inviter=${userId}&source=share&t=${Date.now()}` : `/pages/index/index`,
|
||||||
success: function () {
|
success: function () {
|
||||||
console.log('首页分享成功');
|
console.log('首页分享成功');
|
||||||
@@ -164,6 +168,7 @@ function Home() {
|
|||||||
Taro.getUserInfo({
|
Taro.getUserInfo({
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
const avatar = res.userInfo.avatarUrl;
|
const avatar = res.userInfo.avatarUrl;
|
||||||
|
// Keep WeChat display name in storage so share title can use it.
|
||||||
console.log(avatar, 'avatarUrl')
|
console.log(avatar, 'avatarUrl')
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -216,6 +221,15 @@ function Home() {
|
|||||||
Taro.navigateTo({ url: '/user/ticket/use?goodsId=10074' })
|
Taro.navigateTo({ url: '/user/ticket/use?goodsId=10074' })
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'order',
|
||||||
|
title: '送水订单',
|
||||||
|
icon: <Agenda size={30} />,
|
||||||
|
onClick: () => {
|
||||||
|
if (!ensureLoggedIn('/user/ticket/index')) return
|
||||||
|
Taro.navigateTo({ url: '/user/ticket/index' })
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'invite',
|
key: 'invite',
|
||||||
title: '邀请有礼',
|
title: '邀请有礼',
|
||||||
@@ -245,7 +259,7 @@ function Home() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Header区域 */}
|
{/* Header区域 */}
|
||||||
<Header />
|
{/*<Header />*/}
|
||||||
|
|
||||||
<View className="home-page">
|
<View className="home-page">
|
||||||
{/* 顶部活动主视觉:使用 Banner 组件 */}
|
{/* 顶部活动主视觉:使用 Banner 组件 */}
|
||||||
@@ -276,7 +290,21 @@ function Home() {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 分类Tabs */}
|
<View className="ticket-card" onClick={() => Taro.navigateTo({ url: `/shop/category/index?id=4560` })}>
|
||||||
|
<View className="ticket-card__head">
|
||||||
|
<Text className="ticket-card__title">政企采购专区</Text>
|
||||||
|
<ArrowRight className={'text-gray-50'} size={16} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="ticket-card" onClick={() => Taro.navigateTo({ url: `/shop/category/index?id=4556` })}>
|
||||||
|
<View className="ticket-card__head">
|
||||||
|
<Text className="ticket-card__title">桂乐淘·福利惊爆区</Text>
|
||||||
|
<ArrowRight className={'text-gray-50'} size={16} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/*分类Tabs*/}
|
||||||
<ScrollView className="home-tabs" scrollX enableFlex>
|
<ScrollView className="home-tabs" scrollX enableFlex>
|
||||||
<View className="home-tabs__inner">
|
<View className="home-tabs__inner">
|
||||||
{tabs.map((tab) => {
|
{tabs.map((tab) => {
|
||||||
@@ -293,7 +321,6 @@ function Home() {
|
|||||||
})}
|
})}
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
{/* 商品列表 */}
|
{/* 商品列表 */}
|
||||||
<View className="goods-grid">
|
<View className="goods-grid">
|
||||||
{visibleGoods.map((item) => (
|
{visibleGoods.map((item) => (
|
||||||
@@ -316,20 +343,20 @@ function Home() {
|
|||||||
<Text className="goods-card__sold">已购:{item.sales || 0}人</Text>
|
<Text className="goods-card__sold">已购:{item.sales || 0}人</Text>
|
||||||
<View className="goods-card__price">
|
<View className="goods-card__price">
|
||||||
<Text className="goods-card__priceUnit">¥</Text>
|
<Text className="goods-card__priceUnit">¥</Text>
|
||||||
<Text className="goods-card__priceValue">{item.price}</Text>
|
<Text className="goods-card__priceValue">{item.buyingPrice}</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View className="goods-card__actions">
|
<View className="goods-card__actions">
|
||||||
<View
|
{/*<View*/}
|
||||||
className="goods-card__btn goods-card__btn--ghost"
|
{/* className="goods-card__btn goods-card__btn--ghost"*/}
|
||||||
onClick={() => {
|
{/* onClick={() => {*/}
|
||||||
if (!ensureLoggedIn('/shop/orderConfirm/index?goodsId=10074')) return
|
{/* if (!ensureLoggedIn('/shop/orderConfirm/index?goodsId=10074')) return*/}
|
||||||
Taro.navigateTo({ url: '/shop/orderConfirm/index?goodsId=10074' })
|
{/* Taro.navigateTo({ url: '/shop/orderConfirm/index?goodsId=10074' })*/}
|
||||||
}}
|
{/* }}*/}
|
||||||
>
|
{/*>*/}
|
||||||
<Text className="goods-card__btnText">买水票更优惠</Text>
|
{/* <Text className="goods-card__btnText">买水票更优惠</Text>*/}
|
||||||
</View>
|
{/*</View>*/}
|
||||||
<View
|
<View
|
||||||
className="goods-card__btn goods-card__btn--primary"
|
className="goods-card__btn goods-card__btn--primary"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
@@ -343,6 +370,7 @@ function Home() {
|
|||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ const IsDealer = () => {
|
|||||||
<View style={{display: 'inline-flex', alignItems: 'center'}}>
|
<View style={{display: 'inline-flex', alignItems: 'center'}}>
|
||||||
<Reward className={'text-orange-100 '} size={16}/>
|
<Reward className={'text-orange-100 '} size={16}/>
|
||||||
<Text style={{fontSize: '16px'}}
|
<Text style={{fontSize: '16px'}}
|
||||||
className={'pl-3 text-orange-100 font-medium'}>{config?.vipText || '桂乐淘分享中心'}</Text>
|
className={'pl-3 text-orange-100 font-medium'}>{config?.vipText || '账户管理中心'}</Text>
|
||||||
{/*<Text className={'text-white opacity-80 pl-3'}>门店核销</Text>*/}
|
{/*<Text className={'text-white opacity-80 pl-3'}>门店核销</Text>*/}
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
@@ -76,7 +76,7 @@ const IsDealer = () => {
|
|||||||
title={
|
title={
|
||||||
<View style={{display: 'inline-flex', alignItems: 'center'}}>
|
<View style={{display: 'inline-flex', alignItems: 'center'}}>
|
||||||
<Reward className={'text-orange-100 '} size={16}/>
|
<Reward className={'text-orange-100 '} size={16}/>
|
||||||
<Text style={{fontSize: '16px'}} className={'pl-3 text-orange-100 font-medium'}>{config?.vipText || '桂乐淘分享中心'}</Text>
|
<Text style={{fontSize: '16px'}} className={'pl-3 text-orange-100 font-medium'}>{config?.vipText || '账户管理中心'}</Text>
|
||||||
<Text className={'text-white opacity-80 pl-3'}>{config?.vipComments || ''}</Text>
|
<Text className={'text-white opacity-80 pl-3'}>{config?.vipComments || ''}</Text>
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { saveStorageByLoginUser } from '@/utils/server'
|
|||||||
|
|
||||||
const UserCard = forwardRef<any, any>((_, ref) => {
|
const UserCard = forwardRef<any, any>((_, ref) => {
|
||||||
const {data, refresh} = useUserData()
|
const {data, refresh} = useUserData()
|
||||||
const {getDisplayName, isAdmin} = useUser();
|
const {loadUserFromStorage} = useUser();
|
||||||
const [IsLogin, setIsLogin] = useState<boolean>(false)
|
const [IsLogin, setIsLogin] = useState<boolean>(false)
|
||||||
const [userInfo, setUserInfo] = useState<User>()
|
const [userInfo, setUserInfo] = useState<User>()
|
||||||
const [ticketTotal, setTicketTotal] = useState<number>(0)
|
const [ticketTotal, setTicketTotal] = useState<number>(0)
|
||||||
@@ -25,9 +25,14 @@ const UserCard = forwardRef<any, any>((_, ref) => {
|
|||||||
const themeStyles = useThemeStyles();
|
const themeStyles = useThemeStyles();
|
||||||
const canShowScanButton = (() => {
|
const canShowScanButton = (() => {
|
||||||
const v: any = (userInfo as any)?.isAdmin
|
const v: any = (userInfo as any)?.isAdmin
|
||||||
return isAdmin() || v === true || v === 1 || v === '1'
|
return v === true || v === 1 || v === '1'
|
||||||
})()
|
})()
|
||||||
|
|
||||||
|
const getDisplayName = () => {
|
||||||
|
if (!userInfo) return IsLogin ? '用户' : '点击登录'
|
||||||
|
return userInfo.nickname || (userInfo as any).realName || (userInfo as any).username || (IsLogin ? '用户' : '点击登录')
|
||||||
|
}
|
||||||
|
|
||||||
// 角色名称:优先取用户 roles 数组的第一个角色名称
|
// 角色名称:优先取用户 roles 数组的第一个角色名称
|
||||||
const getRoleName = () => {
|
const getRoleName = () => {
|
||||||
return userInfo?.roles?.[0]?.roleName || userInfo?.roleName || '注册用户'
|
return userInfo?.roles?.[0]?.roleName || userInfo?.roleName || '注册用户'
|
||||||
@@ -45,10 +50,46 @@ const UserCard = forwardRef<any, any>((_, ref) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const syncUserToStorage = (u: User) => {
|
||||||
|
// Keep storage up-to-date for other places that read user info synchronously.
|
||||||
|
Taro.setStorageSync('User', u)
|
||||||
|
if (u?.userId) Taro.setStorageSync('UserId', u.userId)
|
||||||
|
if (u?.nickname) Taro.setStorageSync('WxNickName', u.nickname)
|
||||||
|
}
|
||||||
|
|
||||||
|
const reloadUserInfo = async () => {
|
||||||
|
try {
|
||||||
|
const u = await getUserInfo()
|
||||||
|
if (u) {
|
||||||
|
setUserInfo(u)
|
||||||
|
setIsLogin(true)
|
||||||
|
syncUserToStorage(u)
|
||||||
|
// Refresh this hook instance's state from storage (defensive).
|
||||||
|
await loadUserFromStorage()
|
||||||
|
|
||||||
|
// 获取openId(不阻塞 UI 刷新)
|
||||||
|
if (!u.openid) {
|
||||||
|
Taro.login({
|
||||||
|
success: (res) => {
|
||||||
|
getWxOpenId({code: res.code}).catch(() => {})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Not logged in / token expired: keep UI in "not login" state.
|
||||||
|
// Other error handling is done in request interceptor / callers.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 暴露方法给父组件
|
// 暴露方法给父组件
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
handleRefresh: () => reloadStats(true),
|
handleRefresh: async () => {
|
||||||
reloadStats
|
await reloadUserInfo()
|
||||||
|
await reloadStats(true)
|
||||||
|
},
|
||||||
|
reloadStats,
|
||||||
|
reloadUserInfo
|
||||||
}))
|
}))
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -97,30 +138,15 @@ const UserCard = forwardRef<any, any>((_, ref) => {
|
|||||||
nickname: res.userInfo.nickName,
|
nickname: res.userInfo.nickName,
|
||||||
sexName: res.userInfo.gender == 1 ? '男' : '女'
|
sexName: res.userInfo.gender == 1 ? '男' : '女'
|
||||||
})
|
})
|
||||||
getUserInfo().then((data) => {
|
reloadUserInfo()
|
||||||
if (data) {
|
.then(() => {
|
||||||
setUserInfo(data)
|
|
||||||
setIsLogin(true);
|
|
||||||
// Keep local storage user info in sync so other hooks (e.g. unified scan) can read admin flags.
|
|
||||||
Taro.setStorageSync('User', data)
|
|
||||||
Taro.setStorageSync('UserId', data.userId)
|
|
||||||
// 登录态已就绪后刷新卡片统计(余额/积分/券/水票)
|
// 登录态已就绪后刷新卡片统计(余额/积分/券/水票)
|
||||||
refresh().then()
|
refresh().then()
|
||||||
reloadTicketTotal()
|
reloadTicketTotal()
|
||||||
|
})
|
||||||
// 获取openId
|
.catch(() => {
|
||||||
if (!data.openid) {
|
console.log('未登录')
|
||||||
Taro.login({
|
})
|
||||||
success: (res) => {
|
|
||||||
getWxOpenId({code: res.code}).then(() => {
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).catch(() => {
|
|
||||||
console.log('未登录')
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -309,13 +335,9 @@ const UserCard = forwardRef<any, any>((_, ref) => {
|
|||||||
<View className={'py-2'}>
|
<View className={'py-2'}>
|
||||||
<View className={'flex justify-around mt-1'}>
|
<View className={'flex justify-around mt-1'}>
|
||||||
<View className={'item flex justify-center flex-col items-center'}
|
<View className={'item flex justify-center flex-col items-center'}
|
||||||
onClick={() => navTo('/user/wallet/wallet', true)}>
|
onClick={() => navTo('/user/ticket/index', true)}>
|
||||||
<Text className={'text-xs text-gray-200'} style={themeStyles.textColor}>余额</Text>
|
<Text className={'text-xs text-gray-200'} style={themeStyles.textColor}>水票</Text>
|
||||||
<Text className={'text-xl text-white'} style={themeStyles.textColor}>{data?.balance || '0.00'}</Text>
|
<Text className={'text-xl text-white'} style={themeStyles.textColor}>{ticketTotal}</Text>
|
||||||
</View>
|
|
||||||
<View className={'item flex justify-center flex-col items-center'}>
|
|
||||||
<Text className={'text-xs text-gray-200'} style={themeStyles.textColor}>积分</Text>
|
|
||||||
<Text className={'text-xl text-white'} style={themeStyles.textColor}>{data?.points || 0}</Text>
|
|
||||||
</View>
|
</View>
|
||||||
<View className={'item flex justify-center flex-col items-center'}
|
<View className={'item flex justify-center flex-col items-center'}
|
||||||
onClick={() => navTo('/user/coupon/index', true)}>
|
onClick={() => navTo('/user/coupon/index', true)}>
|
||||||
@@ -323,9 +345,13 @@ const UserCard = forwardRef<any, any>((_, ref) => {
|
|||||||
<Text className={'text-xl text-white'} style={themeStyles.textColor}>{data?.coupons || 0}</Text>
|
<Text className={'text-xl text-white'} style={themeStyles.textColor}>{data?.coupons || 0}</Text>
|
||||||
</View>
|
</View>
|
||||||
<View className={'item flex justify-center flex-col items-center'}
|
<View className={'item flex justify-center flex-col items-center'}
|
||||||
onClick={() => navTo('/user/ticket/index', true)}>
|
onClick={() => navTo('/user/wallet/wallet', true)}>
|
||||||
<Text className={'text-xs text-gray-200'} style={themeStyles.textColor}>水票</Text>
|
<Text className={'text-xs text-gray-200'} style={themeStyles.textColor}>余额</Text>
|
||||||
<Text className={'text-xl text-white'} style={themeStyles.textColor}>{ticketTotal}</Text>
|
<Text className={'text-xl text-white'} style={themeStyles.textColor}>{data?.balance || '0.00'}</Text>
|
||||||
|
</View>
|
||||||
|
<View className={'item flex justify-center flex-col items-center'}>
|
||||||
|
<Text className={'text-xs text-gray-200'} style={themeStyles.textColor}>积分</Text>
|
||||||
|
<Text className={'text-xl text-white'} style={themeStyles.textColor}>{data?.points || 0}</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -47,8 +47,9 @@ const UserFooter = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={'text-center py-4 w-full text-gray-300'} onClick={onLoginByPhone}>
|
<div className={'text-center py-4 w-full text-gray-300'} onClick={onLoginByPhone}>
|
||||||
<div className={'text-xs text-gray-400 py-1'}>当前版本:{Version}</div>
|
{/*<div className={'text-xs text-gray-400 py-1'}>当前版本:{Version}</div>*/}
|
||||||
<div className={'text-xs text-gray-400 py-1'}>Copyright © { new Date().getFullYear() } {Copyright}</div>
|
{/*<div className={'text-xs text-gray-400 py-1'}>Copyright © { new Date().getFullYear() } {Copyright}</div>*/}
|
||||||
|
<div className={'text-xs text-gray-400 py-1'}>{Copyright}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Popup
|
<Popup
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ const UserCell = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<View className="bg-white mx-4 mt-4 rounded-xl">
|
<View className="bg-white mx-4 mt-4 rounded-xl">
|
||||||
<View className="font-semibold text-gray-800 pt-4 pl-4">我的服务</View>
|
<View className="font-semibold text-gray-800 pt-4 pl-4">桂乐淘服务中心</View>
|
||||||
<ConfigProvider>
|
<ConfigProvider>
|
||||||
<Grid
|
<Grid
|
||||||
columns={4}
|
columns={4}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {useEffect, useRef} from 'react'
|
import {useEffect, useRef, useState} from 'react'
|
||||||
import {PullToRefresh} from '@nutui/nutui-react-taro'
|
import {PullToRefresh} from '@nutui/nutui-react-taro'
|
||||||
import UserCard from "./components/UserCard";
|
import UserCard from "./components/UserCard";
|
||||||
import UserOrder from "./components/UserOrder";
|
import UserOrder from "./components/UserOrder";
|
||||||
@@ -14,12 +14,15 @@ function User() {
|
|||||||
|
|
||||||
const userCardRef = useRef<any>()
|
const userCardRef = useRef<any>()
|
||||||
const themeStyles = useThemeStyles();
|
const themeStyles = useThemeStyles();
|
||||||
|
// TabBar 页在小程序里通常不会销毁;从“注册/申请”页返回时需要触发子组件重新初始化/拉取最新状态。
|
||||||
|
const [dealerViewKey, setDealerViewKey] = useState(0)
|
||||||
|
|
||||||
// 下拉刷新处理
|
// 下拉刷新处理
|
||||||
const handleRefresh = async () => {
|
const handleRefresh = async () => {
|
||||||
if (userCardRef.current?.handleRefresh) {
|
if (userCardRef.current?.handleRefresh) {
|
||||||
await userCardRef.current.handleRefresh()
|
await userCardRef.current.handleRefresh()
|
||||||
}
|
}
|
||||||
|
setDealerViewKey(v => v + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -28,6 +31,9 @@ function User() {
|
|||||||
// 每次进入/切回个人中心都刷新一次统计(包含水票数量)
|
// 每次进入/切回个人中心都刷新一次统计(包含水票数量)
|
||||||
useDidShow(() => {
|
useDidShow(() => {
|
||||||
userCardRef.current?.reloadStats?.()
|
userCardRef.current?.reloadStats?.()
|
||||||
|
// 个人资料(头像/昵称)可能在其它页面被修改,这里确保返回时立刻刷新
|
||||||
|
userCardRef.current?.reloadUserInfo?.()
|
||||||
|
setDealerViewKey(v => v + 1)
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -56,7 +62,7 @@ function User() {
|
|||||||
</View>
|
</View>
|
||||||
<UserCard ref={userCardRef}/>
|
<UserCard ref={userCardRef}/>
|
||||||
<UserOrder/>
|
<UserOrder/>
|
||||||
<IsDealer/>
|
<IsDealer key={dealerViewKey}/>
|
||||||
<UserGrid/>
|
<UserGrid/>
|
||||||
<UserFooter/>
|
<UserFooter/>
|
||||||
</PullToRefresh>
|
</PullToRefresh>
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ const DealerIndex: React.FC = () => {
|
|||||||
{dealerUser && (
|
{dealerUser && (
|
||||||
<View className="mx-4 -mt-6 rounded-xl p-4 relative z-10" style={cardGradients.elevated}>
|
<View className="mx-4 -mt-6 rounded-xl p-4 relative z-10" style={cardGradients.elevated}>
|
||||||
<View className="mb-4">
|
<View className="mb-4">
|
||||||
<Text className="font-semibold text-gray-800">工资统计</Text>
|
<Text className="font-semibold text-gray-800">配送提成</Text>
|
||||||
</View>
|
</View>
|
||||||
<View className="grid grid-cols-3 gap-3">
|
<View className="grid grid-cols-3 gap-3">
|
||||||
<View className="text-center p-3 rounded-lg flex flex-col" style={{
|
<View className="text-center p-3 rounded-lg flex flex-col" style={{
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
|
||||||
|
.goods-grid {
|
||||||
|
margin-top: 18rpx;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-card {
|
||||||
|
border-radius: 22rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 18rpx 36rpx rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-card__imgWrap {
|
||||||
|
padding: 18rpx 18rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-card__img {
|
||||||
|
width: 100%;
|
||||||
|
height: 280rpx;
|
||||||
|
border-radius: 18rpx;
|
||||||
|
background: #f4f4f4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-card__body {
|
||||||
|
padding: 18rpx 18rpx 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-card__title {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1c1c1c;
|
||||||
|
min-height: 72rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-card__meta {
|
||||||
|
margin-top: 10rpx;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-card__sold {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #9a9a9a;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-card__price {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 4rpx;
|
||||||
|
color: #27c86b;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-card__priceUnit {
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-card__priceValue {
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-card__actions {
|
||||||
|
margin-top: 16rpx;
|
||||||
|
display: flex;
|
||||||
|
gap: 14rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-card__btn {
|
||||||
|
flex: 1;
|
||||||
|
height: 64rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-card__btn--ghost {
|
||||||
|
border: 2rpx solid rgba(32, 194, 106, 0.7);
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-card__btn--primary {
|
||||||
|
background: linear-gradient(90deg, #24d34c 0%, #6df09a 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-card__btnText {
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #18b85a;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-card__btnText--primary {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buy-btn{
|
||||||
|
height: 70px;
|
||||||
|
background: linear-gradient(to bottom, #1cd98a, #24ca94);
|
||||||
|
border-radius: 100px;
|
||||||
|
color: #ffffff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-around;
|
||||||
|
.cart-icon{
|
||||||
|
background: linear-gradient(to bottom, #bbe094, #4ee265);
|
||||||
|
border-radius: 100px 0 0 100px;
|
||||||
|
height: 70px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,51 +1,57 @@
|
|||||||
import {Image} from '@nutui/nutui-react-taro'
|
import {Image} from '@nutui/nutui-react-taro'
|
||||||
import {Share} from '@nutui/icons-react-taro'
|
|
||||||
import {View, Text} from '@tarojs/components'
|
import {View, Text} from '@tarojs/components'
|
||||||
import Taro from '@tarojs/taro'
|
import Taro from '@tarojs/taro'
|
||||||
import './GoodsList.scss'
|
import './GoodsList.scss'
|
||||||
|
import {ShopGoods} from "@/api/shop/shopGoods/model";
|
||||||
|
|
||||||
|
|
||||||
const GoodsList = (props: any) => {
|
const GoodsList = (props: any) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<View className={'py-3'}>
|
<View className={'p-3'}>
|
||||||
<View className={'flex flex-col justify-between items-center rounded-lg px-2'}>
|
|
||||||
{props.data?.map((item: any, index: number) => {
|
<View className="goods-grid">
|
||||||
return (
|
{props.data?.map((item: ShopGoods) => (
|
||||||
<View key={index} className={'flex flex-col rounded-lg bg-white shadow-sm w-full mb-5'}>
|
<View key={item.goodsId} className="goods-card">
|
||||||
<Image src={item.image} mode={'aspectFit'} lazyLoad={false}
|
<View className="goods-card__imgWrap">
|
||||||
radius="10px 10px 0 0" height="180"
|
<Image
|
||||||
onClick={() => Taro.navigateTo({url: '/shop/goodsDetail/index?id=' + item.goodsId})}/>
|
className="goods-card__img"
|
||||||
<View className={'flex flex-col p-2 rounded-lg'}>
|
src={item.image || ''}
|
||||||
<View>
|
mode="aspectFill"
|
||||||
<View className={'car-no text-sm'}>{item.name}</View>
|
width="100%"
|
||||||
<View className={'flex justify-between text-xs py-1'}>
|
height="280rpx"
|
||||||
<Text className={'text-orange-500'}>{item.comments}</Text>
|
radius="18rpx"
|
||||||
<Text className={'text-gray-400'}>已售 {item.sales}</Text>
|
lazyLoad={false}
|
||||||
</View>
|
onClick={() =>
|
||||||
<View className={'flex justify-between items-center py-2'}>
|
Taro.navigateTo({ url: `/shop/goodsDetail/index?id=${item.goodsId}` })
|
||||||
<View className={'flex text-red-500 text-xl items-baseline'}>
|
}
|
||||||
<Text className={'text-xs'}>¥</Text>
|
/>
|
||||||
<Text className={'font-bold text-2xl'}>{item.price}</Text>
|
</View>
|
||||||
<Text className={'text-xs px-1'}>会员价</Text>
|
|
||||||
<Text className={'text-xs text-gray-400 line-through'}>¥{item.salePrice}</Text>
|
<View className="goods-card__body">
|
||||||
</View>
|
<Text className="goods-card__title">{item.name}</Text>
|
||||||
<View className={'buy-btn'}>
|
<View className="goods-card__meta">
|
||||||
<View className={'cart-icon'}>
|
<Text className="goods-card__sold">已购:{item.sales || 0}人</Text>
|
||||||
<Share size={20} className={'mx-4 mt-2'}
|
<View className="goods-card__price">
|
||||||
onClick={() => Taro.navigateTo({url: '/shop/goodsDetail/index?id=' + item.goodsId})}/>
|
<Text className="goods-card__priceUnit">¥</Text>
|
||||||
</View>
|
<Text className="goods-card__priceValue">{item.buyingPrice}</Text>
|
||||||
<View className={'text-white pl-4 pr-5'}
|
</View>
|
||||||
onClick={() => Taro.navigateTo({url: '/shop/goodsDetail/index?id=' + item.goodsId})}>购买
|
</View>
|
||||||
</View>
|
|
||||||
</View>
|
<View className="goods-card__actions">
|
||||||
</View>
|
<View
|
||||||
|
className="goods-card__btn goods-card__btn--primary"
|
||||||
|
onClick={() =>
|
||||||
|
Taro.navigateTo({ url: `/shop/goodsDetail/index?id=${item.goodsId}` })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text className="goods-card__btnText goods-card__btnText--primary">立即购买</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)
|
</View>
|
||||||
})}
|
))}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Taro from '@tarojs/taro'
|
import Taro from '@tarojs/taro'
|
||||||
import GoodsList from './components/GoodsList'
|
import GoodsList from './components/GoodsList'
|
||||||
import {useShareAppMessage} from "@tarojs/taro"
|
import {useShareAppMessage} from "@tarojs/taro"
|
||||||
import {Loading} from '@nutui/nutui-react-taro'
|
import {Loading,Empty} from '@nutui/nutui-react-taro'
|
||||||
import {useEffect, useState} from "react"
|
import {useEffect, useState} from "react"
|
||||||
import {useRouter} from '@tarojs/taro'
|
import {useRouter} from '@tarojs/taro'
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
@@ -21,7 +21,7 @@ function Category() {
|
|||||||
// 1.加载远程数据
|
// 1.加载远程数据
|
||||||
const id = Number(params.id)
|
const id = Number(params.id)
|
||||||
const nav = await getCmsNavigation(id)
|
const nav = await getCmsNavigation(id)
|
||||||
const shopGoods = await pageShopGoods({categoryId: id})
|
const shopGoods = await pageShopGoods({categoryId: id, status: 0})
|
||||||
|
|
||||||
// 2.处理业务逻辑
|
// 2.处理业务逻辑
|
||||||
setCategoryId(id)
|
setCategoryId(id)
|
||||||
@@ -59,6 +59,12 @@ function Category() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(list.length == 0){
|
||||||
|
return (
|
||||||
|
<Empty description="暂无数据"/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={'flex flex-col'}>
|
<div className={'flex flex-col'}>
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import {useCart} from "@/hooks/useCart";
|
|||||||
import {useConfig} from "@/hooks/useConfig";
|
import {useConfig} from "@/hooks/useConfig";
|
||||||
import {parseInviteParams, saveInviteParams, trackInviteSource} from "@/utils/invite";
|
import {parseInviteParams, saveInviteParams, trackInviteSource} from "@/utils/invite";
|
||||||
import { ensureLoggedIn } from '@/utils/auth'
|
import { ensureLoggedIn } from '@/utils/auth'
|
||||||
|
import {getGltTicketTemplateByGoodsId} from "@/api/glt/gltTicketTemplate";
|
||||||
|
import type {GltTicketTemplate} from "@/api/glt/gltTicketTemplate/model";
|
||||||
|
|
||||||
const GoodsDetail = () => {
|
const GoodsDetail = () => {
|
||||||
const [statusBarHeight, setStatusBarHeight] = useState<number>(44);
|
const [statusBarHeight, setStatusBarHeight] = useState<number>(44);
|
||||||
@@ -32,6 +34,9 @@ const GoodsDetail = () => {
|
|||||||
title: '',
|
title: '',
|
||||||
content: ''
|
content: ''
|
||||||
})
|
})
|
||||||
|
// 水票套票模板:存在时该商品不允许加入购物车(购物车无法支付此类商品)
|
||||||
|
const [ticketTemplate, setTicketTemplate] = useState<GltTicketTemplate | null>(null)
|
||||||
|
const [ticketTemplateChecked, setTicketTemplateChecked] = useState(false)
|
||||||
// const [selectedSku, setSelectedSku] = useState<ShopGoodsSku | null>(null);
|
// const [selectedSku, setSelectedSku] = useState<ShopGoodsSku | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const router = Taro.getCurrentInstance().router;
|
const router = Taro.getCurrentInstance().router;
|
||||||
@@ -60,9 +65,29 @@ const GoodsDetail = () => {
|
|||||||
}, [goodsId])
|
}, [goodsId])
|
||||||
|
|
||||||
// 处理加入购物车
|
// 处理加入购物车
|
||||||
const handleAddToCart = () => {
|
const handleAddToCart = async () => {
|
||||||
if (!goods) return;
|
if (!goods) return;
|
||||||
|
|
||||||
|
// 水票套票商品:不允许加入购物车(购物车无法支付)
|
||||||
|
// 优先使用已加载的 ticketTemplate;若尚未加载则补一次查询
|
||||||
|
let tpl = ticketTemplate
|
||||||
|
let checked = ticketTemplateChecked
|
||||||
|
if (!tpl && goods?.goodsId) {
|
||||||
|
try {
|
||||||
|
tpl = await getGltTicketTemplateByGoodsId(Number(goods.goodsId))
|
||||||
|
setTicketTemplate(tpl)
|
||||||
|
setTicketTemplateChecked(true)
|
||||||
|
checked = true
|
||||||
|
} catch (_e) {
|
||||||
|
tpl = null
|
||||||
|
setTicketTemplateChecked(true)
|
||||||
|
checked = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!checked || tpl) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!ensureLoggedIn(`/shop/goodsDetail/index?id=${goods.goodsId}`)) return
|
if (!ensureLoggedIn(`/shop/goodsDetail/index?id=${goods.goodsId}`)) return
|
||||||
|
|
||||||
// 如果有规格,显示规格选择器
|
// 如果有规格,显示规格选择器
|
||||||
@@ -99,11 +124,30 @@ const GoodsDetail = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 规格选择确认回调
|
// 规格选择确认回调
|
||||||
const handleSpecConfirm = (sku: ShopGoodsSku, quantity: number, action?: 'cart' | 'buy') => {
|
const handleSpecConfirm = async (sku: ShopGoodsSku, quantity: number, action?: 'cart' | 'buy') => {
|
||||||
// setSelectedSku(sku);
|
// setSelectedSku(sku);
|
||||||
setShowSpecSelector(false);
|
setShowSpecSelector(false);
|
||||||
|
|
||||||
if (action === 'cart') {
|
if (action === 'cart') {
|
||||||
|
// 水票套票商品:不允许加入购物车(购物车无法支付)
|
||||||
|
let tpl = ticketTemplate
|
||||||
|
let checked = ticketTemplateChecked
|
||||||
|
if (!tpl && goods?.goodsId) {
|
||||||
|
try {
|
||||||
|
tpl = await getGltTicketTemplateByGoodsId(Number(goods.goodsId))
|
||||||
|
setTicketTemplate(tpl)
|
||||||
|
setTicketTemplateChecked(true)
|
||||||
|
checked = true
|
||||||
|
} catch (_e) {
|
||||||
|
tpl = null
|
||||||
|
setTicketTemplateChecked(true)
|
||||||
|
checked = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!checked || tpl) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 加入购物车
|
// 加入购物车
|
||||||
addToCart({
|
addToCart({
|
||||||
goodsId: goods!.goodsId!,
|
goodsId: goods!.goodsId!,
|
||||||
@@ -143,14 +187,19 @@ const GoodsDetail = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
let alive = true
|
||||||
Taro.getSystemInfo({
|
Taro.getSystemInfo({
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
|
if (!alive) return
|
||||||
setWindowWidth(res.windowWidth)
|
setWindowWidth(res.windowWidth)
|
||||||
setStatusBarHeight(Number(res.statusBarHeight) + 5)
|
setStatusBarHeight(Number(res.statusBarHeight) + 5)
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (goodsId) {
|
if (goodsId) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
// 切换商品时先重置套票模板,避免复用上一个商品状态
|
||||||
|
setTicketTemplate(null)
|
||||||
|
setTicketTemplateChecked(false)
|
||||||
|
|
||||||
// 加载商品详情
|
// 加载商品详情
|
||||||
getShopGoods(Number(goodsId))
|
getShopGoods(Number(goodsId))
|
||||||
@@ -159,6 +208,7 @@ const GoodsDetail = () => {
|
|||||||
if (res.content) {
|
if (res.content) {
|
||||||
res.content = wxParse(res.content);
|
res.content = wxParse(res.content);
|
||||||
}
|
}
|
||||||
|
if (!alive) return
|
||||||
setGoods(res);
|
setGoods(res);
|
||||||
if (res.files) {
|
if (res.files) {
|
||||||
const arr = JSON.parse(res.files);
|
const arr = JSON.parse(res.files);
|
||||||
@@ -169,12 +219,27 @@ const GoodsDetail = () => {
|
|||||||
console.error("Failed to fetch goods detail:", error);
|
console.error("Failed to fetch goods detail:", error);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
|
if (!alive) return
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 查询商品是否绑定水票模板(失败/无数据时不影响正常浏览)
|
||||||
|
getGltTicketTemplateByGoodsId(Number(goodsId))
|
||||||
|
.then((tpl) => {
|
||||||
|
if (!alive) return
|
||||||
|
setTicketTemplate(tpl)
|
||||||
|
setTicketTemplateChecked(true)
|
||||||
|
})
|
||||||
|
.catch((_e) => {
|
||||||
|
if (!alive) return
|
||||||
|
setTicketTemplate(null)
|
||||||
|
setTicketTemplateChecked(true)
|
||||||
|
})
|
||||||
|
|
||||||
// 加载商品规格
|
// 加载商品规格
|
||||||
listShopGoodsSpec({goodsId: Number(goodsId)} as any)
|
listShopGoodsSpec({goodsId: Number(goodsId)} as any)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
|
if (!alive) return
|
||||||
setSpecs(data || []);
|
setSpecs(data || []);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -184,12 +249,16 @@ const GoodsDetail = () => {
|
|||||||
// 加载商品SKU
|
// 加载商品SKU
|
||||||
listShopGoodsSku({goodsId: Number(goodsId)} as any)
|
listShopGoodsSku({goodsId: Number(goodsId)} as any)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
|
if (!alive) return
|
||||||
setSkus(data || []);
|
setSkus(data || []);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error("Failed to fetch goods skus:", error);
|
console.error("Failed to fetch goods skus:", error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
return () => {
|
||||||
|
alive = false
|
||||||
|
}
|
||||||
}, [goodsId]);
|
}, [goodsId]);
|
||||||
|
|
||||||
// 分享给好友
|
// 分享给好友
|
||||||
@@ -227,6 +296,8 @@ const GoodsDetail = () => {
|
|||||||
return <View>加载中...</View>;
|
return <View>加载中...</View>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showAddToCart = ticketTemplateChecked && !ticketTemplate
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className={"py-0"}>
|
<View className={"py-0"}>
|
||||||
<View
|
<View
|
||||||
@@ -295,9 +366,9 @@ const GoodsDetail = () => {
|
|||||||
<View className={'flex justify-between'}>
|
<View className={'flex justify-between'}>
|
||||||
<View className={'flex text-red-500 text-xl items-baseline'}>
|
<View className={'flex text-red-500 text-xl items-baseline'}>
|
||||||
<Text className={'text-xs'}>¥</Text>
|
<Text className={'text-xs'}>¥</Text>
|
||||||
<Text className={'font-bold text-2xl'}>{goods.price}</Text>
|
<Text className={'font-bold text-2xl'}>{goods.buyingPrice}</Text>
|
||||||
<Text className={'text-xs px-1'}>会员价</Text>
|
<Text className={'text-xs px-1'}>会员价</Text>
|
||||||
<Text className={'text-xs text-gray-400 line-through'}>¥{goods.salePrice}</Text>
|
<Text className={'text-xs text-gray-400 line-through'}>¥{goods.salePrice}/{goods.unitName}</Text>
|
||||||
</View>
|
</View>
|
||||||
<span className={"text-gray-400 text-xs"}>已售 {goods.sales}</span>
|
<span className={"text-gray-400 text-xs"}>已售 {goods.sales}</span>
|
||||||
</View>
|
</View>
|
||||||
@@ -385,10 +456,12 @@ const GoodsDetail = () => {
|
|||||||
</button>
|
</button>
|
||||||
</View>
|
</View>
|
||||||
<View className={'buy-btn mx-4'}>
|
<View className={'buy-btn mx-4'}>
|
||||||
<View className={'cart-add px-4 text-sm'}
|
{showAddToCart && (
|
||||||
onClick={() => handleAddToCart()}>加入购物车
|
<View className={'cart-add px-4 text-sm'}
|
||||||
</View>
|
onClick={() => handleAddToCart()}>加入购物车
|
||||||
<View className={'cart-buy pl-4 pr-5 text-sm'}
|
</View>
|
||||||
|
)}
|
||||||
|
<View className={`cart-buy text-sm ${showAddToCart ? 'pl-4 pr-5' : 'cart-buy-only px-4'}`}
|
||||||
onClick={() => handleBuyNow()}>立即购买
|
onClick={() => handleBuyNow()}>立即购买
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -58,7 +58,16 @@ const OrderConfirm = () => {
|
|||||||
const [error, setError] = useState<string>('')
|
const [error, setError] = useState<string>('')
|
||||||
const [payLoading, setPayLoading] = useState<boolean>(false)
|
const [payLoading, setPayLoading] = useState<boolean>(false)
|
||||||
// 配送时间(仅水票套票商品需要)
|
// 配送时间(仅水票套票商品需要)
|
||||||
const [sendTime, setSendTime] = useState<Date>(() => dayjs().startOf('day').toDate())
|
// 当日截单时间:超过该时间下单,最早配送日顺延到次日(避免 21:00 下单仍显示“当天配送”)
|
||||||
|
const DELIVERY_CUTOFF_HOUR = 21
|
||||||
|
const getMinSendDate = () => {
|
||||||
|
const now = dayjs()
|
||||||
|
const cutoff = now.hour(DELIVERY_CUTOFF_HOUR).minute(0).second(0).millisecond(0)
|
||||||
|
const startOfToday = now.startOf('day')
|
||||||
|
// >= 截单时间则最早只能选次日
|
||||||
|
return now.isSame(cutoff) || now.isAfter(cutoff) ? startOfToday.add(1, 'day') : startOfToday
|
||||||
|
}
|
||||||
|
const [sendTime, setSendTime] = useState<Date>(() => getMinSendDate().toDate())
|
||||||
const [sendTimePickerVisible, setSendTimePickerVisible] = useState(false)
|
const [sendTimePickerVisible, setSendTimePickerVisible] = useState(false)
|
||||||
|
|
||||||
// 水票套票活动(若存在则按规则限制最小购买量等)
|
// 水票套票活动(若存在则按规则限制最小购买量等)
|
||||||
@@ -421,6 +430,7 @@ const OrderConfirm = () => {
|
|||||||
* 统一支付入口
|
* 统一支付入口
|
||||||
*/
|
*/
|
||||||
const onPay = async (goods: ShopGoods) => {
|
const onPay = async (goods: ShopGoods) => {
|
||||||
|
let skipFinallyResetPayLoading = false
|
||||||
try {
|
try {
|
||||||
setPayLoading(true)
|
setPayLoading(true)
|
||||||
|
|
||||||
@@ -446,6 +456,17 @@ const OrderConfirm = () => {
|
|||||||
Taro.showToast({ title: '请选择配送时间', icon: 'none' })
|
Taro.showToast({ title: '请选择配送时间', icon: 'none' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (hasTicketTemplate) {
|
||||||
|
const min = getMinSendDate()
|
||||||
|
if (dayjs(sendTime).isBefore(min, 'day')) {
|
||||||
|
setSendTime(min.toDate())
|
||||||
|
Taro.showToast({
|
||||||
|
title: `已过当日${DELIVERY_CUTOFF_HOUR}点截单,最早配送:${min.format('YYYY-MM-DD')}`,
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 水票套票活动:最小购买量校验
|
// 水票套票活动:最小购买量校验
|
||||||
if (isTicketTemplateActive && quantity < minBuyQty) {
|
if (isTicketTemplateActive && quantity < minBuyQty) {
|
||||||
@@ -583,6 +604,29 @@ const OrderConfirm = () => {
|
|||||||
// })
|
// })
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
const message = String(error?.message || '')
|
const message = String(error?.message || '')
|
||||||
|
const isUserCancelPay =
|
||||||
|
message.includes('用户取消支付') ||
|
||||||
|
message.includes('取消支付') ||
|
||||||
|
message.toLowerCase().includes('requestpayment:fail cancel') ||
|
||||||
|
message.toLowerCase().includes('cancel')
|
||||||
|
|
||||||
|
// 用户取消支付:跳转到待付款列表,方便继续支付
|
||||||
|
if (isUserCancelPay) {
|
||||||
|
skipFinallyResetPayLoading = true
|
||||||
|
setPayLoading(false)
|
||||||
|
const url = '/user/order/order?statusFilter=0'
|
||||||
|
try {
|
||||||
|
await Taro.redirectTo({ url })
|
||||||
|
} catch (_e) {
|
||||||
|
try {
|
||||||
|
await Taro.navigateTo({ url })
|
||||||
|
} catch (_e2) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const isOutOfDeliveryRange =
|
const isOutOfDeliveryRange =
|
||||||
message.includes('不在配送范围') ||
|
message.includes('不在配送范围') ||
|
||||||
message.includes('配送范围') ||
|
message.includes('配送范围') ||
|
||||||
@@ -612,7 +656,9 @@ const OrderConfirm = () => {
|
|||||||
Taro.showToast({ title: message || '支付失败,请重试', icon: 'none' })
|
Taro.showToast({ title: message || '支付失败,请重试', icon: 'none' })
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setPayLoading(false)
|
if (!skipFinallyResetPayLoading) {
|
||||||
|
setPayLoading(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -716,7 +762,7 @@ const OrderConfirm = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 切换商品时重置配送时间,避免沿用上一次选择
|
// 切换商品时重置配送时间,避免沿用上一次选择
|
||||||
if (!isLoggedIn()) return
|
if (!isLoggedIn()) return
|
||||||
setSendTime(dayjs().startOf('day').toDate())
|
setSendTime(getMinSendDate().toDate())
|
||||||
setSendTimePickerVisible(false)
|
setSendTimePickerVisible(false)
|
||||||
loadAllData()
|
loadAllData()
|
||||||
}, [goodsId]);
|
}, [goodsId]);
|
||||||
@@ -786,7 +832,14 @@ const OrderConfirm = () => {
|
|||||||
<ArrowRight className={'text-gray-400'} size={14}/>
|
<ArrowRight className={'text-gray-400'} size={14}/>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
onClick={() => setSendTimePickerVisible(true)}
|
onClick={() => {
|
||||||
|
// 若页面停留跨过截单时间,打开选择器前再校正一次最早可选日期
|
||||||
|
const min = getMinSendDate()
|
||||||
|
if (dayjs(sendTime).isBefore(min, 'day')) {
|
||||||
|
setSendTime(min.toDate())
|
||||||
|
}
|
||||||
|
setSendTimePickerVisible(true)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</CellGroup>
|
</CellGroup>
|
||||||
)}
|
)}
|
||||||
@@ -822,19 +875,21 @@ const OrderConfirm = () => {
|
|||||||
</View>
|
</View>
|
||||||
<View className={'flex flex-col w-full ml-2'} style={{width: '100%'}}>
|
<View className={'flex flex-col w-full ml-2'} style={{width: '100%'}}>
|
||||||
<Text className={'font-medium w-full'}>{goods.name}</Text>
|
<Text className={'font-medium w-full'}>{goods.name}</Text>
|
||||||
<Text className={'number text-gray-400 text-sm py-2'}>80g/袋</Text>
|
{/*<Text className={'number text-gray-400 text-sm py-2'}>80g/袋</Text>*/}
|
||||||
<View className={'flex justify-between items-center'}>
|
<View className={'flex justify-between items-center'}>
|
||||||
<Text className={'text-red-500'}>¥{goods.price}</Text>
|
<Text className={'text-red-500'}>¥{goods.price}</Text>
|
||||||
<View className={'flex flex-col items-end gap-1'}>
|
<View className={'flex flex-col items-end gap-1'}>
|
||||||
<ConfigProvider theme={customTheme}>
|
<ConfigProvider theme={customTheme}>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
value={quantity}
|
value={quantity}
|
||||||
min={isTicketTemplateActive ? minBuyQty : 1}
|
min={isTicketTemplateActive ? minBuyQty : 1}
|
||||||
max={goods.stock || 999}
|
max={goods.stock || 999}
|
||||||
disabled={((goods.canBuyNumber ?? 0) !== 0) && !isTicketTemplateActive}
|
step={minBuyQty === 1 ? 1 : 10}
|
||||||
onChange={handleQuantityChange}
|
readOnly
|
||||||
/>
|
disabled={((goods.canBuyNumber ?? 0) !== 0) && !isTicketTemplateActive}
|
||||||
</ConfigProvider>
|
onChange={handleQuantityChange}
|
||||||
|
/>
|
||||||
|
</ConfigProvider>
|
||||||
{goods.stock !== undefined && (
|
{goods.stock !== undefined && (
|
||||||
<Text className={'text-xs text-gray-400'}>
|
<Text className={'text-xs text-gray-400'}>
|
||||||
库存 {goods.stock} 件
|
库存 {goods.stock} 件
|
||||||
@@ -1087,7 +1142,7 @@ const OrderConfirm = () => {
|
|||||||
visible={sendTimePickerVisible}
|
visible={sendTimePickerVisible}
|
||||||
title="选择配送时间"
|
title="选择配送时间"
|
||||||
type="date"
|
type="date"
|
||||||
startDate={dayjs().startOf('day').toDate()}
|
startDate={getMinSendDate().toDate()}
|
||||||
endDate={dayjs().add(30, 'day').toDate()}
|
endDate={dayjs().add(30, 'day').toDate()}
|
||||||
value={sendTime}
|
value={sendTime}
|
||||||
onClose={() => setSendTimePickerVisible(false)}
|
onClose={() => setSendTimePickerVisible(false)}
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ const OrderConfirm = () => {
|
|||||||
}} lazyLoad={false}/>
|
}} lazyLoad={false}/>
|
||||||
<View className={'flex flex-col'}>
|
<View className={'flex flex-col'}>
|
||||||
<View className={'font-medium w-full'}>{item.name}</View>
|
<View className={'font-medium w-full'}>{item.name}</View>
|
||||||
<View className={'number text-gray-400 text-sm py-2'}>80g/袋</View>
|
{/*<View className={'number text-gray-400 text-sm py-2'}>80g/袋</View>*/}
|
||||||
<Space className={'flex justify-start items-center'}>
|
<Space className={'flex justify-start items-center'}>
|
||||||
<View className={'text-red-500'}>¥{item.price}</View>
|
<View className={'text-red-500'}>¥{item.price}</View>
|
||||||
<View className={'text-gray-500 text-sm'}>x {item.quantity}</View>
|
<View className={'text-gray-500 text-sm'}>x {item.quantity}</View>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {Cell, CellGroup, Image, Space, Button, Dialog} from '@nutui/nutui-react-
|
|||||||
import Taro from '@tarojs/taro'
|
import Taro from '@tarojs/taro'
|
||||||
import {View} from '@tarojs/components'
|
import {View} from '@tarojs/components'
|
||||||
import {ShopOrder} from "@/api/shop/shopOrder/model";
|
import {ShopOrder} from "@/api/shop/shopOrder/model";
|
||||||
import {getShopOrder, updateShopOrder, refundShopOrder} from "@/api/shop/shopOrder";
|
import {getShopOrder, updateShopOrder} from "@/api/shop/shopOrder";
|
||||||
import {listShopOrderGoods} from "@/api/shop/shopOrderGoods";
|
import {listShopOrderGoods} from "@/api/shop/shopOrderGoods";
|
||||||
import {ShopOrderGoods} from "@/api/shop/shopOrderGoods/model";
|
import {ShopOrderGoods} from "@/api/shop/shopOrderGoods/model";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
@@ -69,7 +69,7 @@ const OrderDetail = () => {
|
|||||||
Taro.showLoading({ title: '提交中...' })
|
Taro.showLoading({ title: '提交中...' })
|
||||||
|
|
||||||
// 退款相关操作使用退款接口:PUT /api/shop/shop-order/refund
|
// 退款相关操作使用退款接口:PUT /api/shop/shop-order/refund
|
||||||
await refundShopOrder({
|
await updateShopOrder({
|
||||||
orderId: order.orderId,
|
orderId: order.orderId,
|
||||||
refundMoney: order.payPrice || order.totalPrice,
|
refundMoney: order.payPrice || order.totalPrice,
|
||||||
orderStatus: 7
|
orderStatus: 7
|
||||||
|
|||||||
@@ -47,11 +47,17 @@ const AddUserAddress = () => {
|
|||||||
const [FormData, setFormData] = useState<ShopUserAddress>({})
|
const [FormData, setFormData] = useState<ShopUserAddress>({})
|
||||||
const [inputText, setInputText] = useState<string>('')
|
const [inputText, setInputText] = useState<string>('')
|
||||||
const [selectedLocation, setSelectedLocation] = useState<SelectedLocation | null>(null)
|
const [selectedLocation, setSelectedLocation] = useState<SelectedLocation | null>(null)
|
||||||
|
const [regionLocked, setRegionLocked] = useState(false)
|
||||||
const formRef = useRef<any>(null)
|
const formRef = useRef<any>(null)
|
||||||
|
const wxDraftRef = useRef<Partial<ShopUserAddress> | null>(null)
|
||||||
|
const wxDraftPatchedRef = useRef(false)
|
||||||
|
|
||||||
// 判断是编辑还是新增模式
|
// 判断是编辑还是新增模式
|
||||||
const isEditMode = !!params.id
|
const isEditMode = !!params.id
|
||||||
const addressId = params.id ? Number(params.id) : undefined
|
const addressId = params.id ? Number(params.id) : undefined
|
||||||
|
const fromWx = params.fromWx === '1' || params.fromWx === 'true'
|
||||||
|
const skipDefaultCheck =
|
||||||
|
fromWx || params.skipDefaultCheck === '1' || params.skipDefaultCheck === 'true'
|
||||||
|
|
||||||
const reload = async () => {
|
const reload = async () => {
|
||||||
// 整理地区数据
|
// 整理地区数据
|
||||||
@@ -59,7 +65,7 @@ const AddUserAddress = () => {
|
|||||||
|
|
||||||
// 新增模式:若存在“默认地址”但缺少经纬度,则强制引导到编辑页完善定位
|
// 新增模式:若存在“默认地址”但缺少经纬度,则强制引导到编辑页完善定位
|
||||||
// 目的:确保业务依赖默认地址坐标(选店/配送范围)时不会因为缺失坐标而失败
|
// 目的:确保业务依赖默认地址坐标(选店/配送范围)时不会因为缺失坐标而失败
|
||||||
if (!isEditMode) {
|
if (!isEditMode && !skipDefaultCheck) {
|
||||||
try {
|
try {
|
||||||
const defaultList = await listShopUserAddress({ isDefault: true })
|
const defaultList = await listShopUserAddress({ isDefault: true })
|
||||||
const defaultAddr = defaultList?.[0]
|
const defaultAddr = defaultList?.[0]
|
||||||
@@ -82,6 +88,31 @@ const AddUserAddress = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 微信地址导入:先用微信返回的字段预填表单,让用户手动选择定位后再保存
|
||||||
|
if (!isEditMode && fromWx && !wxDraftPatchedRef.current) {
|
||||||
|
try {
|
||||||
|
const draft = Taro.getStorageSync('WxAddressDraft')
|
||||||
|
if (draft) {
|
||||||
|
wxDraftPatchedRef.current = true
|
||||||
|
wxDraftRef.current = draft as any
|
||||||
|
Taro.removeStorageSync('WxAddressDraft')
|
||||||
|
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
...(draft as any)
|
||||||
|
}))
|
||||||
|
|
||||||
|
const p = String((draft as any)?.province || '').trim()
|
||||||
|
const c = String((draft as any)?.city || '').trim()
|
||||||
|
const r = String((draft as any)?.region || '').trim()
|
||||||
|
const regionText = [p, c, r].filter(Boolean).join(' ')
|
||||||
|
if (regionText) setText(regionText)
|
||||||
|
}
|
||||||
|
} catch (_e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 如果是编辑模式,加载地址数据
|
// 如果是编辑模式,加载地址数据
|
||||||
if (isEditMode && addressId) {
|
if (isEditMode && addressId) {
|
||||||
try {
|
try {
|
||||||
@@ -90,7 +121,12 @@ const AddUserAddress = () => {
|
|||||||
// 设置所在地区
|
// 设置所在地区
|
||||||
setText(`${address.province} ${address.city} ${address.region}`)
|
setText(`${address.province} ${address.city} ${address.region}`)
|
||||||
// 回显已保存的经纬度(编辑模式)
|
// 回显已保存的经纬度(编辑模式)
|
||||||
if (hasValidLngLat(address)) setSelectedLocation({ lng: String(address.lng), lat: String(address.lat) })
|
if (hasValidLngLat(address)) {
|
||||||
|
setSelectedLocation({ lng: String(address.lng), lat: String(address.lat) })
|
||||||
|
setRegionLocked(true)
|
||||||
|
} else {
|
||||||
|
setRegionLocked(false)
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载地址失败:', error)
|
console.error('加载地址失败:', error)
|
||||||
Taro.showToast({
|
Taro.showToast({
|
||||||
@@ -142,30 +178,39 @@ const AddUserAddress = () => {
|
|||||||
const result = parseAddressText(inputText);
|
const result = parseAddressText(inputText);
|
||||||
|
|
||||||
// 更新表单数据
|
// 更新表单数据
|
||||||
const newFormData = {
|
const newFormData: any = {
|
||||||
...FormData,
|
...FormData,
|
||||||
name: result.name || FormData.name,
|
name: result.name || FormData.name,
|
||||||
phone: result.phone || FormData.phone,
|
phone: result.phone || FormData.phone,
|
||||||
address: result.address || FormData.address,
|
address: result.address || FormData.address
|
||||||
province: result.province || FormData.province,
|
|
||||||
city: result.city || FormData.city,
|
|
||||||
region: result.region || FormData.region
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!regionLocked) {
|
||||||
|
newFormData.province = result.province || FormData.province
|
||||||
|
newFormData.city = result.city || FormData.city
|
||||||
|
newFormData.region = result.region || FormData.region
|
||||||
|
}
|
||||||
|
|
||||||
setFormData(newFormData);
|
setFormData(newFormData);
|
||||||
|
|
||||||
// 更新地区显示文本
|
// 更新地区显示文本
|
||||||
if (result.province && result.city && result.region) {
|
if (!regionLocked && result.province && result.city && result.region) {
|
||||||
setText(`${result.province} ${result.city} ${result.region}`);
|
setText(`${result.province} ${result.city} ${result.region}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新表单字段值
|
// 更新表单字段值
|
||||||
if (formRef.current) {
|
if (formRef.current) {
|
||||||
formRef.current.setFieldsValue(newFormData);
|
const patch: any = {
|
||||||
|
name: newFormData.name,
|
||||||
|
phone: newFormData.phone,
|
||||||
|
address: newFormData.address
|
||||||
|
}
|
||||||
|
if (!regionLocked && newFormData.region) patch.region = newFormData.region
|
||||||
|
formRef.current.setFieldsValue(patch);
|
||||||
}
|
}
|
||||||
|
|
||||||
Taro.showToast({
|
Taro.showToast({
|
||||||
title: '识别成功',
|
title: regionLocked ? '识别成功(所在地区以定位为准)' : '识别成功',
|
||||||
icon: 'success'
|
icon: 'success'
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -281,7 +326,6 @@ const AddUserAddress = () => {
|
|||||||
name: res.name,
|
name: res.name,
|
||||||
address: res.address
|
address: res.address
|
||||||
}
|
}
|
||||||
setSelectedLocation(next)
|
|
||||||
|
|
||||||
// 尝试从地图返回的 address 文本解析省市区(best-effort)
|
// 尝试从地图返回的 address 文本解析省市区(best-effort)
|
||||||
const regionResult = res?.provinceName || res?.cityName || res?.adName
|
const regionResult = res?.provinceName || res?.cityName || res?.adName
|
||||||
@@ -292,15 +336,22 @@ const AddUserAddress = () => {
|
|||||||
}
|
}
|
||||||
: parseRegion(String(res.address || ''))
|
: parseRegion(String(res.address || ''))
|
||||||
|
|
||||||
|
const province = String(regionResult?.province || '').trim()
|
||||||
|
const city = String(regionResult?.city || '').trim()
|
||||||
|
const region = String(regionResult?.region || '').trim()
|
||||||
|
if (!province || !city || !region) {
|
||||||
|
Taro.showToast({ title: '定位未识别到所在地区,请重新选择定位', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedLocation(next)
|
||||||
|
setRegionLocked(true)
|
||||||
|
|
||||||
// 将地图选点的地址同步到“收货地址”(不额外拼接省市区字段,省市区由独立字段保存)
|
// 将地图选点的地址同步到“收货地址”(不额外拼接省市区字段,省市区由独立字段保存)
|
||||||
const nextDetailAddress = (() => {
|
const nextDetailAddress = (() => {
|
||||||
const rawAddr = String(res.address || '').trim()
|
const rawAddr = String(res.address || '').trim()
|
||||||
const name = String(res.name || '').trim()
|
const name = String(res.name || '').trim()
|
||||||
|
|
||||||
const province = String(regionResult?.province || '').trim()
|
|
||||||
const city = String(regionResult?.city || '').trim()
|
|
||||||
const region = String(regionResult?.region || '').trim()
|
|
||||||
|
|
||||||
// 选择定位返回的 address 往往包含省市区,这里尽量剥离掉,避免和表单的省市区字段重复
|
// 选择定位返回的 address 往往包含省市区,这里尽量剥离掉,避免和表单的省市区字段重复
|
||||||
let detail = rawAddr
|
let detail = rawAddr
|
||||||
for (const part of [province, city, region]) {
|
for (const part of [province, city, region]) {
|
||||||
@@ -320,20 +371,18 @@ const AddUserAddress = () => {
|
|||||||
lng: next.lng,
|
lng: next.lng,
|
||||||
lat: next.lat,
|
lat: next.lat,
|
||||||
address: nextDetailAddress || prev.address,
|
address: nextDetailAddress || prev.address,
|
||||||
province: regionResult?.province || prev.province,
|
province,
|
||||||
city: regionResult?.city || prev.city,
|
city,
|
||||||
region: regionResult?.region || prev.region
|
region
|
||||||
}))
|
}))
|
||||||
|
|
||||||
if (regionResult?.province && regionResult?.city && regionResult?.region) {
|
setText(`${province} ${city} ${region}`)
|
||||||
setText(`${regionResult.province} ${regionResult.city} ${regionResult.region}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新表单展示值(Form initialValues 不会跟随 FormData 变化)
|
// 更新表单展示值(Form initialValues 不会跟随 FormData 变化)
|
||||||
if (formRef.current) {
|
if (formRef.current) {
|
||||||
const patch: any = {}
|
const patch: any = {}
|
||||||
if (nextDetailAddress) patch.address = nextDetailAddress
|
if (nextDetailAddress) patch.address = nextDetailAddress
|
||||||
if (regionResult?.region) patch.region = regionResult.region
|
patch.region = region
|
||||||
formRef.current.setFieldsValue(patch)
|
formRef.current.setFieldsValue(patch)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -377,6 +426,14 @@ const AddUserAddress = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openRegionPicker = () => {
|
||||||
|
if (regionLocked) {
|
||||||
|
Taro.showToast({ title: '所在地区已由定位确定,修改请重新选择定位', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setVisible(true)
|
||||||
|
}
|
||||||
|
|
||||||
// 提交表单
|
// 提交表单
|
||||||
const submitSucceed = async (values: any) => {
|
const submitSucceed = async (values: any) => {
|
||||||
const loc =
|
const loc =
|
||||||
@@ -386,11 +443,16 @@ const AddUserAddress = () => {
|
|||||||
Taro.showToast({ title: '请选择定位', icon: 'none' })
|
Taro.showToast({ title: '请选择定位', icon: 'none' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (!FormData.province || !FormData.city || !FormData.region) {
|
||||||
|
Taro.showToast({ title: '请先选择定位以自动填写所在地区', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 准备提交的数据
|
// 准备提交的数据
|
||||||
const submitData = {
|
const submitData = {
|
||||||
...values,
|
...values,
|
||||||
|
country: FormData.country,
|
||||||
province: FormData.province,
|
province: FormData.province,
|
||||||
city: FormData.city,
|
city: FormData.city,
|
||||||
region: FormData.region,
|
region: FormData.region,
|
||||||
@@ -448,13 +510,40 @@ const AddUserAddress = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 动态设置页面标题
|
// 动态设置页面标题
|
||||||
Taro.setNavigationBarTitle({
|
Taro.setNavigationBarTitle({
|
||||||
title: isEditMode ? '编辑收货地址' : '新增收货地址'
|
title: isEditMode ? '编辑收货地址' : (fromWx ? '完善收货地址' : '新增收货地址')
|
||||||
});
|
});
|
||||||
|
|
||||||
reload().then(() => {
|
reload().then(() => {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
})
|
})
|
||||||
}, [isEditMode]);
|
}, [fromWx, isEditMode]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!regionLocked) return
|
||||||
|
if (!visible) return
|
||||||
|
setVisible(false)
|
||||||
|
}, [regionLocked, visible])
|
||||||
|
|
||||||
|
// NutUI Form 的 initialValues 在首次渲染后不再响应更新;微信导入时做一次 setFieldsValue 兜底回填。
|
||||||
|
useEffect(() => {
|
||||||
|
if (loading) return
|
||||||
|
if (isEditMode) return
|
||||||
|
const draft = wxDraftRef.current
|
||||||
|
if (!draft) return
|
||||||
|
if (!formRef.current?.setFieldsValue) return
|
||||||
|
try {
|
||||||
|
formRef.current.setFieldsValue({
|
||||||
|
name: (draft as any)?.name,
|
||||||
|
phone: (draft as any)?.phone,
|
||||||
|
address: (draft as any)?.address,
|
||||||
|
region: (draft as any)?.region
|
||||||
|
})
|
||||||
|
} catch (_e) {
|
||||||
|
// ignore
|
||||||
|
} finally {
|
||||||
|
wxDraftRef.current = null
|
||||||
|
}
|
||||||
|
}, [fromWx, isEditMode, loading])
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <Loading className={'px-2'}>加载中</Loading>
|
return <Loading className={'px-2'}>加载中</Loading>
|
||||||
@@ -471,7 +560,7 @@ const AddUserAddress = () => {
|
|||||||
onFinishFailed={(errors) => submitFailed(errors)}
|
onFinishFailed={(errors) => submitFailed(errors)}
|
||||||
>
|
>
|
||||||
<CellGroup className={'px-3'}>
|
<CellGroup className={'px-3'}>
|
||||||
<div
|
<View
|
||||||
style={{
|
style={{
|
||||||
border: '1px dashed #22c55e',
|
border: '1px dashed #22c55e',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -497,7 +586,7 @@ const AddUserAddress = () => {
|
|||||||
>
|
>
|
||||||
识别
|
识别
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</View>
|
||||||
</CellGroup>
|
</CellGroup>
|
||||||
<View className={'bg-gray-100 h-3'}></View>
|
<View className={'bg-gray-100 h-3'}></View>
|
||||||
<CellGroup style={{padding: '4px 0'}}>
|
<CellGroup style={{padding: '4px 0'}}>
|
||||||
@@ -529,30 +618,32 @@ const AddUserAddress = () => {
|
|||||||
rules={[{message: '请输入您的所在地区'}]}
|
rules={[{message: '请输入您的所在地区'}]}
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
<div className={'flex justify-between items-center'} onClick={() => setVisible(true)}>
|
<View className={'flex justify-between items-center'} onClick={openRegionPicker}>
|
||||||
<Input placeholder="选择所在地区" value={text} disabled/>
|
<Input placeholder="选择所在地区" value={text} disabled/>
|
||||||
<ArrowRight className={'text-gray-400'}/>
|
<ArrowRight className={'text-gray-400'}/>
|
||||||
</div>
|
</View>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="address" label="收货地址" initialValue={FormData.address} required>
|
<Form.Item name="address" label="收货地址" initialValue={FormData.address} required>
|
||||||
<TextArea maxLength={50} placeholder="请输入详细收货地址"/>
|
<TextArea maxLength={50} placeholder="请输入详细收货地址"/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
</CellGroup>
|
||||||
|
<CellGroup>
|
||||||
<Cell
|
<Cell
|
||||||
title="选择定位"
|
title="选择定位"
|
||||||
description={
|
description={
|
||||||
selectedLocation?.address ||
|
selectedLocation?.address ||
|
||||||
(selectedLocation ? `经纬度:${selectedLocation.lng}, ${selectedLocation.lat}` : '')
|
(selectedLocation ? `经纬度:${selectedLocation.lng}, ${selectedLocation.lat}` : '用于计算是否超出配送范围')
|
||||||
}
|
}
|
||||||
extra={(
|
extra={(
|
||||||
<div className={'flex items-center gap-2'}>
|
<View className={'flex items-center gap-2'}>
|
||||||
<div
|
<View
|
||||||
className={'text-gray-900 text-sm'}
|
className={'text-gray-900 text-sm'}
|
||||||
style={{maxWidth: '200px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap'}}
|
style={{maxWidth: '200px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap'}}
|
||||||
>
|
>
|
||||||
{selectedLocation?.name || (selectedLocation ? '已选择' : '请选择')}
|
{selectedLocation?.name || (selectedLocation ? '已选择' : '请选择')}
|
||||||
</div>
|
</View>
|
||||||
<ArrowRight className={'text-gray-400'}/>
|
<ArrowRight className={'text-gray-400'}/>
|
||||||
</div>
|
</View>
|
||||||
)}
|
)}
|
||||||
onClick={chooseGeoLocation}
|
onClick={chooseGeoLocation}
|
||||||
/>
|
/>
|
||||||
@@ -564,6 +655,10 @@ const AddUserAddress = () => {
|
|||||||
options={optionsDemo1}
|
options={optionsDemo1}
|
||||||
title="选择地址"
|
title="选择地址"
|
||||||
onChange={(value, _) => {
|
onChange={(value, _) => {
|
||||||
|
if (regionLocked) {
|
||||||
|
Taro.showToast({ title: '所在地区已由定位确定,修改请重新选择定位', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
setFormData({
|
setFormData({
|
||||||
...FormData,
|
...FormData,
|
||||||
province: `${value[0]}`,
|
province: `${value[0]}`,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {useState} from "react";
|
import {useState} from "react";
|
||||||
import Taro, {useDidShow} from '@tarojs/taro'
|
import Taro, {useDidShow} from '@tarojs/taro'
|
||||||
import {Button, Cell, CellGroup, Space, Empty, ConfigProvider, Divider} from '@nutui/nutui-react-taro'
|
import {Button, Cell, Space, Empty, ConfigProvider, Divider} from '@nutui/nutui-react-taro'
|
||||||
import {Dongdong, ArrowRight, CheckNormal, Checked} from '@nutui/icons-react-taro'
|
import {CheckNormal, Checked} from '@nutui/icons-react-taro'
|
||||||
import {View} from '@tarojs/components'
|
import {View} from '@tarojs/components'
|
||||||
import {ShopUserAddress} from "@/api/shop/shopUserAddress/model";
|
import {ShopUserAddress} from "@/api/shop/shopUserAddress/model";
|
||||||
import {listShopUserAddress, removeShopUserAddress, updateShopUserAddress} from "@/api/shop/shopUserAddress";
|
import {listShopUserAddress, removeShopUserAddress, updateShopUserAddress} from "@/api/shop/shopUserAddress";
|
||||||
@@ -144,8 +144,8 @@ const Address = () => {
|
|||||||
/>
|
/>
|
||||||
<Space>
|
<Space>
|
||||||
<Button onClick={() => Taro.navigateTo({url: '/user/address/add'})}>新增地址</Button>
|
<Button onClick={() => Taro.navigateTo({url: '/user/address/add'})}>新增地址</Button>
|
||||||
<Button type="success" fill="dashed"
|
{/*<Button type="success" fill="dashed"*/}
|
||||||
onClick={() => Taro.navigateTo({url: '/user/address/wxAddress'})}>获取微信地址</Button>
|
{/* onClick={() => Taro.navigateTo({url: '/user/address/wxAddress'})}>获取微信地址</Button>*/}
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
@@ -154,19 +154,19 @@ const Address = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CellGroup>
|
{/*<CellGroup>*/}
|
||||||
<Cell
|
{/* <Cell*/}
|
||||||
onClick={() => Taro.navigateTo({url: '/user/address/wxAddress'})}
|
{/* onClick={() => Taro.navigateTo({url: '/user/address/wxAddress'})}*/}
|
||||||
>
|
{/* >*/}
|
||||||
<div className={'flex justify-between items-center w-full'}>
|
{/* <div className={'flex justify-between items-center w-full'}>*/}
|
||||||
<div className={'flex items-center gap-3'}>
|
{/* <div className={'flex items-center gap-3'}>*/}
|
||||||
<Dongdong className={'text-green-600'}/>
|
{/* <Dongdong className={'text-green-600'}/>*/}
|
||||||
<div>获取微信地址</div>
|
{/* <div>获取微信地址</div>*/}
|
||||||
</div>
|
{/* </div>*/}
|
||||||
<ArrowRight className={'text-gray-400'}/>
|
{/* <ArrowRight className={'text-gray-400'}/>*/}
|
||||||
</div>
|
{/* </div>*/}
|
||||||
</Cell>
|
{/* </Cell>*/}
|
||||||
</CellGroup>
|
{/*</CellGroup>*/}
|
||||||
{list.map((item, _) => (
|
{list.map((item, _) => (
|
||||||
<Cell.Group>
|
<Cell.Group>
|
||||||
<Cell className={'flex flex-col gap-1'} onClick={() => selectAddress(item)}>
|
<Cell className={'flex flex-col gap-1'} onClick={() => selectAddress(item)}>
|
||||||
|
|||||||
@@ -1,25 +1,17 @@
|
|||||||
import {useEffect} from "react";
|
import {useEffect} from "react";
|
||||||
import Taro from '@tarojs/taro'
|
import Taro from '@tarojs/taro'
|
||||||
import {addShopUserAddress} from "@/api/shop/shopUserAddress";
|
|
||||||
import { getCurrentLngLat } from "@/utils/location";
|
|
||||||
|
|
||||||
const WxAddress = () => {
|
const WxAddress = () => {
|
||||||
/**
|
/**
|
||||||
* 从微信API获取用户收货地址
|
* 从微信API获取用户收货地址
|
||||||
* 调用微信原生地址选择界面,获取成功后保存到服务器并刷新列表
|
* 调用微信原生地址选择界面,获取成功后跳转到“新增收货地址”页面,让用户选择定位后再保存
|
||||||
*/
|
*/
|
||||||
const getWeChatAddress = () => {
|
const getWeChatAddress = () => {
|
||||||
Taro.chooseAddress()
|
Taro.chooseAddress()
|
||||||
.then(async res => {
|
.then(async res => {
|
||||||
const loc = await getCurrentLngLat()
|
// 仅填充微信地址信息,不要用“当前定位”覆盖经纬度(会造成经纬度与地址不匹配)。
|
||||||
if (!loc) {
|
// 选择后跳转到“新增/编辑收货地址”页面,让用户手动选择地图定位后再保存。
|
||||||
// Avoid leaving the user on an empty page.
|
const addressDraft = {
|
||||||
setTimeout(() => Taro.navigateBack(), 300)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 格式化微信返回的地址数据为后端所需格式
|
|
||||||
const addressData = {
|
|
||||||
name: res.userName,
|
name: res.userName,
|
||||||
phone: res.telNumber,
|
phone: res.telNumber,
|
||||||
country: res.nationalCode || '中国',
|
country: res.nationalCode || '中国',
|
||||||
@@ -27,40 +19,32 @@ const WxAddress = () => {
|
|||||||
city: res.cityName,
|
city: res.cityName,
|
||||||
region: res.countyName,
|
region: res.countyName,
|
||||||
address: res.detailInfo,
|
address: res.detailInfo,
|
||||||
postalCode: res.postalCode,
|
isDefault: false,
|
||||||
lng: loc.lng,
|
|
||||||
lat: loc.lat,
|
|
||||||
isDefault: false
|
|
||||||
}
|
}
|
||||||
console.log(res, 'addrs..')
|
Taro.setStorageSync('WxAddressDraft', addressDraft)
|
||||||
// 调用保存地址的API(假设存在该接口)
|
// 用 redirectTo 替换当前页面,避免保存后 navigateBack 回到空白的 wxAddress 页面。
|
||||||
addShopUserAddress(addressData)
|
await Taro.redirectTo({ url: '/user/address/add?fromWx=1&skipDefaultCheck=1' })
|
||||||
.then((msg) => {
|
|
||||||
console.log(msg)
|
|
||||||
Taro.showToast({
|
|
||||||
title: `${msg}`,
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
setTimeout(() => {
|
|
||||||
// 保存成功后返回
|
|
||||||
Taro.navigateBack()
|
|
||||||
}, 1000)
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('保存地址失败:', error)
|
|
||||||
Taro.showToast({title: '保存地址失败', icon: 'error'})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error('获取微信地址失败:', err)
|
console.error('获取微信地址失败:', err)
|
||||||
|
// 用户取消选择地址:直接返回上一页
|
||||||
|
if (String(err?.errMsg || '').includes('cancel')) {
|
||||||
|
setTimeout(() => Taro.navigateBack(), 200)
|
||||||
|
return
|
||||||
|
}
|
||||||
// 处理用户拒绝授权的情况
|
// 处理用户拒绝授权的情况
|
||||||
if (err.errMsg.includes('auth deny')) {
|
if (String(err?.errMsg || '').includes('auth deny')) {
|
||||||
Taro.showModal({
|
Taro.showModal({
|
||||||
title: '授权失败',
|
title: '授权失败',
|
||||||
content: '请在设置中允许获取地址权限',
|
content: '请在设置中允许获取地址权限',
|
||||||
showCancel: false
|
showCancel: false
|
||||||
})
|
})
|
||||||
|
setTimeout(() => Taro.navigateBack(), 300)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Taro.showToast({ title: '获取微信地址失败', icon: 'none' })
|
||||||
|
setTimeout(() => Taro.navigateBack(), 300)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -104,6 +104,10 @@ interface OrderListProps {
|
|||||||
baseParams?: ShopOrderParam;
|
baseParams?: ShopOrderParam;
|
||||||
// 只读模式:隐藏“支付/取消/确认收货/退款”等用户操作按钮
|
// 只读模式:隐藏“支付/取消/确认收货/退款”等用户操作按钮
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
|
// 是否自动取消“支付已过期”的待支付订单(仅 user 模式生效)
|
||||||
|
autoCancelExpired?: boolean;
|
||||||
|
// 支付超时时间(小时),默认 24 小时
|
||||||
|
paymentTimeoutHours?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function OrderList(props: OrderListProps) {
|
function OrderList(props: OrderListProps) {
|
||||||
@@ -111,6 +115,8 @@ function OrderList(props: OrderListProps) {
|
|||||||
const pageRef = useRef(1)
|
const pageRef = useRef(1)
|
||||||
const [hasMore, setHasMore] = useState(true)
|
const [hasMore, setHasMore] = useState(true)
|
||||||
const [payingOrderId, setPayingOrderId] = useState<number | null>(null)
|
const [payingOrderId, setPayingOrderId] = useState<number | null>(null)
|
||||||
|
const autoCanceledOrderIdsRef = useRef<Set<number>>(new Set())
|
||||||
|
const autoCancelRunningRef = useRef(false)
|
||||||
// 根据传入的statusFilter设置初始tab索引
|
// 根据传入的statusFilter设置初始tab索引
|
||||||
const getInitialTabIndex = () => {
|
const getInitialTabIndex = () => {
|
||||||
if (props.searchParams?.statusFilter !== undefined) {
|
if (props.searchParams?.statusFilter !== undefined) {
|
||||||
@@ -132,61 +138,92 @@ function OrderList(props: OrderListProps) {
|
|||||||
const [orderToConfirmReceive, setOrderToConfirmReceive] = useState<ShopOrder | null>(null)
|
const [orderToConfirmReceive, setOrderToConfirmReceive] = useState<ShopOrder | null>(null)
|
||||||
const isReadOnly = props.readOnly || props.mode === 'store' || props.mode === 'rider'
|
const isReadOnly = props.readOnly || props.mode === 'store' || props.mode === 'rider'
|
||||||
|
|
||||||
const isOrderCompleted = (order: ShopOrder) => Number(order.orderStatus) === 1 || order.formId === 10074;
|
const toNum = (v: any): number | undefined => {
|
||||||
|
if (v === null || v === undefined || v === '') return undefined;
|
||||||
|
const n = Number(v);
|
||||||
|
return Number.isFinite(n) ? n : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseTime = (raw: any): dayjs.Dayjs | null => {
|
||||||
|
const text = String(raw ?? '').trim();
|
||||||
|
if (!text) return null;
|
||||||
|
const t = /^\d+$/.test(text)
|
||||||
|
? dayjs(Number(text) < 1e12 ? Number(text) * 1000 : Number(text))
|
||||||
|
: dayjs(text);
|
||||||
|
return t.isValid() ? t : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isOrderPaymentExpiredSafe = (order: ShopOrder, timeoutHours: number) => {
|
||||||
|
if (order.payStatus) return false;
|
||||||
|
if (toNum(order.orderStatus) === 2) return false;
|
||||||
|
|
||||||
|
const expiration = parseTime(order.expirationTime);
|
||||||
|
if (expiration) return dayjs().isAfter(expiration);
|
||||||
|
|
||||||
|
if (order.createTime) return isPaymentExpired(order.createTime, timeoutHours);
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// “已完成”应以订单状态为准;不要用商品ID等字段推断完成态,否则会造成 Tab(待发货/待收货) 与状态文案不同步
|
||||||
|
const isOrderCompleted = (order: ShopOrder) => toNum(order.orderStatus) === 1;
|
||||||
|
|
||||||
// 获取订单状态文本
|
// 获取订单状态文本
|
||||||
const getOrderStatusText = (order: ShopOrder) => {
|
const getOrderStatusText = (order: ShopOrder) => {
|
||||||
|
const orderStatus = toNum(order.orderStatus);
|
||||||
|
const deliveryStatus = toNum(order.deliveryStatus);
|
||||||
|
|
||||||
// 优先检查订单状态
|
// 优先检查订单状态
|
||||||
if (order.orderStatus === 2) return '已取消';
|
if (orderStatus === 2) return '已取消';
|
||||||
if (order.orderStatus === 4) return '退款申请中';
|
if (orderStatus === 4) return '退款申请中';
|
||||||
if (order.orderStatus === 5) return '退款被拒绝';
|
if (orderStatus === 5) return '退款被拒绝';
|
||||||
if (order.orderStatus === 6) return '退款成功';
|
if (orderStatus === 6) return '退款成功';
|
||||||
if (order.orderStatus === 7) return '客户端申请退款';
|
if (orderStatus === 7) return '客户端申请退款';
|
||||||
if (isOrderCompleted(order)) return '已完成';
|
if (isOrderCompleted(order)) return '已完成';
|
||||||
|
|
||||||
// 检查支付状态 (payStatus为boolean类型,false/0表示未付款,true/1表示已付款)
|
// 检查支付状态 (payStatus为boolean类型,false/0表示未付款,true/1表示已付款)
|
||||||
if (!order.payStatus) return '等待买家付款';
|
if (!order.payStatus) return '等待买家付款';
|
||||||
|
|
||||||
// 已付款后检查发货状态
|
// 已付款后检查发货状态
|
||||||
if (order.deliveryStatus === 10) return '待发货';
|
if (deliveryStatus === 10) return '待发货';
|
||||||
if (order.deliveryStatus === 20) {
|
if (deliveryStatus === 20) {
|
||||||
// 若订单没有配送员,沿用原“待收货”语义
|
// 若订单没有配送员,沿用原“待收货”语义
|
||||||
if (!order.riderId || Number(order.riderId) === 0) return '待收货';
|
if (!order.riderId || Number(order.riderId) === 0) return '待收货';
|
||||||
// 配送员确认送达后(sendEndTime有值),才进入“待确认收货”
|
// 配送员确认送达后(sendEndTime有值),才进入“待确认收货”
|
||||||
if (order.sendEndTime && !isOrderCompleted(order)) return '待确认收货';
|
if (order.sendEndTime && !isOrderCompleted(order)) return '待确认收货';
|
||||||
return '配送中';
|
return '配送中';
|
||||||
}
|
}
|
||||||
if (order.deliveryStatus === 30) return '部分发货';
|
if (deliveryStatus === 30) return '部分发货';
|
||||||
|
|
||||||
if (order.orderStatus === 0) return '未使用';
|
if (orderStatus === 0) return '未使用';
|
||||||
|
|
||||||
return '未知状态';
|
return '未知状态';
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取订单状态颜色
|
// 获取订单状态颜色
|
||||||
const getOrderStatusColor = (order: ShopOrder) => {
|
const getOrderStatusColor = (order: ShopOrder) => {
|
||||||
|
const orderStatus = toNum(order.orderStatus);
|
||||||
|
const deliveryStatus = toNum(order.deliveryStatus);
|
||||||
// 优先检查订单状态
|
// 优先检查订单状态
|
||||||
if (order.orderStatus === 2) return 'text-gray-500'; // 已取消
|
if (orderStatus === 2) return 'text-gray-500'; // 已取消
|
||||||
if (order.orderStatus === 4) return 'text-orange-500'; // 退款申请中
|
if (orderStatus === 4) return 'text-orange-500'; // 退款申请中
|
||||||
if (order.orderStatus === 5) return 'text-red-500'; // 退款被拒绝
|
if (orderStatus === 5) return 'text-red-500'; // 退款被拒绝
|
||||||
if (order.orderStatus === 6) return 'text-green-500'; // 退款成功
|
if (orderStatus === 6) return 'text-green-500'; // 退款成功
|
||||||
if (order.orderStatus === 7) return 'text-orange-500'; // 客户端申请退款
|
if (orderStatus === 7) return 'text-orange-500'; // 客户端申请退款
|
||||||
if (isOrderCompleted(order)) return 'text-green-600'; // 已完成
|
if (isOrderCompleted(order)) return 'text-green-600'; // 已完成
|
||||||
|
|
||||||
// 检查支付状态
|
// 检查支付状态
|
||||||
if (!order.payStatus) return 'text-orange-500'; // 等待买家付款
|
if (!order.payStatus) return 'text-orange-500'; // 等待买家付款
|
||||||
|
|
||||||
// 已付款后检查发货状态
|
// 已付款后检查发货状态
|
||||||
if (order.deliveryStatus === 10) return 'text-blue-500'; // 待发货
|
if (deliveryStatus === 10) return 'text-blue-500'; // 待发货
|
||||||
if (order.deliveryStatus === 20) {
|
if (deliveryStatus === 20) {
|
||||||
if (!order.riderId || Number(order.riderId) === 0) return 'text-purple-500'; // 待收货
|
if (!order.riderId || Number(order.riderId) === 0) return 'text-purple-500'; // 待收货
|
||||||
if (order.sendEndTime && !isOrderCompleted(order)) return 'text-purple-500'; // 待确认收货
|
if (order.sendEndTime && !isOrderCompleted(order)) return 'text-purple-500'; // 待确认收货
|
||||||
return 'text-blue-500'; // 配送中
|
return 'text-blue-500'; // 配送中
|
||||||
}
|
}
|
||||||
if (order.deliveryStatus === 30) return 'text-blue-500'; // 部分发货
|
if (deliveryStatus === 30) return 'text-blue-500'; // 部分发货
|
||||||
|
|
||||||
if (order.orderStatus === 0) return 'text-gray-500'; // 未使用
|
if (orderStatus === 0) return 'text-gray-500'; // 未使用
|
||||||
|
|
||||||
return 'text-gray-600'; // 默认颜色
|
return 'text-gray-600'; // 默认颜色
|
||||||
};
|
};
|
||||||
@@ -237,24 +274,82 @@ function OrderList(props: OrderListProps) {
|
|||||||
finalStatusFilter: searchConditions.statusFilter
|
finalStatusFilter: searchConditions.statusFilter
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await pageShopOrder(searchConditions);
|
const timeoutHours = typeof props.paymentTimeoutHours === 'number' ? props.paymentTimeoutHours : 24;
|
||||||
|
const canAutoCancelExpired =
|
||||||
|
!!props.autoCancelExpired &&
|
||||||
|
(!props.mode || props.mode === 'user') &&
|
||||||
|
!props.readOnly;
|
||||||
|
const isPendingPayList = statusParams.statusFilter === 0;
|
||||||
|
|
||||||
if (res?.list && res?.list.length > 0) {
|
const fetchOrders = async () => pageShopOrder(searchConditions);
|
||||||
|
|
||||||
|
let res = await fetchOrders();
|
||||||
|
let incoming = (res?.list || []) as ShopOrder[];
|
||||||
|
let rawIncomingLength = incoming.length;
|
||||||
|
|
||||||
|
// 自动取消“支付已过期”的待支付订单(避免用户看到一堆不可支付的过期单)
|
||||||
|
if (canAutoCancelExpired && incoming.length && !autoCancelRunningRef.current) {
|
||||||
|
const expiredToCancel = incoming
|
||||||
|
.filter(o => !!o?.orderId)
|
||||||
|
.filter(o => !autoCanceledOrderIdsRef.current.has(o.orderId as number))
|
||||||
|
.filter(o => isOrderPaymentExpiredSafe(o, timeoutHours));
|
||||||
|
|
||||||
|
if (expiredToCancel.length) {
|
||||||
|
autoCancelRunningRef.current = true;
|
||||||
|
const justCanceled = new Set<number>();
|
||||||
|
try {
|
||||||
|
// 单次最多处理 20 笔,避免接口风暴
|
||||||
|
for (const order of expiredToCancel.slice(0, 20)) {
|
||||||
|
try {
|
||||||
|
await updateShopOrder({ orderId: order.orderId, orderStatus: 2 });
|
||||||
|
autoCanceledOrderIdsRef.current.add(order.orderId as number);
|
||||||
|
justCanceled.add(order.orderId as number);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('自动取消过期订单失败:', order?.orderId, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
autoCancelRunningRef.current = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (justCanceled.size > 0) {
|
||||||
|
if (resetPage) {
|
||||||
|
// resetPage 时重新拉取一次,确保列表状态与服务端一致
|
||||||
|
res = await fetchOrders();
|
||||||
|
incoming = (res?.list || []) as ShopOrder[];
|
||||||
|
rawIncomingLength = incoming.length;
|
||||||
|
Taro.showToast({ title: '已自动取消过期订单', icon: 'none' });
|
||||||
|
} else {
|
||||||
|
// loadMore 时不重新拉取,避免破坏滚动;仅在本地列表中做最小同步
|
||||||
|
if (isPendingPayList) {
|
||||||
|
incoming = incoming.filter(o => !justCanceled.has(o.orderId as number));
|
||||||
|
} else {
|
||||||
|
incoming = incoming.map(o => (
|
||||||
|
justCanceled.has(o.orderId as number) ? { ...o, orderStatus: 2 } : o
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rawIncomingLength > 0) {
|
||||||
// 订单分页接口已返回 orderGoods:列表直接使用该字段
|
// 订单分页接口已返回 orderGoods:列表直接使用该字段
|
||||||
const incoming = res.list as ShopOrder[];
|
|
||||||
|
|
||||||
// 使用函数式更新避免依赖 list
|
// 使用函数式更新避免依赖 list
|
||||||
setList(prevList => {
|
if (incoming.length > 0) {
|
||||||
const newList = resetPage ? incoming : (prevList || []).concat(incoming);
|
setList(prevList => (resetPage ? incoming : (prevList || []).concat(incoming)));
|
||||||
return newList;
|
} else {
|
||||||
});
|
// 本页数据全部被自动取消过滤掉:不清空历史列表,仅保持现状
|
||||||
|
setList(prevList => (resetPage ? [] : prevList));
|
||||||
|
}
|
||||||
|
|
||||||
// 正确判断是否还有更多数据
|
// 正确判断是否还有更多数据(以服务端返回条数为准)
|
||||||
const hasMoreData = incoming.length >= 10; // 假设每页10条数据
|
const hasMoreData = rawIncomingLength >= 10; // 假设每页10条数据
|
||||||
setHasMore(hasMoreData);
|
setHasMore(hasMoreData);
|
||||||
} else {
|
} else {
|
||||||
setList(prevList => resetPage ? [] : prevList);
|
// 服务端已无更多数据
|
||||||
|
setList(prevList => (resetPage ? [] : prevList));
|
||||||
setHasMore(false);
|
setHasMore(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,7 +365,7 @@ function OrderList(props: OrderListProps) {
|
|||||||
icon: 'none'
|
icon: 'none'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [tapIndex, props.searchParams]); // 移除 list/page 依赖,避免useEffect触发循环
|
}, [tapIndex, props.searchParams, props.baseParams, props.mode, props.readOnly, props.autoCancelExpired, props.paymentTimeoutHours]); // 移除 list/page 依赖,避免useEffect触发循环
|
||||||
|
|
||||||
const reloadMore = useCallback(async () => {
|
const reloadMore = useCallback(async () => {
|
||||||
if (loading || !hasMore) return; // 防止重复加载
|
if (loading || !hasMore) return; // 防止重复加载
|
||||||
@@ -712,17 +807,20 @@ function OrderList(props: OrderListProps) {
|
|||||||
{/* 订单列表 */}
|
{/* 订单列表 */}
|
||||||
{list.length > 0 && list
|
{list.length > 0 && list
|
||||||
?.filter((item) => {
|
?.filter((item) => {
|
||||||
|
const orderStatus = toNum(item.orderStatus);
|
||||||
// “待收货”不展示退款中的/已退款订单,这些订单统一放到“退货/售后”
|
// “待收货”不展示退款中的/已退款订单,这些订单统一放到“退货/售后”
|
||||||
if (tapIndex === 3 && (item.orderStatus === 4 || item.orderStatus === 6)) {
|
if (tapIndex === 3 && (orderStatus === 4 || orderStatus === 6)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// “退货/售后”只展示售后相关状态
|
// “退货/售后”只展示售后相关状态
|
||||||
if (tapIndex === 5) {
|
if (tapIndex === 5) {
|
||||||
return item.orderStatus === 4 || item.orderStatus === 5 || item.orderStatus === 6 || item.orderStatus === 7;
|
return orderStatus === 4 || orderStatus === 5 || orderStatus === 6 || orderStatus === 7;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
?.map((item, index) => {
|
?.map((item, index) => {
|
||||||
|
const orderStatus = toNum(item.orderStatus);
|
||||||
|
const deliveryStatus = toNum(item.deliveryStatus);
|
||||||
return (
|
return (
|
||||||
<Cell key={item.orderId ?? item.orderNo ?? index} style={{padding: '16px'}}
|
<Cell key={item.orderId ?? item.orderNo ?? index} style={{padding: '16px'}}
|
||||||
onClick={() => Taro.navigateTo({url: `/shop/orderDetail/index?orderId=${item.orderId}`})}>
|
onClick={() => Taro.navigateTo({url: `/shop/orderDetail/index?orderId=${item.orderId}`})}>
|
||||||
@@ -737,7 +835,7 @@ function OrderList(props: OrderListProps) {
|
|||||||
</View>
|
</View>
|
||||||
{/* 右侧显示合并的状态和倒计时 */}
|
{/* 右侧显示合并的状态和倒计时 */}
|
||||||
<View className={`${getOrderStatusColor(item)} font-medium`}>
|
<View className={`${getOrderStatusColor(item)} font-medium`}>
|
||||||
{!item.payStatus && item.orderStatus !== 2 ? (
|
{!item.payStatus && orderStatus !== 2 ? (
|
||||||
<PaymentCountdown
|
<PaymentCountdown
|
||||||
expirationTime={item.expirationTime}
|
expirationTime={item.expirationTime}
|
||||||
createTime={item.createTime}
|
createTime={item.createTime}
|
||||||
@@ -801,23 +899,23 @@ function OrderList(props: OrderListProps) {
|
|||||||
{!isReadOnly && (
|
{!isReadOnly && (
|
||||||
<Space className={'btn flex justify-end'}>
|
<Space className={'btn flex justify-end'}>
|
||||||
{/* 待付款状态:显示取消订单和立即支付 */}
|
{/* 待付款状态:显示取消订单和立即支付 */}
|
||||||
{(!item.payStatus) && item.orderStatus !== 2 && (
|
{(!item.payStatus) && orderStatus !== 2 && (
|
||||||
<Space>
|
<Space>
|
||||||
<Button size={'small'} onClick={(e) => {
|
<Button size={'small'} onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
void cancelOrder(item);
|
void cancelOrder(item);
|
||||||
}}>取消订单</Button>
|
}}>取消</Button>
|
||||||
{(!item.createTime || !isPaymentExpired(item.createTime, 24)) && (
|
{(!item.createTime || !isPaymentExpired(item.createTime, 24)) && (
|
||||||
<Button size={'small'} type="primary" onClick={(e) => {
|
<Button size={'small'} type="primary" onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
void payOrder(item);
|
void payOrder(item);
|
||||||
}}>立即支付</Button>
|
}}>继续支付</Button>
|
||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 待发货状态:显示申请退款 */}
|
{/* 待发货状态:显示申请退款 */}
|
||||||
{item.payStatus && isWithinRefundWindow(item.payTime, 60) && item.deliveryStatus === 10 && item.orderStatus !== 2 && item.orderStatus !== 4 && item.orderStatus !== 6 && item.orderStatus !== 7 && !isOrderCompleted(item) && (
|
{item.payStatus && isWithinRefundWindow(item.payTime, 60) && deliveryStatus === 10 && orderStatus !== 2 && orderStatus !== 4 && orderStatus !== 6 && orderStatus !== 7 && !isOrderCompleted(item) && (
|
||||||
<Button size={'small'} onClick={(e) => {
|
<Button size={'small'} onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
applyRefund(item);
|
applyRefund(item);
|
||||||
@@ -825,7 +923,7 @@ function OrderList(props: OrderListProps) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 待收货状态:显示查看物流和确认收货 */}
|
{/* 待收货状态:显示查看物流和确认收货 */}
|
||||||
{item.deliveryStatus === 20 && (!item.riderId || Number(item.riderId) === 0 || !!item.sendEndTime) && item.orderStatus !== 2 && item.orderStatus !== 6 && !isOrderCompleted(item) && (
|
{deliveryStatus === 20 && (!item.riderId || Number(item.riderId) === 0 || !!item.sendEndTime) && orderStatus !== 2 && orderStatus !== 6 && !isOrderCompleted(item) && (
|
||||||
<Space>
|
<Space>
|
||||||
{/*<Button size={'small'} onClick={(e) => {*/}
|
{/*<Button size={'small'} onClick={(e) => {*/}
|
||||||
{/* e.stopPropagation();*/}
|
{/* e.stopPropagation();*/}
|
||||||
@@ -839,7 +937,7 @@ function OrderList(props: OrderListProps) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 退款/售后状态:显示查看进度和撤销申请 */}
|
{/* 退款/售后状态:显示查看进度和撤销申请 */}
|
||||||
{(item.orderStatus === 4 || item.orderStatus === 7) && (
|
{(orderStatus === 4 || orderStatus === 7) && (
|
||||||
<Space>
|
<Space>
|
||||||
{/*<Button size={'small'} onClick={(e) => {*/}
|
{/*<Button size={'small'} onClick={(e) => {*/}
|
||||||
{/* e.stopPropagation();*/}
|
{/* e.stopPropagation();*/}
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ function Order() {
|
|||||||
onReload={() => reload(searchParams)}
|
onReload={() => reload(searchParams)}
|
||||||
searchParams={searchParams}
|
searchParams={searchParams}
|
||||||
showSearch={showSearch}
|
showSearch={showSearch}
|
||||||
|
autoCancelExpired
|
||||||
onSearchParamsChange={(newParams) => {
|
onSearchParamsChange={(newParams) => {
|
||||||
console.log('父组件接收到searchParams变化:', newParams);
|
console.log('父组件接收到searchParams变化:', newParams);
|
||||||
setSearchParams(newParams);
|
setSearchParams(newParams);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import Taro, { useDidShow } from '@tarojs/taro';
|
import Taro, { useDidShow } from '@tarojs/taro';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@@ -18,6 +18,7 @@ import { pageGltUserTicket } from '@/api/glt/gltUserTicket';
|
|||||||
import type { GltUserTicket } from '@/api/glt/gltUserTicket/model';
|
import type { GltUserTicket } from '@/api/glt/gltUserTicket/model';
|
||||||
import { pageGltTicketOrder, updateGltTicketOrder } from '@/api/glt/gltTicketOrder';
|
import { pageGltTicketOrder, updateGltTicketOrder } from '@/api/glt/gltTicketOrder';
|
||||||
import type { GltTicketOrder } from '@/api/glt/gltTicketOrder/model';
|
import type { GltTicketOrder } from '@/api/glt/gltTicketOrder/model';
|
||||||
|
import { getShopUserAddress } from '@/api/shop/shopUserAddress';
|
||||||
import { BaseUrl } from '@/config/app';
|
import { BaseUrl } from '@/config/app';
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
@@ -46,6 +47,8 @@ const UserTicketList = () => {
|
|||||||
const [qrTicket, setQrTicket] = useState<GltUserTicket | null>(null);
|
const [qrTicket, setQrTicket] = useState<GltUserTicket | null>(null);
|
||||||
const [qrImageUrl, setQrImageUrl] = useState('');
|
const [qrImageUrl, setQrImageUrl] = useState('');
|
||||||
|
|
||||||
|
const addressCacheRef = useRef<Record<number, { lng: number; lat: number; fullAddress?: string } | null>>({});
|
||||||
|
|
||||||
const getUserId = () => {
|
const getUserId = () => {
|
||||||
const raw = Taro.getStorageSync('UserId');
|
const raw = Taro.getStorageSync('UserId');
|
||||||
const id = Number(raw);
|
const id = Number(raw);
|
||||||
@@ -262,6 +265,81 @@ const UserTicketList = () => {
|
|||||||
return d.isValid() ? d.format('YYYY年MM月DD日') : v;
|
return d.isValid() ? d.format('YYYY年MM月DD日') : v;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const parseLatLng = (latRaw?: unknown, lngRaw?: unknown) => {
|
||||||
|
const lat = typeof latRaw === 'number' ? latRaw : parseFloat(String(latRaw ?? ''));
|
||||||
|
const lng = typeof lngRaw === 'number' ? lngRaw : parseFloat(String(lngRaw ?? ''));
|
||||||
|
if (!Number.isFinite(lat) || !Number.isFinite(lng)) return null;
|
||||||
|
if (Math.abs(lat) > 90 || Math.abs(lng) > 180) return null;
|
||||||
|
return { lat, lng };
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNavigateToAddress = async (order: GltTicketOrder) => {
|
||||||
|
try {
|
||||||
|
// Prefer coordinates from backend if present (non-typed fields), otherwise fetch by addressId.
|
||||||
|
const anyOrder = order as any;
|
||||||
|
const direct =
|
||||||
|
parseLatLng(anyOrder?.addressLat ?? anyOrder?.lat, anyOrder?.addressLng ?? anyOrder?.lng) ||
|
||||||
|
parseLatLng(anyOrder?.receiverLat, anyOrder?.receiverLng);
|
||||||
|
|
||||||
|
let coords = direct;
|
||||||
|
let fullAddress: string | undefined = order.address || undefined;
|
||||||
|
|
||||||
|
if (!coords && order.addressId) {
|
||||||
|
const cached = addressCacheRef.current[order.addressId];
|
||||||
|
if (cached) {
|
||||||
|
coords = { lat: cached.lat, lng: cached.lng };
|
||||||
|
fullAddress = fullAddress || cached.fullAddress;
|
||||||
|
} else if (cached === null) {
|
||||||
|
coords = null;
|
||||||
|
} else {
|
||||||
|
const addr = await getShopUserAddress(order.addressId);
|
||||||
|
const parsed = parseLatLng(addr?.lat, addr?.lng);
|
||||||
|
if (parsed) {
|
||||||
|
coords = parsed;
|
||||||
|
fullAddress = fullAddress || addr?.fullAddress || addr?.address || undefined;
|
||||||
|
addressCacheRef.current[order.addressId] = { ...parsed, fullAddress };
|
||||||
|
} else {
|
||||||
|
addressCacheRef.current[order.addressId] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!coords) {
|
||||||
|
if (fullAddress) {
|
||||||
|
await Taro.setClipboardData({ data: fullAddress });
|
||||||
|
Taro.showToast({ title: '未配置定位,地址已复制', icon: 'none' });
|
||||||
|
} else {
|
||||||
|
Taro.showToast({ title: '暂无可导航的地址', icon: 'none' });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Taro.openLocation({
|
||||||
|
latitude: coords.lat,
|
||||||
|
longitude: coords.lng,
|
||||||
|
name: '收货地址',
|
||||||
|
address: fullAddress || ''
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error('一键导航失败:', e);
|
||||||
|
Taro.showToast({ title: '导航失败,请重试', icon: 'none' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOneClickCall = async (order: GltTicketOrder) => {
|
||||||
|
const phone = (order.riderPhone || order.storePhone || '').trim();
|
||||||
|
if (!phone) {
|
||||||
|
Taro.showToast({ title: '暂无可呼叫的电话', icon: 'none' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await Taro.makePhoneCall({ phoneNumber: phone });
|
||||||
|
} catch (e) {
|
||||||
|
console.error('一键呼叫失败:', e);
|
||||||
|
Taro.showToast({ title: '呼叫失败,请手动拨打', icon: 'none' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const getTicketOrderStatusMeta = (order: GltTicketOrder) => {
|
const getTicketOrderStatusMeta = (order: GltTicketOrder) => {
|
||||||
if (order.status === 1) return { text: '已冻结', type: 'warning' as const };
|
if (order.status === 1) return { text: '已冻结', type: 'warning' as const };
|
||||||
|
|
||||||
@@ -412,7 +490,7 @@ const UserTicketList = () => {
|
|||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
<View className="flex flex-col items-end gap-2">
|
<View className="flex flex-col items-end gap-2 hidden">
|
||||||
{/*<Tag type={item.status === 1 ? 'danger' : 'success'}>*/}
|
{/*<Tag type={item.status === 1 ? 'danger' : 'success'}>*/}
|
||||||
{/* {item.status === 1 ? '冻结' : '正常'}*/}
|
{/* {item.status === 1 ? '冻结' : '正常'}*/}
|
||||||
{/*</Tag>*/}
|
{/*</Tag>*/}
|
||||||
@@ -498,6 +576,32 @@ const UserTicketList = () => {
|
|||||||
<View className="mt-1">
|
<View className="mt-1">
|
||||||
<Text className="text-xs text-gray-500">下单时间:{formatDateTime(item.createTime)}</Text>
|
<Text className="text-xs text-gray-500">下单时间:{formatDateTime(item.createTime)}</Text>
|
||||||
</View>
|
</View>
|
||||||
|
{(!!item.addressId || !!item.address || !!item.riderPhone || !!item.storePhone) ? (
|
||||||
|
<View className="mt-3 flex justify-end gap-2">
|
||||||
|
{(!!item.addressId || !!item.address) ? (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
void handleNavigateToAddress(item);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
一键导航
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
{(!!item.riderPhone || !!item.storePhone) ? (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
void handleOneClickCall(item);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
一键呼叫
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
{/*{item.storeName ? (*/}
|
{/*{item.storeName ? (*/}
|
||||||
{/* <View className="mt-1 text-xs text-gray-500">*/}
|
{/* <View className="mt-1 text-xs text-gray-500">*/}
|
||||||
{/* <Text>门店:{item.storeName}</Text>*/}
|
{/* <Text>门店:{item.storeName}</Text>*/}
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ import dayjs from 'dayjs'
|
|||||||
import { pageGltTicketOrder, updateGltTicketOrder } from '@/api/glt/gltTicketOrder'
|
import { pageGltTicketOrder, updateGltTicketOrder } from '@/api/glt/gltTicketOrder'
|
||||||
import type { GltTicketOrder, GltTicketOrderParam } from '@/api/glt/gltTicketOrder/model'
|
import type { GltTicketOrder, GltTicketOrderParam } from '@/api/glt/gltTicketOrder/model'
|
||||||
import { uploadFile } from '@/api/system/file'
|
import { uploadFile } from '@/api/system/file'
|
||||||
|
import { listShopStoreRider, updateShopStoreRider } from '@/api/shop/shopStoreRider'
|
||||||
|
import { getCurrentLngLat } from '@/utils/location'
|
||||||
|
|
||||||
const PAGE_SIZE = 10
|
const PAGE_SIZE = 10
|
||||||
|
|
||||||
@@ -236,6 +238,37 @@ export default function TicketOrdersPage() {
|
|||||||
}
|
}
|
||||||
setDeliverSubmitting(true)
|
setDeliverSubmitting(true)
|
||||||
try {
|
try {
|
||||||
|
// 送达时同步记录配送员当前位置(用于门店/后台跟踪骑手位置)
|
||||||
|
const loc = await getCurrentLngLat('确认送达需要记录您的当前位置,请在设置中开启定位权限后重试。')
|
||||||
|
if (!loc) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 优先按 userId 精确查找;后端若未支持该字段,会自动忽略,我们再做兜底。
|
||||||
|
let riderRow =
|
||||||
|
(await listShopStoreRider({ userId: riderId, storeId: deliverOrder.storeId, status: 1 } as any))
|
||||||
|
?.find(r => String(r?.userId || '') === String(riderId || '')) ||
|
||||||
|
null
|
||||||
|
|
||||||
|
// 兜底:按门店筛选后再匹配 userId
|
||||||
|
if (!riderRow && deliverOrder.storeId) {
|
||||||
|
const list = await listShopStoreRider({ storeId: deliverOrder.storeId, status: 1 } as any)
|
||||||
|
riderRow = list?.find(r => String(r?.userId || '') === String(riderId || '')) || null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (riderRow?.id) {
|
||||||
|
await updateShopStoreRider({
|
||||||
|
id: riderRow.id,
|
||||||
|
longitude: loc.lng,
|
||||||
|
latitude: loc.lat
|
||||||
|
} as any)
|
||||||
|
} else {
|
||||||
|
console.warn('未找到 ShopStoreRider 记录,无法更新骑手经纬度:', { riderId, storeId: deliverOrder.storeId })
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 不阻塞送达流程,但记录日志便于排查。
|
||||||
|
console.warn('更新 ShopStoreRider 经纬度失败:', e)
|
||||||
|
}
|
||||||
|
|
||||||
const now = dayjs().format('YYYY-MM-DD HH:mm:ss')
|
const now = dayjs().format('YYYY-MM-DD HH:mm:ss')
|
||||||
// 送达时间:首次“确认送达”写入;补传照片时不要覆盖原送达时间
|
// 送达时间:首次“确认送达”写入;补传照片时不要覆盖原送达时间
|
||||||
const deliveredAt = deliverOrder.sendEndTime || now
|
const deliveredAt = deliverOrder.sendEndTime || now
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { ArrowRight, Location, Ticket } from '@nutui/icons-react-taro'
|
|||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import type { ShopGoods } from '@/api/shop/shopGoods/model'
|
import type { ShopGoods } from '@/api/shop/shopGoods/model'
|
||||||
import { getShopGoods } from '@/api/shop/shopGoods'
|
import { getShopGoods } from '@/api/shop/shopGoods'
|
||||||
import { listShopUserAddress } from '@/api/shop/shopUserAddress'
|
import { getShopUserAddress, listShopUserAddress } from '@/api/shop/shopUserAddress'
|
||||||
import type { ShopUserAddress } from '@/api/shop/shopUserAddress/model'
|
import type { ShopUserAddress } from '@/api/shop/shopUserAddress/model'
|
||||||
import './use.scss'
|
import './use.scss'
|
||||||
import Gap from "@/components/Gap";
|
import Gap from "@/components/Gap";
|
||||||
@@ -27,6 +27,8 @@ import {getSelectedStoreFromStorage, saveSelectedStoreToStorage} from "@/utils/s
|
|||||||
import type { GltUserTicket } from '@/api/glt/gltUserTicket/model'
|
import type { GltUserTicket } from '@/api/glt/gltUserTicket/model'
|
||||||
import { listGltUserTicket } from '@/api/glt/gltUserTicket'
|
import { listGltUserTicket } from '@/api/glt/gltUserTicket'
|
||||||
import { addGltTicketOrder } from '@/api/glt/gltTicketOrder'
|
import { addGltTicketOrder } from '@/api/glt/gltTicketOrder'
|
||||||
|
import { pageGltTicketOrder } from '@/api/glt/gltTicketOrder'
|
||||||
|
import type { GltTicketOrder } from '@/api/glt/gltTicketOrder/model'
|
||||||
import type { ShopStoreRider } from '@/api/shop/shopStoreRider/model'
|
import type { ShopStoreRider } from '@/api/shop/shopStoreRider/model'
|
||||||
import { listShopStoreRider } from '@/api/shop/shopStoreRider'
|
import { listShopStoreRider } from '@/api/shop/shopStoreRider'
|
||||||
import type { ShopStoreFence } from '@/api/shop/shopStoreFence/model'
|
import type { ShopStoreFence } from '@/api/shop/shopStoreFence/model'
|
||||||
@@ -34,6 +36,7 @@ import { listShopStoreFence } from '@/api/shop/shopStoreFence'
|
|||||||
import { parseFencePoints, parseLngLatFromText, pointInAnyPolygon, pointInPolygon } from '@/utils/geofence'
|
import { parseFencePoints, parseLngLatFromText, pointInAnyPolygon, pointInPolygon } from '@/utils/geofence'
|
||||||
|
|
||||||
const MIN_START_QTY = 10
|
const MIN_START_QTY = 10
|
||||||
|
const ADDRESS_CHANGE_COOLDOWN_DAYS = 30
|
||||||
|
|
||||||
const OrderConfirm = () => {
|
const OrderConfirm = () => {
|
||||||
const [goods, setGoods] = useState<ShopGoods | null>(null);
|
const [goods, setGoods] = useState<ShopGoods | null>(null);
|
||||||
@@ -68,9 +71,9 @@ const OrderConfirm = () => {
|
|||||||
|
|
||||||
// 水票:用于“立即送水”下单(用水票抵扣,无需支付)
|
// 水票:用于“立即送水”下单(用水票抵扣,无需支付)
|
||||||
const [tickets, setTickets] = useState<GltUserTicket[]>([])
|
const [tickets, setTickets] = useState<GltUserTicket[]>([])
|
||||||
const [selectedTicketId, setSelectedTicketId] = useState<number | undefined>(undefined)
|
|
||||||
const [ticketPopupVisible, setTicketPopupVisible] = useState(false)
|
const [ticketPopupVisible, setTicketPopupVisible] = useState(false)
|
||||||
const [ticketLoading, setTicketLoading] = useState(false)
|
const [ticketLoading, setTicketLoading] = useState(false)
|
||||||
|
const [ticketLoaded, setTicketLoaded] = useState(false)
|
||||||
const noTicketPromptedRef = useRef(false)
|
const noTicketPromptedRef = useRef(false)
|
||||||
|
|
||||||
// Delivery range (geofence): block ordering if address/current location is outside.
|
// Delivery range (geofence): block ordering if address/current location is outside.
|
||||||
@@ -81,6 +84,8 @@ const OrderConfirm = () => {
|
|||||||
const [deliveryRangeChecking, setDeliveryRangeChecking] = useState(false)
|
const [deliveryRangeChecking, setDeliveryRangeChecking] = useState(false)
|
||||||
const deliveryRangeCheckingRef = useRef(false)
|
const deliveryRangeCheckingRef = useRef(false)
|
||||||
const [inDeliveryRange, setInDeliveryRange] = useState<boolean | undefined>(undefined)
|
const [inDeliveryRange, setInDeliveryRange] = useState<boolean | undefined>(undefined)
|
||||||
|
// Prevent using stale `inDeliveryRange` from a previous address when user switches addresses.
|
||||||
|
const [deliveryRangeCheckedAddressId, setDeliveryRangeCheckedAddressId] = useState<number | undefined>(undefined)
|
||||||
|
|
||||||
const router = Taro.getCurrentInstance().router;
|
const router = Taro.getCurrentInstance().router;
|
||||||
const goodsId = router?.params?.goodsId;
|
const goodsId = router?.params?.goodsId;
|
||||||
@@ -95,15 +100,173 @@ const OrderConfirm = () => {
|
|||||||
return Number.isFinite(id) && id > 0 ? id : undefined
|
return Number.isFinite(id) && id > 0 ? id : undefined
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
type TicketAddressModifyLimit = {
|
||||||
|
loaded: boolean
|
||||||
|
canModify: boolean
|
||||||
|
nextAllowedText?: string
|
||||||
|
lockedAddressId?: number
|
||||||
|
}
|
||||||
|
const [ticketAddressModifyLimit, setTicketAddressModifyLimit] = useState<TicketAddressModifyLimit>({
|
||||||
|
loaded: false,
|
||||||
|
canModify: true,
|
||||||
|
})
|
||||||
|
const ticketAddressModifyLimitPromiseRef = useRef<Promise<TicketAddressModifyLimit> | null>(null)
|
||||||
|
|
||||||
|
const parseTime = (raw?: unknown) => {
|
||||||
|
if (raw === undefined || raw === null || raw === '') return null
|
||||||
|
// Compatible with seconds/milliseconds timestamps.
|
||||||
|
if (typeof raw === 'number' || (typeof raw === 'string' && /^\d+$/.test(raw))) {
|
||||||
|
const n = Number(raw)
|
||||||
|
if (!Number.isFinite(n)) return null
|
||||||
|
return dayjs(n < 1e12 ? n * 1000 : n)
|
||||||
|
}
|
||||||
|
const d = dayjs(raw as any)
|
||||||
|
return d.isValid() ? d : null
|
||||||
|
}
|
||||||
|
|
||||||
|
const getOrderTime = (o?: Partial<GltTicketOrder> | null) => {
|
||||||
|
return parseTime(o?.createTime) || parseTime(o?.updateTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getOrderAddressKey = (o?: Partial<GltTicketOrder> | null) => {
|
||||||
|
const id = Number(o?.addressId)
|
||||||
|
if (Number.isFinite(id) && id > 0) return `id:${id}`
|
||||||
|
const txt = String(o?.address || '').trim()
|
||||||
|
if (txt) return `txt:${txt}`
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadTicketAddressModifyLimit = async (): Promise<TicketAddressModifyLimit> => {
|
||||||
|
if (ticketAddressModifyLimitPromiseRef.current) return ticketAddressModifyLimitPromiseRef.current
|
||||||
|
|
||||||
|
ticketAddressModifyLimitPromiseRef.current = (async () => {
|
||||||
|
if (!userId) return { loaded: true, canModify: true }
|
||||||
|
|
||||||
|
const now = dayjs()
|
||||||
|
const pageSize = 20
|
||||||
|
let page = 1
|
||||||
|
const all: GltTicketOrder[] = []
|
||||||
|
|
||||||
|
let latestKey = ''
|
||||||
|
let latestAddressId: number | undefined = undefined
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const res = await pageGltTicketOrder({ page, limit: pageSize, userId })
|
||||||
|
const list = Array.isArray(res?.list) ? res.list : []
|
||||||
|
if (page === 1) {
|
||||||
|
const first = list[0]
|
||||||
|
latestKey = getOrderAddressKey(first)
|
||||||
|
const id = Number(first?.addressId)
|
||||||
|
latestAddressId = Number.isFinite(id) && id > 0 ? id : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!list.length) break
|
||||||
|
all.push(...list)
|
||||||
|
|
||||||
|
// Find the oldest order in the newest contiguous block of the latest address key.
|
||||||
|
// That order's time represents the last time user "set/changed" the ticket delivery address.
|
||||||
|
const currentKey = latestKey
|
||||||
|
if (!currentKey) {
|
||||||
|
return { loaded: true, canModify: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastSameIndex = 0
|
||||||
|
let foundDifferent = false
|
||||||
|
for (let i = 1; i < all.length; i++) {
|
||||||
|
const k = getOrderAddressKey(all[i])
|
||||||
|
if (!k) continue
|
||||||
|
if (k === currentKey) {
|
||||||
|
lastSameIndex = i
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
foundDifferent = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundDifferent) {
|
||||||
|
const lastSetAt = getOrderTime(all[lastSameIndex])
|
||||||
|
if (!lastSetAt) return { loaded: true, canModify: true, lockedAddressId: latestAddressId }
|
||||||
|
const nextAllowed = lastSetAt.add(ADDRESS_CHANGE_COOLDOWN_DAYS, 'day')
|
||||||
|
const canModify = now.isAfter(nextAllowed)
|
||||||
|
return {
|
||||||
|
loaded: true,
|
||||||
|
canModify,
|
||||||
|
nextAllowedText: nextAllowed.format('YYYY-MM-DD'),
|
||||||
|
lockedAddressId: latestAddressId,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldest = getOrderTime(all[all.length - 1])
|
||||||
|
if (oldest && now.diff(oldest, 'day') >= ADDRESS_CHANGE_COOLDOWN_DAYS) {
|
||||||
|
// We have enough history beyond the cooldown window, and still no different address found.
|
||||||
|
return { loaded: true, canModify: true, lockedAddressId: latestAddressId }
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalCount = typeof (res as any)?.count === 'number' ? Number((res as any).count) : undefined
|
||||||
|
if (totalCount !== undefined && all.length >= totalCount) break
|
||||||
|
if (list.length < pageSize) break
|
||||||
|
|
||||||
|
page += 1
|
||||||
|
if (page > 10) break // safety: avoid excessive paging
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!all.length) return { loaded: true, canModify: true }
|
||||||
|
|
||||||
|
// If we can't prove the last-set time is older than the cooldown window, be conservative and lock.
|
||||||
|
const lastSetAt = getOrderTime(all[all.length - 1])
|
||||||
|
if (!lastSetAt) return { loaded: true, canModify: true, lockedAddressId: latestAddressId }
|
||||||
|
const nextAllowed = lastSetAt.add(ADDRESS_CHANGE_COOLDOWN_DAYS, 'day')
|
||||||
|
const canModify = now.isAfter(nextAllowed)
|
||||||
|
return {
|
||||||
|
loaded: true,
|
||||||
|
canModify,
|
||||||
|
nextAllowedText: nextAllowed.format('YYYY-MM-DD'),
|
||||||
|
lockedAddressId: latestAddressId,
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
.finally(() => {
|
||||||
|
ticketAddressModifyLimitPromiseRef.current = null
|
||||||
|
})
|
||||||
|
|
||||||
|
return ticketAddressModifyLimitPromiseRef.current
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTicketAvailableQty = (t?: Partial<GltUserTicket> | null) => {
|
||||||
|
if (!t) return 0
|
||||||
|
const anyT: any = t
|
||||||
|
const raw =
|
||||||
|
anyT.availableQty ??
|
||||||
|
anyT.availableNum ??
|
||||||
|
anyT.availableCount ??
|
||||||
|
anyT.remainQty ??
|
||||||
|
anyT.remainNum ??
|
||||||
|
anyT.remainCount
|
||||||
|
const n = Number(raw)
|
||||||
|
if (Number.isFinite(n)) return n
|
||||||
|
|
||||||
|
// Fallback for tenants that don't return `availableQty`.
|
||||||
|
const total = Number(anyT.totalQty ?? anyT.totalNum ?? anyT.totalCount ?? 0)
|
||||||
|
const used = Number(anyT.usedQty ?? anyT.usedNum ?? anyT.usedCount ?? 0)
|
||||||
|
const frozen = Number(anyT.frozenQty ?? anyT.frozenNum ?? anyT.frozenCount ?? 0)
|
||||||
|
const computed = (Number.isFinite(total) ? total : 0) - (Number.isFinite(used) ? used : 0) - (Number.isFinite(frozen) ? frozen : 0)
|
||||||
|
return Number.isFinite(computed) ? computed : 0
|
||||||
|
}
|
||||||
|
|
||||||
const usableTickets = useMemo(() => {
|
const usableTickets = useMemo(() => {
|
||||||
const list = (tickets || [])
|
const list = (tickets || [])
|
||||||
.filter(t => t?.deleted !== 1)
|
.filter(t => Number(t?.deleted) !== 1)
|
||||||
.filter(t => t?.status !== 1)
|
// 1 = 冻结(兼容 status 为字符串)
|
||||||
.filter(t => Number.isFinite(Number(t?.id)) && Number(t.id) > 0)
|
.filter(t => Number(t?.status) !== 1)
|
||||||
.filter(t => (t.availableQty ?? 0) > 0)
|
.filter(t => Number.isFinite(Number(t?.id)) && Number(t?.id) > 0)
|
||||||
// Some tenants don't fill goodsId on ticket; allow it as a fallback.
|
.filter(t => getTicketAvailableQty(t) > 0)
|
||||||
.filter(t => (numericGoodsId ? (!t.goodsId || t.goodsId === numericGoodsId) : true))
|
// Some tenants return goodsId as string; coerce before comparison.
|
||||||
// FIFO: use older tickets first (reduce disputes).
|
.filter((t) => {
|
||||||
|
if (!numericGoodsId) return true
|
||||||
|
const tg = Number((t as any)?.goodsId)
|
||||||
|
const hasGoodsId = Number.isFinite(tg) && tg > 0
|
||||||
|
return !hasGoodsId || tg === numericGoodsId
|
||||||
|
})
|
||||||
|
// Default order in list: older first (reduce disputes). Real consumption order is computed separately.
|
||||||
return list.sort((a, b) => {
|
return list.sort((a, b) => {
|
||||||
const ta = new Date(a.createTime || 0).getTime() || 0
|
const ta = new Date(a.createTime || 0).getTime() || 0
|
||||||
const tb = new Date(b.createTime || 0).getTime() || 0
|
const tb = new Date(b.createTime || 0).getTime() || 0
|
||||||
@@ -112,19 +275,28 @@ const OrderConfirm = () => {
|
|||||||
})
|
})
|
||||||
}, [tickets, numericGoodsId])
|
}, [tickets, numericGoodsId])
|
||||||
|
|
||||||
const selectedTicket = useMemo(() => {
|
|
||||||
if (!selectedTicketId) return undefined
|
|
||||||
return usableTickets.find(t => Number(t.id) === Number(selectedTicketId))
|
|
||||||
}, [usableTickets, selectedTicketId])
|
|
||||||
|
|
||||||
const availableTicketTotal = useMemo(() => {
|
const availableTicketTotal = useMemo(() => {
|
||||||
return Number(selectedTicket?.availableQty || 0)
|
return usableTickets.reduce((sum, t) => sum + getTicketAvailableQty(t), 0)
|
||||||
}, [selectedTicket?.availableQty])
|
}, [usableTickets])
|
||||||
|
|
||||||
|
// Consume tickets with smaller available qty first; ties: older first.
|
||||||
|
const ticketsToConsume = useMemo(() => {
|
||||||
|
const list = [...usableTickets]
|
||||||
|
return list.sort((a, b) => {
|
||||||
|
const qa = getTicketAvailableQty(a)
|
||||||
|
const qb = getTicketAvailableQty(b)
|
||||||
|
if (qa !== qb) return qa - qb
|
||||||
|
const ta = new Date(a.createTime || 0).getTime() || 0
|
||||||
|
const tb = new Date(b.createTime || 0).getTime() || 0
|
||||||
|
if (ta !== tb) return ta - tb
|
||||||
|
return (a.id || 0) - (b.id || 0)
|
||||||
|
})
|
||||||
|
}, [usableTickets])
|
||||||
|
|
||||||
const noUsableTickets = useMemo(() => {
|
const noUsableTickets = useMemo(() => {
|
||||||
// Only show "go buy tickets" guidance after we have finished loading.
|
// Only show "go buy tickets" guidance after we have finished loading.
|
||||||
return !!userId && !ticketLoading && usableTickets.length === 0
|
return !!userId && ticketLoaded && !ticketLoading && usableTickets.length === 0
|
||||||
}, [ticketLoading, usableTickets.length, userId])
|
}, [ticketLoaded, ticketLoading, usableTickets.length, userId])
|
||||||
|
|
||||||
const maxQuantity = useMemo(() => {
|
const maxQuantity = useMemo(() => {
|
||||||
const stockMax = goods?.stock ?? 999
|
const stockMax = goods?.stock ?? 999
|
||||||
@@ -161,6 +333,22 @@ const OrderConfirm = () => {
|
|||||||
return parseLngLatFromText((s.lngAndLat || s.location || '').trim())
|
return parseLngLatFromText((s.lngAndLat || s.location || '').trim())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openAddressPage = async () => {
|
||||||
|
const limit = ticketAddressModifyLimit.loaded
|
||||||
|
? ticketAddressModifyLimit
|
||||||
|
: await loadTicketAddressModifyLimit().catch(() => ({ loaded: true, canModify: true } as TicketAddressModifyLimit))
|
||||||
|
if (!ticketAddressModifyLimit.loaded) setTicketAddressModifyLimit(limit)
|
||||||
|
|
||||||
|
if (!limit.canModify) {
|
||||||
|
Taro.showToast({
|
||||||
|
title: `送水地址每${ADDRESS_CHANGE_COOLDOWN_DAYS}天可修改一次${limit.nextAllowedText ? ',' + limit.nextAllowedText + ' 后可修改' : ''}`,
|
||||||
|
icon: 'none',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Taro.navigateTo({ url: '/user/address/index' })
|
||||||
|
}
|
||||||
|
|
||||||
const loadFences = async (): Promise<ShopStoreFence[]> => {
|
const loadFences = async (): Promise<ShopStoreFence[]> => {
|
||||||
if (fencesLoadedRef.current) return fences
|
if (fencesLoadedRef.current) return fences
|
||||||
if (fencesPromiseRef.current) return fencesPromiseRef.current
|
if (fencesPromiseRef.current) return fencesPromiseRef.current
|
||||||
@@ -213,12 +401,11 @@ const OrderConfirm = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getCheckPoint = async (): Promise<{ lng: number; lat: number }> => {
|
const getCheckPoint = async (): Promise<{ lng: number; lat: number }> => {
|
||||||
// Prefer address coords (delivery location). Fallback to current GPS if address doesn't have coords.
|
// Immediate water delivery must validate by the delivery address coordinates.
|
||||||
|
// Falling back to current GPS may allow ordering with an out-of-fence address.
|
||||||
const byAddress = parseLngLatFromText(`${address?.lng || ''},${address?.lat || ''}`)
|
const byAddress = parseLngLatFromText(`${address?.lng || ''},${address?.lat || ''}`)
|
||||||
if (byAddress) return byAddress
|
if (byAddress) return byAddress
|
||||||
|
throw new Error('该收货地址缺少经纬度,请在地址里选择地图定位后重试')
|
||||||
const loc = await Taro.getLocation({ type: 'gcj02' })
|
|
||||||
return { lng: loc.longitude, lat: loc.latitude }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ensureInDeliveryRange = async (): Promise<boolean> => {
|
const ensureInDeliveryRange = async (): Promise<boolean> => {
|
||||||
@@ -229,6 +416,7 @@ const OrderConfirm = () => {
|
|||||||
const p = await getCheckPoint()
|
const p = await getCheckPoint()
|
||||||
const ok = await isPointInFence(p)
|
const ok = await isPointInFence(p)
|
||||||
setInDeliveryRange(ok)
|
setInDeliveryRange(ok)
|
||||||
|
setDeliveryRangeCheckedAddressId(address?.id)
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
Taro.showToast({ title: '不在配送范围内,暂不支持下单', icon: 'none' })
|
Taro.showToast({ title: '不在配送范围内,暂不支持下单', icon: 'none' })
|
||||||
}
|
}
|
||||||
@@ -236,30 +424,8 @@ const OrderConfirm = () => {
|
|||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('配送范围校验失败:', e)
|
console.error('配送范围校验失败:', e)
|
||||||
setInDeliveryRange(undefined)
|
setInDeliveryRange(undefined)
|
||||||
|
setDeliveryRangeCheckedAddressId(undefined)
|
||||||
const msg = String(e?.errMsg || e?.message || '')
|
// Note: we validate by address coords only; no GPS permission prompt here.
|
||||||
const denied =
|
|
||||||
msg.includes('auth deny') ||
|
|
||||||
msg.includes('authorize') ||
|
|
||||||
msg.includes('permission') ||
|
|
||||||
msg.includes('denied') ||
|
|
||||||
msg.includes('scope.userLocation')
|
|
||||||
|
|
||||||
if (denied) {
|
|
||||||
const r = await Taro.showModal({
|
|
||||||
title: '需要定位权限',
|
|
||||||
content: '下单前需要校验是否在配送范围内,请在设置中开启定位权限后重试。',
|
|
||||||
confirmText: '去设置'
|
|
||||||
})
|
|
||||||
if (r.confirm) {
|
|
||||||
try {
|
|
||||||
await Taro.openSetting()
|
|
||||||
} catch (_e) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
Taro.showToast({ title: e?.message || '配送范围校验失败,请稍后重试', icon: 'none' })
|
Taro.showToast({ title: e?.message || '配送范围校验失败,请稍后重试', icon: 'none' })
|
||||||
return false
|
return false
|
||||||
@@ -420,11 +586,14 @@ const OrderConfirm = () => {
|
|||||||
if (ticketLoading) return
|
if (ticketLoading) return
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
setTickets([])
|
setTickets([])
|
||||||
|
setTicketLoaded(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
setTicketLoading(true)
|
setTicketLoading(true)
|
||||||
const list = await listGltUserTicket({ userId, status: 0 })
|
// Do not pass `status` here: some backends use different status semantics;
|
||||||
|
// we filter out frozen tickets on the client for compatibility.
|
||||||
|
const list = await listGltUserTicket({ userId })
|
||||||
setTickets(list || [])
|
setTickets(list || [])
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('获取水票失败:', e)
|
console.error('获取水票失败:', e)
|
||||||
@@ -432,6 +601,7 @@ const OrderConfirm = () => {
|
|||||||
Taro.showToast({ title: '获取水票失败', icon: 'none' })
|
Taro.showToast({ title: '获取水票失败', icon: 'none' })
|
||||||
} finally {
|
} finally {
|
||||||
setTicketLoading(false)
|
setTicketLoading(false)
|
||||||
|
setTicketLoaded(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -465,15 +635,43 @@ const OrderConfirm = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ticket delivery address is based on order snapshot. Enforce "once per 30 days" by latest ticket-order history.
|
||||||
|
const limit = ticketAddressModifyLimit.loaded
|
||||||
|
? ticketAddressModifyLimit
|
||||||
|
: await loadTicketAddressModifyLimit().catch(() => ({ loaded: true, canModify: true } as TicketAddressModifyLimit))
|
||||||
|
if (!ticketAddressModifyLimit.loaded) setTicketAddressModifyLimit(limit)
|
||||||
|
if (!limit.canModify && limit.lockedAddressId && address.id !== limit.lockedAddressId) {
|
||||||
|
Taro.showToast({
|
||||||
|
title: `送水地址每${ADDRESS_CHANGE_COOLDOWN_DAYS}天可修改一次,请使用上次下单地址${limit.nextAllowedText ? '(' + limit.nextAllowedText + ' 后可修改)' : ''}`,
|
||||||
|
icon: 'none',
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
const locked = await getShopUserAddress(limit.lockedAddressId)
|
||||||
|
if (locked?.id) setAddress(locked)
|
||||||
|
} catch (_e) {
|
||||||
|
// ignore: keep current address, but still block submission
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!addressHasCoords) {
|
||||||
|
Taro.showToast({ title: '该收货地址缺少经纬度,请在地址里选择地图定位后重试', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure ticket list is loaded.
|
||||||
|
if (ticketLoading) {
|
||||||
|
Taro.showToast({ title: '水票加载中,请稍后再试', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!ticketLoaded) {
|
||||||
|
await loadUserTickets()
|
||||||
|
}
|
||||||
|
|
||||||
const storeForOrder = await resolveStoreForOrder()
|
const storeForOrder = await resolveStoreForOrder()
|
||||||
if (!storeForOrder?.id) {
|
if (!storeForOrder?.id) {
|
||||||
Taro.showToast({ title: '未找到可配送门店,请先在首页选择门店或联系管理员配置门店坐标', icon: 'none' })
|
Taro.showToast({ title: '未找到可配送门店,请先在首页选择门店或联系管理员配置门店坐标', icon: 'none' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!selectedTicket?.id) {
|
|
||||||
Taro.showToast({ title: '请选择水票', icon: 'none' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (availableTicketTotal <= 0) {
|
if (availableTicketTotal <= 0) {
|
||||||
Taro.showToast({ title: '暂无可用水票', icon: 'none' })
|
Taro.showToast({ title: '暂无可用水票', icon: 'none' })
|
||||||
return
|
return
|
||||||
@@ -507,7 +705,7 @@ const OrderConfirm = () => {
|
|||||||
|
|
||||||
const confirmRes = await Taro.showModal({
|
const confirmRes = await Taro.showModal({
|
||||||
title: '确认下单',
|
title: '确认下单',
|
||||||
content: `配送时间:${sendTimeText}\n将使用 ${finalQty} 张水票下单,送水 ${finalQty} 桶,是否确认?`
|
content: `配送时间:${sendTimeText}\n将使用 ${finalQty} 张水票下单(优先使用可用数量少的水票),送水 ${finalQty} 桶,是否确认?`
|
||||||
})
|
})
|
||||||
if (!confirmRes.confirm) return
|
if (!confirmRes.confirm) return
|
||||||
|
|
||||||
@@ -518,24 +716,41 @@ const OrderConfirm = () => {
|
|||||||
// Best-effort auto dispatch rider. If it fails, backend/manual dispatch can still handle it.
|
// Best-effort auto dispatch rider. If it fails, backend/manual dispatch can still handle it.
|
||||||
const autoRider = storeForOrder.id ? await resolveAutoRiderForStore(storeForOrder.id) : null
|
const autoRider = storeForOrder.id ? await resolveAutoRiderForStore(storeForOrder.id) : null
|
||||||
|
|
||||||
await addGltTicketOrder({
|
// Split this "delivery" into multiple ticket orders (backend order model binds to one userTicketId).
|
||||||
userTicketId: selectedTicket.id,
|
// Consume tickets with smaller available qty first.
|
||||||
storeId: storeForOrder.id,
|
let remain = finalQty
|
||||||
addressId: address.id,
|
let created = 0
|
||||||
totalNum: finalQty,
|
for (const t of ticketsToConsume) {
|
||||||
buyerRemarks: orderRemark,
|
if (remain <= 0) break
|
||||||
sendTime: dayjs(sendTime).startOf('day').format('YYYY-MM-DD HH:mm:ss'),
|
const avail = getTicketAvailableQty(t)
|
||||||
// Backend may take userId from token; pass-through is harmless if backend ignores it.
|
const useQty = Math.min(remain, avail)
|
||||||
userId,
|
if (useQty <= 0) continue
|
||||||
riderId: Number.isFinite(Number(autoRider?.userId)) ? Number(autoRider?.userId) : undefined,
|
await addGltTicketOrder({
|
||||||
riderName: autoRider?.realName,
|
userTicketId: Number(t.id),
|
||||||
riderPhone: autoRider?.mobile,
|
storeId: storeForOrder.id,
|
||||||
comments: goods.name ? `立即送水:${goods.name}` : '立即送水'
|
addressId: address.id,
|
||||||
})
|
totalNum: useQty,
|
||||||
|
buyerRemarks: orderRemark,
|
||||||
|
sendTime: dayjs(sendTime).startOf('day').format('YYYY-MM-DD HH:mm:ss'),
|
||||||
|
// Backend may take userId from token; pass-through is harmless if backend ignores it.
|
||||||
|
userId,
|
||||||
|
riderId: Number.isFinite(Number(autoRider?.userId)) ? Number(autoRider?.userId) : undefined,
|
||||||
|
riderName: autoRider?.realName,
|
||||||
|
riderPhone: autoRider?.mobile,
|
||||||
|
comments: goods.name ? `立即送水:${goods.name}` : '立即送水'
|
||||||
|
})
|
||||||
|
remain -= useQty
|
||||||
|
created += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remain > 0) {
|
||||||
|
// Ticket counts might have changed between loading and submission.
|
||||||
|
throw new Error('水票可用次数不足,请刷新后重试')
|
||||||
|
}
|
||||||
|
|
||||||
await loadUserTickets()
|
await loadUserTickets()
|
||||||
|
|
||||||
Taro.showToast({ title: '下单成功', icon: 'success' })
|
Taro.showToast({ title: created > 1 ? '下单成功(已拆分多张水票)' : '下单成功', icon: 'success' })
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// 跳转到“我的送水订单”
|
// 跳转到“我的送水订单”
|
||||||
Taro.redirectTo({ url: '/user/ticket/index?tab=order' })
|
Taro.redirectTo({ url: '/user/ticket/index?tab=order' })
|
||||||
@@ -572,6 +787,20 @@ const OrderConfirm = () => {
|
|||||||
if (addressRes && addressRes.length > 0) {
|
if (addressRes && addressRes.length > 0) {
|
||||||
setAddress(addressRes[0])
|
setAddress(addressRes[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load ticket-order history to enforce "address can be modified once per 30 days".
|
||||||
|
// If currently locked, force using last ticket-order address (snapshot) to avoid getting stuck with a new default address.
|
||||||
|
try {
|
||||||
|
const limit = await loadTicketAddressModifyLimit()
|
||||||
|
setTicketAddressModifyLimit(limit)
|
||||||
|
if (!limit.canModify && limit.lockedAddressId) {
|
||||||
|
const locked = await getShopUserAddress(limit.lockedAddressId)
|
||||||
|
if (locked?.id) setAddress(locked)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('加载送水地址修改限制失败:', e)
|
||||||
|
setTicketAddressModifyLimit({ loaded: true, canModify: true })
|
||||||
|
}
|
||||||
// Tickets are non-blocking for first paint; load in background.
|
// Tickets are non-blocking for first paint; load in background.
|
||||||
loadUserTickets()
|
loadUserTickets()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -593,6 +822,11 @@ const OrderConfirm = () => {
|
|||||||
loadAllData({ silent: hasInitialLoadedRef.current })
|
loadAllData({ silent: hasInitialLoadedRef.current })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const addressHasCoords = useMemo(() => {
|
||||||
|
if (!address?.id) return false
|
||||||
|
return !!parseLngLatFromText(`${address?.lng || ''},${address?.lat || ''}`)
|
||||||
|
}, [address?.id, address?.lng, address?.lat])
|
||||||
|
|
||||||
// Auto-pick nearest store by delivery address (best-effort, won't override manual selection).
|
// Auto-pick nearest store by delivery address (best-effort, won't override manual selection).
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!address?.id) return
|
if (!address?.id) return
|
||||||
@@ -605,17 +839,35 @@ const OrderConfirm = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
;(async () => {
|
;(async () => {
|
||||||
|
if (!address?.id) {
|
||||||
|
setInDeliveryRange(undefined)
|
||||||
|
setDeliveryRangeCheckedAddressId(undefined)
|
||||||
|
return
|
||||||
|
}
|
||||||
const p = parseLngLatFromText(`${address?.lng || ''},${address?.lat || ''}`)
|
const p = parseLngLatFromText(`${address?.lng || ''},${address?.lat || ''}`)
|
||||||
if (!p) return
|
if (!p) {
|
||||||
|
// Cannot validate without address coords -> treat as out of range to block ordering.
|
||||||
|
setInDeliveryRange(false)
|
||||||
|
setDeliveryRangeCheckedAddressId(address.id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Avoid keeping stale state from previous address while we validate this one.
|
||||||
|
setInDeliveryRange(undefined)
|
||||||
|
setDeliveryRangeCheckedAddressId(undefined)
|
||||||
let ok = true
|
let ok = true
|
||||||
try {
|
try {
|
||||||
ok = await isPointInFence(p)
|
ok = await isPointInFence(p)
|
||||||
} catch (_e) {
|
} catch (_e) {
|
||||||
// Pre-check is best-effort; don't block UI here.
|
// Pre-check is best-effort; don't block UI here.
|
||||||
|
if (!cancelled) {
|
||||||
|
setInDeliveryRange(undefined)
|
||||||
|
setDeliveryRangeCheckedAddressId(undefined)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (cancelled) return
|
if (cancelled) return
|
||||||
setInDeliveryRange(ok)
|
setInDeliveryRange(ok)
|
||||||
|
setDeliveryRangeCheckedAddressId(address.id)
|
||||||
})()
|
})()
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true
|
cancelled = true
|
||||||
@@ -623,6 +875,29 @@ const OrderConfirm = () => {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [address?.id, address?.lng, address?.lat])
|
}, [address?.id, address?.lng, address?.lat])
|
||||||
|
|
||||||
|
// When user changes the delivery address to an out-of-fence one, prompt immediately (once per address).
|
||||||
|
const outOfRangePromptedAddressIdRef = useRef<number | undefined>(undefined)
|
||||||
|
useEffect(() => {
|
||||||
|
// Only prompt when user is allowed to change the ticket delivery address.
|
||||||
|
// Otherwise this toast is noisy (they can't fix it within the cooldown window).
|
||||||
|
if (!ticketAddressModifyLimit.loaded) return
|
||||||
|
if (!ticketAddressModifyLimit.canModify) return
|
||||||
|
const id = address?.id
|
||||||
|
if (!id) return
|
||||||
|
if (deliveryRangeCheckedAddressId !== id) return
|
||||||
|
if (inDeliveryRange !== false) return
|
||||||
|
if (outOfRangePromptedAddressIdRef.current === id) return
|
||||||
|
outOfRangePromptedAddressIdRef.current = id
|
||||||
|
Taro.showToast({ title: addressHasCoords ? '该地址不在配送范围,请更换围栏内地址' : '该地址缺少定位,请在地址里选择地图定位后重试', icon: 'none' })
|
||||||
|
}, [
|
||||||
|
address?.id,
|
||||||
|
addressHasCoords,
|
||||||
|
deliveryRangeCheckedAddressId,
|
||||||
|
inDeliveryRange,
|
||||||
|
ticketAddressModifyLimit.loaded,
|
||||||
|
ticketAddressModifyLimit.canModify
|
||||||
|
])
|
||||||
|
|
||||||
// When tickets/stock change, clamp quantity into [0..maxQuantity].
|
// When tickets/stock change, clamp quantity into [0..maxQuantity].
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setQuantity(prev => {
|
setQuantity(prev => {
|
||||||
@@ -633,18 +908,6 @@ const OrderConfirm = () => {
|
|||||||
})
|
})
|
||||||
}, [maxQuantity])
|
}, [maxQuantity])
|
||||||
|
|
||||||
// Auto-pick a default ticket (first usable) when ticket list changes.
|
|
||||||
useEffect(() => {
|
|
||||||
if (!usableTickets.length) {
|
|
||||||
setSelectedTicketId(undefined)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const currentValid = selectedTicketId && usableTickets.some(t => Number(t.id) === Number(selectedTicketId))
|
|
||||||
if (!currentValid) {
|
|
||||||
setSelectedTicketId(Number(usableTickets[0].id))
|
|
||||||
}
|
|
||||||
}, [usableTickets, selectedTicketId])
|
|
||||||
|
|
||||||
// If user has no usable tickets, proactively guide them to purchase (only once per page lifecycle).
|
// If user has no usable tickets, proactively guide them to purchase (only once per page lifecycle).
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!noUsableTickets) return
|
if (!noUsableTickets) return
|
||||||
@@ -708,10 +971,13 @@ const OrderConfirm = () => {
|
|||||||
{/* onClick={openStorePopup}*/}
|
{/* onClick={openStorePopup}*/}
|
||||||
{/* />*/}
|
{/* />*/}
|
||||||
{/*</CellGroup>*/}
|
{/*</CellGroup>*/}
|
||||||
<CellGroup>
|
<CellGroup>
|
||||||
{
|
{
|
||||||
address && (
|
address && (
|
||||||
<Cell className={'address-bottom-line'}>
|
<Cell
|
||||||
|
className={'address-bottom-line'}
|
||||||
|
onClick={openAddressPage}
|
||||||
|
>
|
||||||
<Space>
|
<Space>
|
||||||
<Location className={'text-gray-500'}/>
|
<Location className={'text-gray-500'}/>
|
||||||
<View className={'flex flex-col w-full justify-between items-start'}>
|
<View className={'flex flex-col w-full justify-between items-start'}>
|
||||||
@@ -723,14 +989,22 @@ const OrderConfirm = () => {
|
|||||||
<ArrowRight className={'text-gray-400'} size={14}/>
|
<ArrowRight className={'text-gray-400'} size={14}/>
|
||||||
</View>
|
</View>
|
||||||
</Space>
|
</Space>
|
||||||
<View className={'pt-1 pb-3 text-gray-500'}>{address.name} {address.phone}</View>
|
<View className={'pt-1 pb-3'}>
|
||||||
</View>
|
<View className={'text-gray-500'}>{address.name} {address.phone}</View>
|
||||||
</Space>
|
{ticketAddressModifyLimit.loaded && !ticketAddressModifyLimit.canModify && (
|
||||||
</Cell>
|
<View className={'pt-1 text-xs text-orange-500 hidden'}>
|
||||||
)
|
送水地址每{ADDRESS_CHANGE_COOLDOWN_DAYS}天可修改一次
|
||||||
}
|
{ticketAddressModifyLimit.nextAllowedText ? `,${ticketAddressModifyLimit.nextAllowedText} 后可修改` : ''}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Space>
|
||||||
|
</Cell>
|
||||||
|
)
|
||||||
|
}
|
||||||
{!address && (
|
{!address && (
|
||||||
<Cell className={''} onClick={() => Taro.navigateTo({url: '/user/address/index'})}>
|
<Cell className={''} onClick={openAddressPage}>
|
||||||
<Space>
|
<Space>
|
||||||
<Location/>
|
<Location/>
|
||||||
添加收货地址
|
添加收货地址
|
||||||
@@ -759,55 +1033,65 @@ const OrderConfirm = () => {
|
|||||||
: `最低起送 ${MIN_START_QTY} 桶(当前最多 ${maxQuantity} 桶)`
|
: `最低起送 ${MIN_START_QTY} 桶(当前最多 ${maxQuantity} 桶)`
|
||||||
}
|
}
|
||||||
extra={(
|
extra={(
|
||||||
<ConfigProvider theme={customTheme}>
|
<ConfigProvider theme={customTheme}>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
value={displayQty}
|
value={displayQty}
|
||||||
min={canStartOrder ? MIN_START_QTY : 0}
|
min={canStartOrder ? MIN_START_QTY : 0}
|
||||||
max={canStartOrder ? maxQuantity : 0}
|
max={canStartOrder ? maxQuantity : 0}
|
||||||
disabled={!canStartOrder}
|
step={10}
|
||||||
onChange={handleQuantityChange}
|
readOnly
|
||||||
/>
|
disabled={!canStartOrder}
|
||||||
</ConfigProvider>
|
onChange={handleQuantityChange}
|
||||||
)}
|
/>
|
||||||
/>
|
</ConfigProvider>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</CellGroup>
|
</CellGroup>
|
||||||
|
|
||||||
<CellGroup>
|
<CellGroup>
|
||||||
<Cell
|
<Cell
|
||||||
title={(
|
title={(
|
||||||
<View className="flex items-center gap-2">
|
<View className="flex items-center gap-2">
|
||||||
<Ticket className={'text-gray-500'}/>
|
<Ticket className={'text-gray-500'}/>
|
||||||
<Text>选择水票</Text>
|
<Text>水票明细</Text>
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
extra={(
|
|
||||||
<View className={'flex items-center gap-2'}>
|
|
||||||
<View className={'text-gray-900'}>
|
|
||||||
{ticketLoading
|
|
||||||
? '加载中...'
|
|
||||||
: (selectedTicket
|
|
||||||
? `${selectedTicket.templateName || '水票'}(可用${selectedTicket.availableQty ?? 0})`
|
|
||||||
: (noUsableTickets ? '暂无可用水票' : '请选择')
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</View>
|
|
||||||
<ArrowRight className={'text-gray-400'} size={14}/>
|
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
onClick={async () => {
|
extra={(
|
||||||
if (ticketLoading) return
|
<View className={'flex items-center gap-2'}>
|
||||||
if (noUsableTickets) {
|
<View className={'text-gray-900'}>
|
||||||
const r = await Taro.showModal({
|
{ticketLoading
|
||||||
title: '暂无可用水票',
|
? '加载中...'
|
||||||
content: '您还没有可用水票,是否前往购买?',
|
: (ticketLoaded
|
||||||
confirmText: '去购买',
|
? (noUsableTickets
|
||||||
cancelText: '暂不'
|
? '暂无可用水票'
|
||||||
})
|
: `可用合计 ${availableTicketTotal}(${usableTickets.length}组)`
|
||||||
if (r.confirm) await goBuyTickets()
|
)
|
||||||
return
|
: '点击查看'
|
||||||
}
|
)
|
||||||
setTicketPopupVisible(true)
|
}
|
||||||
}}
|
</View>
|
||||||
|
<ArrowRight className={'text-gray-400'} size={14}/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
onClick={async () => {
|
||||||
|
if (ticketLoading) return
|
||||||
|
if (!ticketLoaded) {
|
||||||
|
setTicketPopupVisible(true)
|
||||||
|
await loadUserTickets()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (noUsableTickets) {
|
||||||
|
const r = await Taro.showModal({
|
||||||
|
title: '暂无可用水票',
|
||||||
|
content: '您还没有可用水票,是否前往购买?',
|
||||||
|
confirmText: '去购买',
|
||||||
|
cancelText: '暂不'
|
||||||
|
})
|
||||||
|
if (r.confirm) await goBuyTickets()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setTicketPopupVisible(true)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
{noUsableTickets && (
|
{noUsableTickets && (
|
||||||
<Cell
|
<Cell
|
||||||
@@ -838,50 +1122,50 @@ const OrderConfirm = () => {
|
|||||||
)}/>
|
)}/>
|
||||||
</CellGroup>
|
</CellGroup>
|
||||||
|
|
||||||
{/* 水票明细弹窗 */}
|
{/* 水票明细弹窗 */}
|
||||||
<Popup
|
<Popup
|
||||||
visible={ticketPopupVisible}
|
visible={ticketPopupVisible}
|
||||||
position="bottom"
|
position="bottom"
|
||||||
style={{ height: '70vh' }}
|
style={{ height: '70vh' }}
|
||||||
onClose={() => setTicketPopupVisible(false)}
|
onClose={() => setTicketPopupVisible(false)}
|
||||||
>
|
>
|
||||||
<View className="p-4">
|
<View className="p-4">
|
||||||
<View className="flex justify-between items-center mb-3">
|
<View className="flex justify-between items-center mb-3">
|
||||||
<Text className="text-base font-medium">水票明细</Text>
|
<Text className="text-base font-medium">水票明细</Text>
|
||||||
<Text
|
<Text
|
||||||
className="text-sm text-gray-500"
|
className="text-sm text-gray-500"
|
||||||
onClick={() => setTicketPopupVisible(false)}
|
onClick={() => setTicketPopupVisible(false)}
|
||||||
>
|
>
|
||||||
关闭
|
关闭
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
{!!usableTickets.length && !ticketLoading && (
|
||||||
|
<View className="text-xs text-gray-500 mb-2">
|
||||||
|
<Text>可用合计 {availableTicketTotal} 张;下单时将优先使用可用数量少的水票</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
{ticketLoading ? (
|
{ticketLoading ? (
|
||||||
<View className="py-10 text-center text-gray-500">
|
<View className="py-10 text-center text-gray-500">
|
||||||
<Text>加载中...</Text>
|
<Text>加载中...</Text>
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{!!usableTickets.length ? (
|
{!!usableTickets.length ? (
|
||||||
<CellGroup>
|
<CellGroup>
|
||||||
{usableTickets.map((t) => {
|
{ticketsToConsume.map((t) => {
|
||||||
const active = selectedTicket?.id && Number(selectedTicket.id) === Number(t.id)
|
return (
|
||||||
return (
|
<Cell
|
||||||
<Cell
|
key={t.id}
|
||||||
key={t.id}
|
title={<Text>票号 {t.id}</Text>}
|
||||||
title={<Text className={active ? 'text-green-600' : ''}>票号 {t.id}</Text>}
|
description={t.orderNo ? `来源订单:${t.orderNo}` : ''}
|
||||||
description={t.orderNo ? `来源订单:${t.orderNo}` : ''}
|
extra={<Text className="text-gray-700">可用 {getTicketAvailableQty(t)}</Text>}
|
||||||
extra={<Text className="text-gray-700">可用 {t.availableQty ?? 0}</Text>}
|
onClick={() => setTicketPopupVisible(false)}
|
||||||
onClick={() => {
|
/>
|
||||||
setSelectedTicketId(Number(t.id))
|
)
|
||||||
setTicketPopupVisible(false)
|
})}
|
||||||
Taro.showToast({ title: '水票已选择', icon: 'success' })
|
</CellGroup>
|
||||||
}}
|
) : (
|
||||||
/>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</CellGroup>
|
|
||||||
) : (
|
|
||||||
<View className="py-10 text-center">
|
<View className="py-10 text-center">
|
||||||
<Empty description="暂无可用水票" />
|
<Empty description="暂无可用水票" />
|
||||||
<View className="mt-4 flex justify-center">
|
<View className="mt-4 flex justify-center">
|
||||||
@@ -890,11 +1174,11 @@ const OrderConfirm = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</Popup>
|
</Popup>
|
||||||
|
|
||||||
{/* 门店选择弹窗 */}
|
{/* 门店选择弹窗 */}
|
||||||
<Popup
|
<Popup
|
||||||
@@ -973,22 +1257,32 @@ const OrderConfirm = () => {
|
|||||||
去购买水票
|
去购买水票
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
type="success"
|
type="success"
|
||||||
size="large"
|
size="large"
|
||||||
loading={submitLoading || deliveryRangeChecking}
|
loading={submitLoading || deliveryRangeChecking}
|
||||||
disabled={
|
disabled={
|
||||||
deliveryRangeChecking ||
|
deliveryRangeChecking ||
|
||||||
inDeliveryRange === false ||
|
!address?.id ||
|
||||||
!selectedTicket?.id ||
|
!addressHasCoords ||
|
||||||
availableTicketTotal <= 0 ||
|
(deliveryRangeCheckedAddressId === address?.id && inDeliveryRange === false) ||
|
||||||
!canStartOrder
|
availableTicketTotal <= 0 ||
|
||||||
}
|
!canStartOrder
|
||||||
onClick={onSubmit}
|
}
|
||||||
>
|
onClick={onSubmit}
|
||||||
|
>
|
||||||
{deliveryRangeChecking
|
{deliveryRangeChecking
|
||||||
? '校验配送范围...'
|
? '校验配送范围...'
|
||||||
: (inDeliveryRange === false ? '不在配送范围' : (submitLoading ? '提交中...' : '立即提交'))
|
: (!address?.id
|
||||||
|
? '请选择地址'
|
||||||
|
: (!addressHasCoords
|
||||||
|
? '地址缺少定位'
|
||||||
|
: ((deliveryRangeCheckedAddressId === address?.id && inDeliveryRange === false)
|
||||||
|
? '不在配送范围'
|
||||||
|
: (submitLoading ? '提交中...' : '立即提交')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -16,5 +16,7 @@ export function saveStorageByLoginUser(token: string, user: User) {
|
|||||||
Taro.setStorageSync('access_token', token)
|
Taro.setStorageSync('access_token', token)
|
||||||
Taro.setStorageSync('UserId', user.userId)
|
Taro.setStorageSync('UserId', user.userId)
|
||||||
Taro.setStorageSync('Phone', user.phone)
|
Taro.setStorageSync('Phone', user.phone)
|
||||||
|
Taro.setStorageSync('WxNickName', user.nickname);
|
||||||
Taro.setStorageSync('User', user)
|
Taro.setStorageSync('User', user)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user