feat(credit): 新增信用订单功能模块
- 添加信用订单创建页面,支持填写拖欠方、金额、年数等信息 - 实现附件上传功能,支持图片和文档文件上传预览 - 集成城市选择组件,方便用户选择所在地区 - 添加服务协议勾选确认机制 - 在app配置中注册信用订单相关路由 - 重构文件上传API,新增uploadFileByPath方法支持路径上传 - 更新发现页面,集成店铺网点查询和定位功能 - 实现下拉刷新和无限滚动加载更多网点数据 - 添加地图导航和电话拨打功能 - 优化网点列表显示,按距离排序并显示距离信息
This commit is contained in:
@@ -56,18 +56,17 @@ const computeSignature = (accessKeySecret: string, canonicalString: string): str
|
||||
/**
|
||||
* 上传阿里云OSS
|
||||
*/
|
||||
export async function uploadFile() {
|
||||
return new Promise(async (resolve: (result: FileRecord) => void, reject) => {
|
||||
Taro.chooseImage({
|
||||
count: 1,
|
||||
sizeType: ['compressed'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: async (res) => {
|
||||
const tempFilePath = res.tempFilePaths[0];
|
||||
// 上传图片到OSS
|
||||
export async function uploadFileByPath(filePath: string) {
|
||||
return new Promise((resolve: (result: FileRecord) => void, reject) => {
|
||||
if (!filePath) {
|
||||
reject(new Error('缺少 filePath'))
|
||||
return
|
||||
}
|
||||
|
||||
// 统一走同一个上传接口:既支持图片,也支持文档等文件(由后端决定白名单/大小限制)
|
||||
Taro.uploadFile({
|
||||
url: 'https://server.websoft.top/api/oss/upload',
|
||||
filePath: tempFilePath,
|
||||
filePath,
|
||||
name: 'file',
|
||||
header: {
|
||||
'content-type': 'application/json',
|
||||
@@ -81,7 +80,7 @@ export async function uploadFile() {
|
||||
} else {
|
||||
reject(new Error(data.message || '上传失败'))
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
reject(new Error('解析响应数据失败'))
|
||||
}
|
||||
},
|
||||
@@ -90,6 +89,23 @@ export async function uploadFile() {
|
||||
reject(new Error('上传请求失败'))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export async function uploadFile() {
|
||||
return new Promise(async (resolve: (result: FileRecord) => void, reject) => {
|
||||
Taro.chooseImage({
|
||||
count: 1,
|
||||
sizeType: ['compressed'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: async (res) => {
|
||||
const tempFilePath = res.tempFilePaths[0];
|
||||
try {
|
||||
const record = await uploadFileByPath(tempFilePath)
|
||||
resolve(record)
|
||||
} catch (e) {
|
||||
reject(e)
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.log('选择图片失败', err);
|
||||
|
||||
@@ -116,6 +116,13 @@ export default {
|
||||
"index",
|
||||
"article/index",
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "credit",
|
||||
"pages": [
|
||||
"order/index",
|
||||
"order/add"
|
||||
]
|
||||
}
|
||||
],
|
||||
window: {
|
||||
|
||||
5
src/credit/order/add.config.ts
Normal file
5
src/credit/order/add.config.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '发需求',
|
||||
navigationBarTextStyle: 'black',
|
||||
navigationBarBackgroundColor: '#ffffff'
|
||||
})
|
||||
63
src/credit/order/add.scss
Normal file
63
src/credit/order/add.scss
Normal file
@@ -0,0 +1,63 @@
|
||||
.credit-order-add-page {
|
||||
padding-bottom: 100px; // leave space for FixedButton
|
||||
}
|
||||
|
||||
.attachments {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
&__empty {
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&__list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
&__item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 8px;
|
||||
background: #fafafa;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
max-width: 220px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__remove {
|
||||
color: #999;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.agreement {
|
||||
margin: 12px 16px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&__text {
|
||||
color: #666;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
&__link {
|
||||
color: #1677ff;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
342
src/credit/order/add.tsx
Normal file
342
src/credit/order/add.tsx
Normal file
@@ -0,0 +1,342 @@
|
||||
import { useMemo, useRef, useState } from 'react'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import { Address, Button, Cell, CellGroup, Form, Input, Radio, TextArea } from '@nutui/nutui-react-taro'
|
||||
import { ArrowRight, Close } from '@nutui/icons-react-taro'
|
||||
|
||||
import FixedButton from '@/components/FixedButton'
|
||||
import RegionData from '@/api/json/regions-data.json'
|
||||
import { uploadFileByPath } from '@/api/system/file'
|
||||
|
||||
import './add.scss'
|
||||
|
||||
type Attachment = {
|
||||
id: string
|
||||
name: string
|
||||
url: string
|
||||
isImage: boolean
|
||||
thumbnail?: string
|
||||
}
|
||||
|
||||
const isHttpUrl = (url?: string) => {
|
||||
if (!url) return false
|
||||
return /^https?:\/\//i.test(url)
|
||||
}
|
||||
|
||||
export default function CreditOrderAddPage() {
|
||||
const formRef = useRef<any>(null)
|
||||
|
||||
const [cityVisible, setCityVisible] = useState(false)
|
||||
const [cityText, setCityText] = useState('')
|
||||
|
||||
const [attachments, setAttachments] = useState<Attachment[]>([])
|
||||
const [uploading, setUploading] = useState(false)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [agree, setAgree] = useState(false)
|
||||
|
||||
const cityOptions = useMemo(() => {
|
||||
// NutUI Address options: [{ text, value, children }]
|
||||
// @ts-ignore
|
||||
return (RegionData || []).map(a => ({
|
||||
value: a.label,
|
||||
text: a.label,
|
||||
children: (a.children || []).map(b => ({
|
||||
value: b.label,
|
||||
text: b.label,
|
||||
children: (b.children || []).map(c => ({
|
||||
value: c.label,
|
||||
text: c.label
|
||||
}))
|
||||
}))
|
||||
}))
|
||||
}, [])
|
||||
|
||||
const addUploadedRecords = (incoming: Array<{ url?: string; thumbnail?: string; name?: string }>, opts: { isImage: boolean }) => {
|
||||
const next = incoming
|
||||
.map((r, idx) => {
|
||||
const url = String(r.url || r.thumbnail || '').trim()
|
||||
if (!url) return null
|
||||
const name = String(r.name || (opts.isImage ? `图片${idx + 1}` : `文件${idx + 1}`)).trim()
|
||||
const id = `${Date.now()}-${Math.random().toString(16).slice(2)}`
|
||||
return { id, name, url, thumbnail: r.thumbnail, isImage: opts.isImage } as Attachment
|
||||
})
|
||||
.filter(Boolean) as Attachment[]
|
||||
|
||||
if (!next.length) return
|
||||
setAttachments(prev => prev.concat(next))
|
||||
}
|
||||
|
||||
const chooseAndUploadImages = async () => {
|
||||
const res = await Taro.chooseImage({
|
||||
count: 9,
|
||||
sizeType: ['compressed'],
|
||||
sourceType: ['album', 'camera']
|
||||
})
|
||||
const paths = (res?.tempFilePaths || []).filter(Boolean)
|
||||
if (!paths.length) return
|
||||
|
||||
setUploading(true)
|
||||
try {
|
||||
const uploaded = []
|
||||
for (const p of paths) {
|
||||
const record = await uploadFileByPath(p)
|
||||
uploaded.push(record as any)
|
||||
}
|
||||
addUploadedRecords(uploaded, { isImage: true })
|
||||
Taro.showToast({ title: '上传成功', icon: 'success' })
|
||||
} catch (e) {
|
||||
console.error('上传图片失败:', e)
|
||||
Taro.showToast({ title: '上传失败,请重试', icon: 'none' })
|
||||
} finally {
|
||||
setUploading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const chooseAndUploadDocs = async () => {
|
||||
// 微信小程序:从会话/聊天等入口选择文件
|
||||
// H5 等环境可能不支持,走 try/catch 提示即可
|
||||
// @ts-ignore
|
||||
const res = await Taro.chooseMessageFile({
|
||||
count: 9,
|
||||
type: 'file'
|
||||
})
|
||||
// @ts-ignore
|
||||
const tempFiles = (res?.tempFiles || []) as Array<{ path?: string; name?: string }>
|
||||
const paths = tempFiles.map(f => f?.path).filter(Boolean) as string[]
|
||||
if (!paths.length) return
|
||||
|
||||
setUploading(true)
|
||||
try {
|
||||
const uploaded = []
|
||||
for (let i = 0; i < paths.length; i++) {
|
||||
const record = await uploadFileByPath(paths[i])
|
||||
const name = tempFiles[i]?.name
|
||||
uploaded.push({ ...(record as any), name: name || (record as any)?.name })
|
||||
}
|
||||
addUploadedRecords(uploaded, { isImage: false })
|
||||
Taro.showToast({ title: '上传成功', icon: 'success' })
|
||||
} catch (e) {
|
||||
console.error('上传文件失败:', e)
|
||||
Taro.showToast({ title: '上传失败,请重试', icon: 'none' })
|
||||
} finally {
|
||||
setUploading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const chooseAttachment = async () => {
|
||||
try {
|
||||
const res = await Taro.showActionSheet({
|
||||
itemList: ['上传图片', '上传文件']
|
||||
})
|
||||
if (res.tapIndex === 0) await chooseAndUploadImages()
|
||||
if (res.tapIndex === 1) await chooseAndUploadDocs()
|
||||
} catch (e) {
|
||||
// 用户取消
|
||||
const msg = String((e as any)?.errMsg || (e as any)?.message || e || '')
|
||||
if (msg.includes('cancel')) return
|
||||
console.error('选择上传类型失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
const removeAttachment = (id: string) => {
|
||||
setAttachments(prev => prev.filter(a => a.id !== id))
|
||||
}
|
||||
|
||||
const previewAttachment = async (a: Attachment) => {
|
||||
try {
|
||||
if (a.isImage) {
|
||||
const imgs = attachments.filter(x => x.isImage).map(x => x.url)
|
||||
await Taro.previewImage({ urls: imgs, current: a.url })
|
||||
return
|
||||
}
|
||||
|
||||
let filePath = a.url
|
||||
if (isHttpUrl(a.url)) {
|
||||
const dl = await Taro.downloadFile({ url: a.url })
|
||||
// @ts-ignore
|
||||
filePath = dl?.tempFilePath
|
||||
}
|
||||
|
||||
await Taro.openDocument({ filePath, showMenu: true })
|
||||
} catch (e) {
|
||||
console.error('预览文件失败:', e)
|
||||
Taro.showToast({ title: '无法打开该文件', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
const submitSucceed = async (values: any) => {
|
||||
const payer = String(values?.payer || '').trim()
|
||||
const amount = Number(values?.amount)
|
||||
const years = Number(values?.years)
|
||||
const remark = String(values?.remark || '').trim()
|
||||
|
||||
if (!payer) {
|
||||
Taro.showToast({ title: '请输入拖欠方(付款方)', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!Number.isFinite(amount) || amount <= 0) {
|
||||
Taro.showToast({ title: '请输入正确的拖欠金额', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!Number.isFinite(years) || years <= 0) {
|
||||
Taro.showToast({ title: '请输入正确的拖欠年数', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!agree) {
|
||||
Taro.showToast({ title: '请先同意服务协议', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
const payload = {
|
||||
payer,
|
||||
amount,
|
||||
years,
|
||||
remark,
|
||||
city: cityText,
|
||||
files: attachments.map(a => ({ name: a.name, url: a.url, isImage: a.isImage }))
|
||||
}
|
||||
|
||||
setSubmitting(true)
|
||||
try {
|
||||
// TODO: 这里可替换为真实接口提交(如后端提供 `/credit/order`)
|
||||
console.log('发需求提交:', payload)
|
||||
Taro.showToast({ title: '提交成功', icon: 'success' })
|
||||
Taro.eventCenter.trigger('credit:order:created', payload)
|
||||
setTimeout(() => {
|
||||
Taro.navigateBack()
|
||||
}, 800)
|
||||
} catch (e) {
|
||||
console.error('提交失败:', e)
|
||||
Taro.showToast({ title: '提交失败,请重试', icon: 'none' })
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const submitFailed = (err: any) => {
|
||||
console.log('表单校验失败:', err)
|
||||
}
|
||||
|
||||
const canSubmit = !uploading && !submitting
|
||||
|
||||
return (
|
||||
<View className="credit-order-add-page">
|
||||
<Form
|
||||
ref={formRef}
|
||||
divider
|
||||
labelPosition="left"
|
||||
onFinish={submitSucceed}
|
||||
onFinishFailed={submitFailed}
|
||||
>
|
||||
<CellGroup>
|
||||
<Form.Item
|
||||
name="payer"
|
||||
label="拖欠方"
|
||||
required
|
||||
rules={[{ required: true, message: '请输入拖欠方(付款方)' }]}
|
||||
>
|
||||
<Input placeholder="请输入拖欠方/付款方名称" maxLength={50} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="amount"
|
||||
label="拖欠金额"
|
||||
required
|
||||
rules={[{ required: true, message: '请输入拖欠金额' }]}
|
||||
>
|
||||
<Input placeholder="请输入拖欠金额" type="number" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="years"
|
||||
label="拖欠几年"
|
||||
required
|
||||
rules={[{ required: true, message: '请输入拖欠年数' }]}
|
||||
>
|
||||
<Input placeholder="请输入拖欠年数" type="number" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="remark" label="备注">
|
||||
<TextArea placeholder="补充说明(可选)" maxLength={200} showCount rows={3} />
|
||||
</Form.Item>
|
||||
</CellGroup>
|
||||
</Form>
|
||||
|
||||
<CellGroup>
|
||||
<Cell
|
||||
title="所在城市"
|
||||
description={cityText || '请选择(可选)'}
|
||||
extra={<ArrowRight color="#cccccc" />}
|
||||
onClick={() => setCityVisible(true)}
|
||||
/>
|
||||
</CellGroup>
|
||||
|
||||
<CellGroup title="上传文件(支持图片和文档)">
|
||||
<Cell>
|
||||
<View className="attachments">
|
||||
{attachments.length ? (
|
||||
<View className="attachments__list">
|
||||
{attachments.map(a => (
|
||||
<View key={a.id} className="attachments__item" onClick={() => previewAttachment(a)}>
|
||||
<Text className="attachments__name">{a.name}</Text>
|
||||
<Text
|
||||
className="attachments__remove"
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
removeAttachment(a.id)
|
||||
}}
|
||||
>
|
||||
<Close size={12} />
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
) : (
|
||||
<Text className="attachments__empty"></Text>
|
||||
)}
|
||||
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
fill="outline"
|
||||
loading={uploading}
|
||||
disabled={uploading || submitting}
|
||||
onClick={chooseAttachment}
|
||||
>
|
||||
{uploading ? '上传中...' : '添加文件'}
|
||||
</Button>
|
||||
</View>
|
||||
</Cell>
|
||||
</CellGroup>
|
||||
|
||||
<View className="agreement py-2 px-3">
|
||||
<Radio checked={agree} onClick={() => setAgree(v => !v)} />
|
||||
<Text className="agreement__text" onClick={() => setAgree(v => !v)}>
|
||||
我已阅读并同意
|
||||
</Text>
|
||||
<Text className="agreement__link" onClick={() => Taro.navigateTo({ url: '/passport/agreement' })}>
|
||||
《服务协议》
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Address
|
||||
visible={cityVisible}
|
||||
options={cityOptions as any}
|
||||
title="选择城市"
|
||||
onChange={(value: any[]) => {
|
||||
const txt = value.filter(Boolean).slice(0, 2).join(' ')
|
||||
setCityText(txt)
|
||||
setCityVisible(false)
|
||||
}}
|
||||
onClose={() => setCityVisible(false)}
|
||||
/>
|
||||
|
||||
<FixedButton
|
||||
text={submitting ? '提交中...' : '提交需求'}
|
||||
disabled={!canSubmit}
|
||||
onClick={() => formRef.current?.submit()}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
5
src/credit/order/index.config.ts
Normal file
5
src/credit/order/index.config.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '需求列表',
|
||||
navigationBarTextStyle: 'black',
|
||||
navigationBarBackgroundColor: '#ffffff'
|
||||
})
|
||||
636
src/credit/order/index.tsx
Normal file
636
src/credit/order/index.tsx
Normal file
@@ -0,0 +1,636 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import Taro, { useDidShow } from '@tarojs/taro'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import {
|
||||
Tabs,
|
||||
TabPane,
|
||||
Cell,
|
||||
Space,
|
||||
Button,
|
||||
Dialog,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
Image,
|
||||
Empty,
|
||||
InfiniteLoading,
|
||||
PullToRefresh,
|
||||
Loading
|
||||
} from '@nutui/nutui-react-taro'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
import { pageGltTicketOrder, updateGltTicketOrder } from '@/api/glt/gltTicketOrder'
|
||||
import type { GltTicketOrder, GltTicketOrderParam } from '@/api/glt/gltTicketOrder/model'
|
||||
import { uploadFile } from '@/api/system/file'
|
||||
import { listShopStoreRider, updateShopStoreRider } from '@/api/shop/shopStoreRider'
|
||||
import { getCurrentLngLat } from '@/utils/location'
|
||||
|
||||
const PAGE_SIZE = 10
|
||||
|
||||
type DeliverConfirmMode = 'photoComplete' | 'waitCustomerConfirm'
|
||||
|
||||
export default function TicketOrdersPage() {
|
||||
const riderId = useMemo(() => {
|
||||
const raw = Taro.getStorageSync('UserId')
|
||||
const id = Number(raw)
|
||||
return Number.isFinite(id) && id > 0 ? id : undefined
|
||||
}, [])
|
||||
|
||||
const pageRef = useRef(1)
|
||||
const listRef = useRef<GltTicketOrder[]>([])
|
||||
|
||||
const [tabIndex, setTabIndex] = useState(0)
|
||||
const [list, setList] = useState<GltTicketOrder[]>([])
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const [deliverDialogVisible, setDeliverDialogVisible] = useState(false)
|
||||
const [deliverSubmitting, setDeliverSubmitting] = useState(false)
|
||||
const [deliverOrder, setDeliverOrder] = useState<GltTicketOrder | null>(null)
|
||||
const [deliverImg, setDeliverImg] = useState<string | undefined>(undefined)
|
||||
const [deliverConfirmMode, setDeliverConfirmMode] = useState<DeliverConfirmMode>('photoComplete')
|
||||
|
||||
const riderTabs = useMemo(
|
||||
() => [
|
||||
{ index: 0, title: '全部' },
|
||||
{ index: 1, title: '待配送', deliveryStatus: 10 },
|
||||
{ index: 2, title: '配送中', deliveryStatus: 20 },
|
||||
{ index: 3, title: '待确认', deliveryStatus: 30 },
|
||||
{ index: 4, title: '已完成', deliveryStatus: 40 }
|
||||
],
|
||||
[]
|
||||
)
|
||||
|
||||
const getOrderStatusText = (order: GltTicketOrder) => {
|
||||
if (order.status === 1) return '已冻结'
|
||||
|
||||
const deliveryStatus = order.deliveryStatus
|
||||
if (deliveryStatus === 40) return '已完成'
|
||||
if (deliveryStatus === 30) return '待客户确认'
|
||||
if (deliveryStatus === 20) return '配送中'
|
||||
if (deliveryStatus === 10) return '待配送'
|
||||
|
||||
// 兼容:如果后端暂未下发 deliveryStatus,就用时间字段推断
|
||||
if (order.receiveConfirmTime) return '已完成'
|
||||
if (order.sendEndTime) return '待客户确认'
|
||||
if (order.sendStartTime) return '配送中'
|
||||
if (order.riderId) return '待配送'
|
||||
return '待派单'
|
||||
}
|
||||
|
||||
const getOrderStatusColor = (order: GltTicketOrder) => {
|
||||
const text = getOrderStatusText(order)
|
||||
if (text === '已完成') return 'text-green-600'
|
||||
if (text === '待客户确认') return 'text-purple-600'
|
||||
if (text === '配送中') return 'text-blue-600'
|
||||
if (text === '待配送') return 'text-amber-600'
|
||||
if (text === '已冻结') return 'text-orange-600'
|
||||
return 'text-gray-500'
|
||||
}
|
||||
|
||||
const canStartDeliver = (order: GltTicketOrder) => {
|
||||
if (!order.id) return false
|
||||
if (order.status === 1) return false
|
||||
if (!riderId || order.riderId !== riderId) return false
|
||||
if (order.deliveryStatus && order.deliveryStatus !== 10) return false
|
||||
return !order.sendStartTime && !order.sendEndTime
|
||||
}
|
||||
|
||||
const canConfirmDelivered = (order: GltTicketOrder) => {
|
||||
if (!order.id) return false
|
||||
if (order.status === 1) return false
|
||||
if (!riderId || order.riderId !== riderId) return false
|
||||
if (order.receiveConfirmTime) return false
|
||||
if (order.deliveryStatus === 40) return false
|
||||
if (order.sendEndTime) return false
|
||||
|
||||
// 只允许在“配送中”阶段确认送达
|
||||
if (typeof order.deliveryStatus === 'number') return order.deliveryStatus === 20
|
||||
return !!order.sendStartTime
|
||||
}
|
||||
|
||||
const canCompleteByPhoto = (order: GltTicketOrder) => {
|
||||
if (!order.id) return false
|
||||
if (order.status === 1) return false
|
||||
if (!riderId || order.riderId !== riderId) return false
|
||||
if (order.receiveConfirmTime) return false
|
||||
if (order.deliveryStatus === 40) return false
|
||||
// 已送达但未完成:允许补传照片并直接完成
|
||||
return !!order.sendEndTime
|
||||
}
|
||||
|
||||
const filterByTab = useCallback(
|
||||
(orders: GltTicketOrder[]) => {
|
||||
if (tabIndex === 0) return orders
|
||||
|
||||
const current = riderTabs.find(t => t.index === tabIndex)
|
||||
const status = current?.deliveryStatus
|
||||
if (!status) return orders
|
||||
|
||||
// 如果后端已实现 deliveryStatus 筛选,这里基本不会再过滤;否则用兼容逻辑兜底。
|
||||
return orders.filter(o => {
|
||||
const ds = o.deliveryStatus
|
||||
if (typeof ds === 'number') return ds === status
|
||||
if (status === 10) return !!o.riderId && !o.sendStartTime && !o.sendEndTime
|
||||
if (status === 20) return !!o.sendStartTime && !o.sendEndTime
|
||||
if (status === 30) return !!o.sendEndTime && !o.receiveConfirmTime
|
||||
if (status === 40) return !!o.receiveConfirmTime
|
||||
return true
|
||||
})
|
||||
},
|
||||
[riderTabs, tabIndex]
|
||||
)
|
||||
|
||||
const reload = useCallback(
|
||||
async (resetPage = false) => {
|
||||
if (!riderId) return
|
||||
if (loading) return
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
const currentPage = resetPage ? 1 : pageRef.current
|
||||
const currentTab = riderTabs.find(t => t.index === tabIndex)
|
||||
|
||||
const params: GltTicketOrderParam = {
|
||||
page: currentPage,
|
||||
limit: PAGE_SIZE,
|
||||
riderId,
|
||||
deliveryStatus: currentTab?.deliveryStatus
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await pageGltTicketOrder(params as any)
|
||||
const incomingAll = (res?.list || []) as GltTicketOrder[]
|
||||
// 兼容:后端若暂未实现 riderId 过滤,前端兜底过滤掉非本人的订单
|
||||
const incoming = incomingAll.filter(o => o?.deleted !== 1 && o?.riderId === riderId)
|
||||
|
||||
const prev = resetPage ? [] : listRef.current
|
||||
const next = resetPage ? incoming : prev.concat(incoming)
|
||||
listRef.current = next
|
||||
setList(next)
|
||||
|
||||
const total = typeof res?.count === 'number' ? res.count : undefined
|
||||
const filteredOut = incomingAll.length - incoming.length
|
||||
if (typeof total === 'number' && filteredOut === 0) {
|
||||
setHasMore(next.length < total)
|
||||
} else {
|
||||
setHasMore(incomingAll.length >= PAGE_SIZE)
|
||||
}
|
||||
|
||||
pageRef.current = currentPage + 1
|
||||
} catch (e) {
|
||||
console.error('加载配送订单失败:', e)
|
||||
setError('加载失败,请重试')
|
||||
setHasMore(false)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
},
|
||||
[loading, riderId, riderTabs, tabIndex]
|
||||
)
|
||||
|
||||
const reloadMore = useCallback(async () => {
|
||||
if (loading || !hasMore) return
|
||||
await reload(false)
|
||||
}, [hasMore, loading, reload])
|
||||
|
||||
const openDeliverDialog = (order: GltTicketOrder, opts?: { mode?: DeliverConfirmMode }) => {
|
||||
setDeliverOrder(order)
|
||||
setDeliverImg(order.sendEndImg)
|
||||
setDeliverConfirmMode(opts?.mode || (order.sendEndImg ? 'photoComplete' : 'waitCustomerConfirm'))
|
||||
setDeliverDialogVisible(true)
|
||||
}
|
||||
|
||||
const handleChooseDeliverImg = async () => {
|
||||
try {
|
||||
const file = await uploadFile()
|
||||
setDeliverImg(file?.url)
|
||||
} catch (e) {
|
||||
console.error('上传送达照片失败:', e)
|
||||
Taro.showToast({ title: '上传失败,请重试', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
const handleStartDeliver = async (order: GltTicketOrder) => {
|
||||
if (!order?.id) return
|
||||
if (!canStartDeliver(order)) return
|
||||
try {
|
||||
await updateGltTicketOrder({
|
||||
id: order.id,
|
||||
deliveryStatus: 20,
|
||||
sendStartTime: dayjs().format('YYYY-MM-DD HH:mm:ss')
|
||||
})
|
||||
Taro.showToast({ title: '已开始配送', icon: 'success' })
|
||||
pageRef.current = 1
|
||||
await reload(true)
|
||||
} catch (e) {
|
||||
console.error('开始配送失败:', e)
|
||||
Taro.showToast({ title: '开始配送失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
const handleConfirmDelivered = async () => {
|
||||
if (!deliverOrder?.id) return
|
||||
if (deliverSubmitting) return
|
||||
if (deliverConfirmMode === 'photoComplete' && !deliverImg) {
|
||||
Taro.showToast({ title: '请先拍照/上传送达照片', icon: 'none' })
|
||||
return
|
||||
}
|
||||
setDeliverSubmitting(true)
|
||||
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 deliveredAt = deliverOrder.sendEndTime || now
|
||||
// 说明:
|
||||
// - waitCustomerConfirm:只标记“已送达”,进入待客户确认(客户点击确认收货后完成)
|
||||
// - photoComplete:拍照留档后可直接完成(由后端策略决定是否允许)
|
||||
const payload: GltTicketOrder =
|
||||
deliverConfirmMode === 'photoComplete'
|
||||
? {
|
||||
id: deliverOrder.id,
|
||||
deliveryStatus: 40,
|
||||
sendEndTime: deliveredAt,
|
||||
sendEndImg: deliverImg,
|
||||
receiveConfirmTime: now,
|
||||
receiveConfirmType: 20
|
||||
}
|
||||
: {
|
||||
id: deliverOrder.id,
|
||||
deliveryStatus: 30,
|
||||
sendEndTime: deliveredAt,
|
||||
sendEndImg: deliverImg
|
||||
}
|
||||
|
||||
await updateGltTicketOrder(payload)
|
||||
Taro.showToast({ title: '已确认送达', icon: 'success' })
|
||||
setDeliverDialogVisible(false)
|
||||
setDeliverOrder(null)
|
||||
setDeliverImg(undefined)
|
||||
setDeliverConfirmMode('photoComplete')
|
||||
pageRef.current = 1
|
||||
await reload(true)
|
||||
} catch (e) {
|
||||
console.error('确认送达失败:', e)
|
||||
Taro.showToast({ title: '确认送达失败', icon: 'none' })
|
||||
} finally {
|
||||
setDeliverSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
listRef.current = list
|
||||
}, [list])
|
||||
|
||||
useDidShow(() => {
|
||||
pageRef.current = 1
|
||||
listRef.current = []
|
||||
setList([])
|
||||
setHasMore(true)
|
||||
void reload(true)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
pageRef.current = 1
|
||||
listRef.current = []
|
||||
setList([])
|
||||
setHasMore(true)
|
||||
void reload(true)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [tabIndex, riderId])
|
||||
|
||||
if (!riderId) {
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen p-4">
|
||||
<Text>请先登录</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const displayList = filterByTab(list)
|
||||
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen">
|
||||
<Tabs value={tabIndex} onChange={paneKey => setTabIndex(Number(paneKey))} align="left">
|
||||
{riderTabs.map(t => (
|
||||
<TabPane key={t.index} title={loading && tabIndex === t.index ? `${t.title}...` : t.title} />
|
||||
))}
|
||||
</Tabs>
|
||||
|
||||
<View className="px-3 pb-4">
|
||||
<PullToRefresh
|
||||
onRefresh={async () => {
|
||||
pageRef.current = 1
|
||||
listRef.current = []
|
||||
setList([])
|
||||
setHasMore(true)
|
||||
await reload(true)
|
||||
}}
|
||||
>
|
||||
{error ? (
|
||||
<View className="bg-white rounded-lg p-6">
|
||||
<View className="flex flex-col items-center justify-center">
|
||||
<Text className="text-gray-500 mb-3">{error}</Text>
|
||||
<Button size="small" type="primary" onClick={() => reload(true)}>
|
||||
重新加载
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<InfiniteLoading
|
||||
hasMore={hasMore}
|
||||
onLoadMore={reloadMore}
|
||||
loadingText={
|
||||
<View className="flex justify-center items-center py-4">
|
||||
<Loading />
|
||||
<View className="ml-2">加载中...</View>
|
||||
</View>
|
||||
}
|
||||
loadMoreText={
|
||||
displayList.length === 0 ? (
|
||||
<View className="bg-white rounded-lg p-6">
|
||||
<Empty description="暂无配送订单" />
|
||||
</View>
|
||||
) : (
|
||||
<View className="text-center py-4 text-gray-500">没有更多了</View>
|
||||
)
|
||||
}
|
||||
>
|
||||
{displayList.map(o => {
|
||||
const qty = Number(o.totalNum || 0)
|
||||
const timeText = o.createTime ? dayjs(o.createTime).format('YYYY-MM-DD HH:mm') : '-'
|
||||
const addr = o.address || (o.addressId ? `地址ID:${o.addressId}` : '-')
|
||||
const remark = o.buyerRemarks || o.comments || ''
|
||||
const ticketNo = o.userTicketId || '-'
|
||||
|
||||
const flow1Done = !!o.riderId
|
||||
const flow2Done =
|
||||
!!o.sendStartTime || (typeof o.deliveryStatus === 'number' && o.deliveryStatus >= 20)
|
||||
const flow3Done =
|
||||
!!o.sendEndTime || (typeof o.deliveryStatus === 'number' && o.deliveryStatus >= 30)
|
||||
const flow4Done = !!o.receiveConfirmTime || o.deliveryStatus === 40
|
||||
|
||||
const phoneToCall = o.phone
|
||||
const storePhone = o.storePhone
|
||||
const pickupName = o.warehouseName || o.storeName
|
||||
const pickupAddr = o.warehouseAddress || o.storeAddress
|
||||
|
||||
return (
|
||||
<Cell key={String(o.id)} style={{ padding: '16px' }}>
|
||||
<View className="w-full">
|
||||
<View className="flex justify-between items-center">
|
||||
<Text className="text-gray-800 font-bold text-sm">{`订单#${o.id}`}</Text>
|
||||
<Text className={`${getOrderStatusColor(o)} text-sm font-medium`}>{getOrderStatusText(o)}</Text>
|
||||
</View>
|
||||
|
||||
<View className="text-gray-400 text-xs mt-1">下单时间:{timeText}</View>
|
||||
<View className="text-gray-400 text-xs mt-1">票号:{ticketNo}</View>
|
||||
|
||||
<View className="mt-3 bg-white rounded-lg">
|
||||
<View className="text-sm text-gray-700">
|
||||
<Text className="text-gray-500">收货地址:</Text>
|
||||
<Text>{addr}</Text>
|
||||
</View>
|
||||
<View className="text-sm text-gray-700 mt-1">
|
||||
<Text className="text-gray-500">客户:</Text>
|
||||
<Text>
|
||||
{o.nickname || '-'} {o.phone ? `(${o.phone})` : ''}
|
||||
</Text>
|
||||
</View>
|
||||
<View className="text-sm text-gray-700 mt-1">
|
||||
<Text className="text-gray-500">取货点:</Text>
|
||||
<Text>{pickupName || '-'}</Text>
|
||||
</View>
|
||||
{pickupAddr ? (
|
||||
<View className="text-sm text-gray-700 mt-1">
|
||||
<Text className="text-gray-500">取货地址:</Text>
|
||||
<Text>{pickupAddr}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
<View className="text-sm text-gray-700 mt-1">
|
||||
<Text className="text-gray-500">预约配送:</Text>
|
||||
<Text>{o.sendTime ? dayjs(o.sendTime).format('YYYY-MM-DD HH:mm') : '-'}</Text>
|
||||
</View>
|
||||
<View className="text-sm text-gray-700 mt-1">
|
||||
<Text className="text-gray-500">数量:</Text>
|
||||
<Text>{qty || '-'}</Text>
|
||||
<Text className="text-gray-500 ml-3">门店:</Text>
|
||||
<Text>{o.storeName || '-'}</Text>
|
||||
</View>
|
||||
{o.storePhone ? (
|
||||
<View className="text-sm text-gray-700 mt-1">
|
||||
<Text className="text-gray-500">门店电话:</Text>
|
||||
<Text>{o.storePhone}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
{remark ? (
|
||||
<View className="text-sm text-gray-700 mt-1">
|
||||
<Text className="text-gray-500">备注:</Text>
|
||||
<Text>{remark}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
{o.sendStartTime ? (
|
||||
<View className="text-sm text-gray-700 mt-1">
|
||||
<Text className="text-gray-500">开始配送:</Text>
|
||||
<Text>{dayjs(o.sendStartTime).format('YYYY-MM-DD HH:mm')}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
{o.sendEndTime ? (
|
||||
<View className="text-sm text-gray-700 mt-1">
|
||||
<Text className="text-gray-500">送达时间:</Text>
|
||||
<Text>{dayjs(o.sendEndTime).format('YYYY-MM-DD HH:mm')}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
{o.receiveConfirmTime ? (
|
||||
<View className="text-sm text-gray-700 mt-1">
|
||||
<Text className="text-gray-500">确认收货:</Text>
|
||||
<Text>{dayjs(o.receiveConfirmTime).format('YYYY-MM-DD HH:mm')}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
{o.sendEndImg ? (
|
||||
<View className="text-sm text-gray-700 mt-2">
|
||||
<Text className="text-gray-500">送达照片:</Text>
|
||||
<View className="mt-2">
|
||||
<Image src={o.sendEndImg} width="100%" height="120" />
|
||||
</View>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
{/* 配送流程 */}
|
||||
<View className="mt-3 bg-gray-50 rounded-lg p-2 text-xs">
|
||||
<Text className="text-gray-600">流程:</Text>
|
||||
<Text className={flow1Done ? 'text-green-600 font-medium' : 'text-gray-400'}>1 派单</Text>
|
||||
<Text className="mx-1 text-gray-400">{'>'}</Text>
|
||||
<Text className={flow2Done ? 'text-blue-600 font-medium' : 'text-gray-400'}>2 配送中</Text>
|
||||
<Text className="mx-1 text-gray-400">{'>'}</Text>
|
||||
<Text className={flow3Done ? 'text-purple-600 font-medium' : 'text-gray-400'}>3 送达留档</Text>
|
||||
<Text className="mx-1 text-gray-400">{'>'}</Text>
|
||||
<Text className={flow4Done ? 'text-green-600 font-medium' : 'text-gray-400'}>4 完成</Text>
|
||||
</View>
|
||||
|
||||
<View className="mt-3 flex justify-end">
|
||||
<Space>
|
||||
{!!phoneToCall && (
|
||||
<Button
|
||||
size="small"
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
Taro.makePhoneCall({ phoneNumber: phoneToCall })
|
||||
}}
|
||||
>
|
||||
联系客户
|
||||
</Button>
|
||||
)}
|
||||
{!!addr && addr !== '-' && (
|
||||
<Button
|
||||
size="small"
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
void Taro.setClipboardData({ data: addr })
|
||||
Taro.showToast({ title: '地址已复制', icon: 'none' })
|
||||
}}
|
||||
>
|
||||
复制地址
|
||||
</Button>
|
||||
)}
|
||||
{!!storePhone && (
|
||||
<Button
|
||||
size="small"
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
Taro.makePhoneCall({ phoneNumber: storePhone })
|
||||
}}
|
||||
>
|
||||
联系门店
|
||||
</Button>
|
||||
)}
|
||||
{canStartDeliver(o) && (
|
||||
<Button
|
||||
size="small"
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
void handleStartDeliver(o)
|
||||
}}
|
||||
>
|
||||
开始配送
|
||||
</Button>
|
||||
)}
|
||||
{canConfirmDelivered(o) && (
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
openDeliverDialog(o, { mode: 'waitCustomerConfirm' })
|
||||
}}
|
||||
>
|
||||
确认送达
|
||||
</Button>
|
||||
)}
|
||||
{canCompleteByPhoto(o) && (
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
openDeliverDialog(o, { mode: 'photoComplete' })
|
||||
}}
|
||||
>
|
||||
补传照片完成
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
</View>
|
||||
</View>
|
||||
</Cell>
|
||||
)
|
||||
})}
|
||||
</InfiniteLoading>
|
||||
)}
|
||||
</PullToRefresh>
|
||||
</View>
|
||||
|
||||
<Dialog
|
||||
title="确认送达"
|
||||
visible={deliverDialogVisible}
|
||||
confirmText={
|
||||
deliverSubmitting
|
||||
? '提交中...'
|
||||
: deliverConfirmMode === 'photoComplete'
|
||||
? '拍照完成'
|
||||
: '确认送达'
|
||||
}
|
||||
cancelText="取消"
|
||||
onConfirm={handleConfirmDelivered}
|
||||
onCancel={() => {
|
||||
if (deliverSubmitting) return
|
||||
setDeliverDialogVisible(false)
|
||||
setDeliverOrder(null)
|
||||
setDeliverImg(undefined)
|
||||
setDeliverConfirmMode('photoComplete')
|
||||
}}
|
||||
>
|
||||
<View className="text-sm text-gray-700">
|
||||
<View>到达收货点后,可选择“拍照留档直接完成”或“等待客户确认收货”。</View>
|
||||
|
||||
<View className="mt-3">
|
||||
<RadioGroup value={deliverConfirmMode} onChange={v => setDeliverConfirmMode(v as DeliverConfirmMode)}>
|
||||
<Radio value="photoComplete">拍照留档(直接完成)</Radio>
|
||||
<Radio value="waitCustomerConfirm">客户确认收货(可不拍照)</Radio>
|
||||
</RadioGroup>
|
||||
</View>
|
||||
<View className="mt-3">
|
||||
<Button size="small" onClick={handleChooseDeliverImg}>
|
||||
{deliverImg ? '重新拍照/上传' : '拍照/上传'}
|
||||
</Button>
|
||||
</View>
|
||||
{deliverImg && (
|
||||
<View className="mt-3">
|
||||
<Image src={deliverImg} width="100%" height="120" />
|
||||
<View className="mt-2 flex justify-end">
|
||||
<Button size="small" onClick={() => setDeliverImg(undefined)}>
|
||||
移除照片
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
<View className="mt-3 text-xs text-gray-500">
|
||||
说明:如选择“客户确认收货”,订单进入“待客户确认”;客户在用户端确认收货或超时自动确认(需后端支持)。
|
||||
</View>
|
||||
</View>
|
||||
</Dialog>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -1,68 +1,164 @@
|
||||
import {useMemo, useState} from 'react'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useMemo, useRef, useState } from 'react'
|
||||
import Taro, { useDidShow } from '@tarojs/taro'
|
||||
import {Input, Text, View} from '@tarojs/components'
|
||||
import { Empty, InfiniteLoading, Loading, PullToRefresh } from '@nutui/nutui-react-taro'
|
||||
import {Search} from '@nutui/icons-react-taro'
|
||||
import { pageShopStore } from '@/api/shop/shopStore'
|
||||
import type { ShopStore } from '@/api/shop/shopStore/model'
|
||||
import { getCurrentLngLat } from '@/utils/location'
|
||||
import './find.scss'
|
||||
|
||||
type SiteItem = {
|
||||
id: string
|
||||
cityName: string
|
||||
address: string
|
||||
phone: string
|
||||
contact: string
|
||||
distanceMeter: number
|
||||
const PAGE_SIZE = 10
|
||||
|
||||
type LngLat = { lng: number; lat: number }
|
||||
type ShopStoreView = ShopStore & { __distanceMeter?: number }
|
||||
|
||||
const parseLngLat = (raw: string | undefined): LngLat | null => {
|
||||
const text = (raw || '').trim()
|
||||
if (!text) return null
|
||||
const parts = text.split(/[,\s]+/).filter(Boolean)
|
||||
if (parts.length < 2) return null
|
||||
const a = Number(parts[0])
|
||||
const b = Number(parts[1])
|
||||
if (!Number.isFinite(a) || !Number.isFinite(b)) return null
|
||||
|
||||
// Accept both "lng,lat" and "lat,lng".
|
||||
const looksLikeLngLat = Math.abs(a) <= 180 && Math.abs(b) <= 90
|
||||
const looksLikeLatLng = Math.abs(a) <= 90 && Math.abs(b) <= 180
|
||||
if (looksLikeLngLat) return { lng: a, lat: b }
|
||||
if (looksLikeLatLng) return { lng: b, lat: a }
|
||||
return null
|
||||
}
|
||||
|
||||
const MOCK_SITES: SiteItem[] = [
|
||||
{
|
||||
id: '1',
|
||||
cityName: '北京朝阳区网点',
|
||||
address: '地安门西大街(南门)',
|
||||
phone: '15878179339',
|
||||
contact: '刘先生',
|
||||
distanceMeter: 100
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
cityName: '兰州某某区网点',
|
||||
address: '地安门西大街(南门)',
|
||||
phone: '15878179339',
|
||||
contact: '黄先生',
|
||||
distanceMeter: 150
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
cityName: '合肥市某某区网点',
|
||||
address: '地安门西大街(南门)',
|
||||
phone: '15878179339',
|
||||
contact: '黄先生',
|
||||
distanceMeter: 250
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
cityName: '南宁市某某区网点',
|
||||
address: '广西壮族自治区南宁市良庆区五象新区五象大道403号富雅国际金融中心G1栋高层6006',
|
||||
phone: '15878179339',
|
||||
contact: '柳先生',
|
||||
distanceMeter: 1250
|
||||
const distanceMeters = (a: LngLat, b: LngLat) => {
|
||||
const toRad = (x: number) => (x * Math.PI) / 180
|
||||
const R = 6371000
|
||||
const dLat = toRad(b.lat - a.lat)
|
||||
const dLng = toRad(b.lng - a.lng)
|
||||
const lat1 = toRad(a.lat)
|
||||
const lat2 = toRad(b.lat)
|
||||
const sin1 = Math.sin(dLat / 2)
|
||||
const sin2 = Math.sin(dLng / 2)
|
||||
const h = sin1 * sin1 + Math.cos(lat1) * Math.cos(lat2) * sin2 * sin2
|
||||
return 2 * R * Math.asin(Math.min(1, Math.sqrt(h)))
|
||||
}
|
||||
|
||||
const formatDistance = (meter: number | undefined) => {
|
||||
if (!Number.isFinite(meter as number)) return ''
|
||||
const m = Math.max(0, Math.round(meter as number))
|
||||
if (m < 1000) return `${m}米`
|
||||
const km = m / 1000
|
||||
return `${km.toFixed(km >= 10 ? 0 : 1)}公里`
|
||||
}
|
||||
]
|
||||
|
||||
const Find = () => {
|
||||
const [keyword, setKeyword] = useState<string>('')
|
||||
const [storeList, setStoreList] = useState<ShopStoreView[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
const [total, setTotal] = useState(0)
|
||||
const [userLngLat, setUserLngLat] = useState<LngLat | null>(null)
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
const key = keyword.trim()
|
||||
if (!key) return MOCK_SITES
|
||||
return MOCK_SITES.filter((it) => it.cityName.includes(key))
|
||||
}, [keyword])
|
||||
const pageRef = useRef(1)
|
||||
const latestListRef = useRef<ShopStoreView[]>([])
|
||||
const loadingRef = useRef(false)
|
||||
const coordsRef = useRef<LngLat | null>(null)
|
||||
|
||||
const onNavigate = (item: SiteItem) => {
|
||||
Taro.showToast({title: `导航至:${item.cityName}(示例)`, icon: 'none'})
|
||||
const viewList = useMemo<ShopStoreView[]>(() => {
|
||||
const me = userLngLat
|
||||
if (!me) return storeList
|
||||
|
||||
// Keep backend order; only attach distance for display.
|
||||
return storeList.map((s) => {
|
||||
const coords = parseLngLat(s.lngAndLat || s.location)
|
||||
if (!coords) return s
|
||||
return { ...s, __distanceMeter: distanceMeters(me, coords) }
|
||||
})
|
||||
}, [storeList, userLngLat])
|
||||
|
||||
const loadStores = async (isRefresh = true, keywordsOverride?: string) => {
|
||||
if (loadingRef.current) return
|
||||
loadingRef.current = true
|
||||
setLoading(true)
|
||||
|
||||
if (isRefresh) {
|
||||
pageRef.current = 1
|
||||
latestListRef.current = []
|
||||
setStoreList([])
|
||||
setHasMore(true)
|
||||
setTotal(0)
|
||||
}
|
||||
|
||||
try {
|
||||
if (!coordsRef.current) {
|
||||
const me = await getCurrentLngLat('为您展示附近网点,需要获取定位信息。')
|
||||
const lng = me ? Number(me.lng) : NaN
|
||||
const lat = me ? Number(me.lat) : NaN
|
||||
coordsRef.current = Number.isFinite(lng) && Number.isFinite(lat) ? { lng, lat } : null
|
||||
setUserLngLat(coordsRef.current)
|
||||
}
|
||||
|
||||
const currentPage = pageRef.current
|
||||
const kw = (keywordsOverride ?? keyword).trim()
|
||||
const res = await pageShopStore({
|
||||
page: currentPage,
|
||||
limit: PAGE_SIZE,
|
||||
keywords: kw || undefined
|
||||
})
|
||||
|
||||
const resList = res?.list || []
|
||||
const nextList = isRefresh ? resList : [...latestListRef.current, ...resList]
|
||||
latestListRef.current = nextList
|
||||
setStoreList(nextList)
|
||||
|
||||
const count = typeof res?.count === 'number' ? res.count : nextList.length
|
||||
setTotal(count)
|
||||
setHasMore(nextList.length < count)
|
||||
|
||||
if (resList.length > 0) {
|
||||
pageRef.current = currentPage + 1
|
||||
} else {
|
||||
setHasMore(false)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取网点列表失败:', e)
|
||||
Taro.showToast({ title: '获取网点失败', icon: 'none' })
|
||||
setHasMore(false)
|
||||
} finally {
|
||||
loadingRef.current = false
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useDidShow(() => {
|
||||
loadStores(true).then()
|
||||
})
|
||||
|
||||
const onNavigate = (item: ShopStore) => {
|
||||
const coords = parseLngLat(item.lngAndLat || item.location)
|
||||
if (!coords) {
|
||||
Taro.showToast({ title: '网点暂无坐标,无法导航', icon: 'none' })
|
||||
return
|
||||
}
|
||||
Taro.openLocation({
|
||||
latitude: coords.lat,
|
||||
longitude: coords.lng,
|
||||
name: item.name || item.city || '网点',
|
||||
address: item.address || ''
|
||||
})
|
||||
}
|
||||
|
||||
const onCall = (phone: string | undefined) => {
|
||||
const p = (phone || '').trim()
|
||||
if (!p) {
|
||||
Taro.showToast({ title: '暂无联系电话', icon: 'none' })
|
||||
return
|
||||
}
|
||||
Taro.makePhoneCall({ phoneNumber: p })
|
||||
}
|
||||
|
||||
const onSearch = () => {
|
||||
Taro.showToast({title: '查询(示例)', icon: 'none'})
|
||||
loadStores(true).then()
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -84,44 +180,84 @@ const Find = () => {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<PullToRefresh onRefresh={() => loadStores(true)} headHeight={60}>
|
||||
<View style={{ height: 'calc(100vh)', overflowY: 'auto' }} id='store-scroll'>
|
||||
{viewList.length === 0 && !loading ? (
|
||||
<View className='emptyWrap'>
|
||||
<Empty description='暂无网点' style={{ backgroundColor: 'transparent' }} />
|
||||
</View>
|
||||
) : (
|
||||
<View className='siteList'>
|
||||
{filtered.map((item) => (
|
||||
<View key={item.id} className='siteCard'>
|
||||
<InfiniteLoading
|
||||
target='store-scroll'
|
||||
hasMore={hasMore}
|
||||
onLoadMore={() => loadStores(false)}
|
||||
loadingText={
|
||||
<View className='emptyWrap'>
|
||||
<Loading />
|
||||
<Text className='emptyText' style={{ marginLeft: '8px' }}>
|
||||
加载中...
|
||||
</Text>
|
||||
</View>
|
||||
}
|
||||
loadMoreText={
|
||||
<View className='emptyWrap'>
|
||||
<Text className='emptyText'>
|
||||
{viewList.length === 0 ? '暂无网点' : '没有更多了'}
|
||||
</Text>
|
||||
</View>
|
||||
}
|
||||
>
|
||||
{viewList.map((item, idx) => {
|
||||
const name = item?.name || item?.city || item?.province || '网点'
|
||||
const contact = item?.managerName || '--'
|
||||
const distanceText = formatDistance(item?.__distanceMeter)
|
||||
return (
|
||||
<View key={String(item?.id ?? `${name}-${idx}`)} className='siteCard'>
|
||||
<View className='siteCardInner'>
|
||||
<View className='siteInfo'>
|
||||
<View className='siteRow siteRowTop'>
|
||||
<Text className='siteLabel'>城市名称:</Text>
|
||||
<Text className='siteValue siteValueStrong'>{item.cityName}</Text>
|
||||
<Text className='siteLabel'>网点名称:</Text>
|
||||
<Text className='siteValue siteValueStrong'>{name}</Text>
|
||||
</View>
|
||||
<View className='siteDivider' />
|
||||
<View className='siteRow'>
|
||||
<Text className='siteLabel'>网点地址:</Text>
|
||||
<Text className='siteValue'>{item.address}</Text>
|
||||
<Text className='siteValue'>{item?.address || '--'}</Text>
|
||||
</View>
|
||||
<View className='siteRow'>
|
||||
<Text className='siteLabel'>联系电话:</Text>
|
||||
<Text className='siteValue'>{item.phone}</Text>
|
||||
<Text className='siteValue' onClick={() => onCall(item?.phone)}>
|
||||
{item?.phone || '--'}
|
||||
</Text>
|
||||
</View>
|
||||
<View className='siteRow'>
|
||||
<Text className='siteLabel'>联系人:</Text>
|
||||
<Text className='siteValue'>{item.contact}</Text>
|
||||
<Text className='siteValue'>{contact}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className='siteSide' onClick={() => onNavigate(item)}>
|
||||
<View className='navArrow' />
|
||||
<Text className='distanceText'>距离{item.distanceMeter}米</Text>
|
||||
<Text className='distanceText'>
|
||||
{distanceText ? `距离${distanceText}` : '查看导航'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</InfiniteLoading>
|
||||
|
||||
{filtered.length === 0 && (
|
||||
<View className='emptyWrap'>
|
||||
<Text className='emptyText'>暂无网点</Text>
|
||||
{total > 0 && (
|
||||
<View className='emptyWrap' style={{ paddingTop: '10rpx' }}>
|
||||
<Text className='emptyText'>共 {total} 个网点</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</PullToRefresh>
|
||||
|
||||
<View className='bottomSafe' />
|
||||
</View>
|
||||
|
||||
@@ -4,6 +4,7 @@ import iconShop from '@/assets/tabbar/shop.png'
|
||||
import iconFind from '@/assets/tabbar/find.png'
|
||||
import iconKefu from '@/assets/tabbar/kefu.png'
|
||||
import './index.scss'
|
||||
import navTo from "@/utils/common";
|
||||
|
||||
function Home() {
|
||||
useShareTimeline(() => {
|
||||
@@ -42,10 +43,6 @@ function Home() {
|
||||
|
||||
return (
|
||||
<View className='home'>
|
||||
<View className='welcomeCard'>
|
||||
<Text className='welcomeText'>欢迎来到易赊宝小程序~</Text>
|
||||
</View>
|
||||
|
||||
<View className='bannerCard'>
|
||||
<Swiper
|
||||
className='bannerSwiper'
|
||||
@@ -115,7 +112,7 @@ function Home() {
|
||||
</View>
|
||||
|
||||
<View className='ctaWrap'>
|
||||
<View className='ctaBtn' onClick={onDemand}>
|
||||
<View className='ctaBtn' onClick={() => navTo('/credit/order/add', true)}>
|
||||
<Text className='ctaBtnText'>发需求</Text>
|
||||
</View>
|
||||
<Text className='ctaHint'>提出您的述求,免费获取解决方案</Text>
|
||||
|
||||
@@ -333,27 +333,27 @@ const UserCard = forwardRef<any, any>((_, ref) => {
|
||||
)}
|
||||
</View>
|
||||
<View className={'py-2'}>
|
||||
<View className={'flex justify-around mt-1'}>
|
||||
<View className={'item flex justify-center flex-col items-center'}
|
||||
onClick={() => navTo('/user/ticket/index', true)}>
|
||||
<Text className={'text-xs text-gray-200'} style={themeStyles.textColor}>水票</Text>
|
||||
<Text className={'text-xl text-white'} style={themeStyles.textColor}>{ticketTotal}</Text>
|
||||
</View>
|
||||
<View className={'item flex justify-center flex-col items-center'}
|
||||
onClick={() => navTo('/user/coupon/index', true)}>
|
||||
<Text className={'text-xs text-gray-200'} style={themeStyles.textColor}>优惠券</Text>
|
||||
<Text className={'text-xl text-white'} style={themeStyles.textColor}>{data?.coupons || 0}</Text>
|
||||
</View>
|
||||
<View className={'item flex justify-center flex-col items-center'}
|
||||
onClick={() => navTo('/user/wallet/wallet', true)}>
|
||||
<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>
|
||||
</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={'flex justify-around mt-1'}>*/}
|
||||
{/* <View className={'item flex justify-center flex-col items-center'}*/}
|
||||
{/* onClick={() => navTo('/user/ticket/index', true)}>*/}
|
||||
{/* <Text className={'text-xs text-gray-200'} style={themeStyles.textColor}>水票</Text>*/}
|
||||
{/* <Text className={'text-xl text-white'} style={themeStyles.textColor}>{ticketTotal}</Text>*/}
|
||||
{/* </View>*/}
|
||||
{/* <View className={'item flex justify-center flex-col items-center'}*/}
|
||||
{/* onClick={() => navTo('/user/coupon/index', true)}>*/}
|
||||
{/* <Text className={'text-xs text-gray-200'} style={themeStyles.textColor}>优惠券</Text>*/}
|
||||
{/* <Text className={'text-xl text-white'} style={themeStyles.textColor}>{data?.coupons || 0}</Text>*/}
|
||||
{/* </View>*/}
|
||||
{/* <View className={'item flex justify-center flex-col items-center'}*/}
|
||||
{/* onClick={() => navTo('/user/wallet/wallet', true)}>*/}
|
||||
{/* <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>*/}
|
||||
{/* </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>
|
||||
|
||||
@@ -46,7 +46,12 @@ const UserFooter = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={'text-center py-4 w-full text-gray-300'} onClick={onLoginByPhone}>
|
||||
<div style={{
|
||||
position: 'fixed',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
}} 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'}>Copyright © { new Date().getFullYear() } {Copyright}</div>
|
||||
</div>
|
||||
|
||||
@@ -3,12 +3,9 @@ import navTo from "@/utils/common";
|
||||
import Taro from '@tarojs/taro'
|
||||
import {View, Button} from '@tarojs/components'
|
||||
import {
|
||||
ShieldCheck,
|
||||
Location,
|
||||
Tips,
|
||||
Ask,
|
||||
// Dongdong,
|
||||
People,
|
||||
Agenda,
|
||||
// AfterSaleService,
|
||||
Logout,
|
||||
Shop,
|
||||
@@ -38,7 +35,7 @@ const UserCell = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<View className="bg-white mx-4 mt-4 rounded-xl">
|
||||
<View className="bg-white mx-4 mt-4 rounded-xl" style={{backgroundColor: '#fff', width:'92%', position: 'fixed'}}>
|
||||
<View className="font-semibold text-gray-800 pt-4 pl-4">我的服务</View>
|
||||
<ConfigProvider>
|
||||
<Grid
|
||||
@@ -71,23 +68,21 @@ const UserCell = () => {
|
||||
</Grid.Item>
|
||||
)}
|
||||
|
||||
{(hasRole('staff') || hasRole('admin')) && (
|
||||
<Grid.Item text="门店订单" onClick={() => navTo('/user/store/orders/index', true)}>
|
||||
<Grid.Item text="我的需求" onClick={() => navTo('/credit/order/index', true)}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-orange-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Shop color="#f59e0b" size="20"/>
|
||||
<Agenda color="#8b5cf6" size="20"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
)}
|
||||
|
||||
<Grid.Item text="配送地址" onClick={() => navTo('/user/address/index', true)}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-emerald-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Location color="#3b82f6" size="20"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
{/*<Grid.Item text="配送地址" onClick={() => navTo('/user/address/index', true)}>*/}
|
||||
{/* <View className="text-center">*/}
|
||||
{/* <View className="w-12 h-12 bg-emerald-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
|
||||
{/* <Location color="#3b82f6" size="20"/>*/}
|
||||
{/* </View>*/}
|
||||
{/* </View>*/}
|
||||
{/*</Grid.Item>*/}
|
||||
|
||||
<Grid.Item text={'常见问题'} onClick={() => navTo('/user/help/index')}>
|
||||
<View className="text-center">
|
||||
@@ -111,21 +106,21 @@ const UserCell = () => {
|
||||
</Button>
|
||||
</Grid.Item>
|
||||
|
||||
<Grid.Item text={'实名认证'} onClick={() => navTo('/user/userVerify/index', true)}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-green-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<ShieldCheck color="#10b981" size="20"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
{/*<Grid.Item text={'实名认证'} onClick={() => navTo('/user/userVerify/index', true)}>*/}
|
||||
{/* <View className="text-center">*/}
|
||||
{/* <View className="w-12 h-12 bg-green-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
|
||||
{/* <ShieldCheck color="#10b981" size="20"/>*/}
|
||||
{/* </View>*/}
|
||||
{/* </View>*/}
|
||||
{/*</Grid.Item>*/}
|
||||
|
||||
<Grid.Item text={'推广邀请'} onClick={() => navTo('/dealer/team/index', true)}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-purple-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<People color="#8b5cf6" size="20"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
{/*<Grid.Item text={'推广邀请'} onClick={() => navTo('/dealer/team/index', true)}>*/}
|
||||
{/* <View className="text-center">*/}
|
||||
{/* <View className="w-12 h-12 bg-purple-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
|
||||
{/* <People color="#8b5cf6" size="20"/>*/}
|
||||
{/* </View>*/}
|
||||
{/* </View>*/}
|
||||
{/*</Grid.Item>*/}
|
||||
|
||||
{/*<Grid.Item text={'我的邀请码'} onClick={() => navTo('/dealer/qrcode/index', true)}>*/}
|
||||
{/* <View className="text-center">*/}
|
||||
@@ -144,13 +139,13 @@ const UserCell = () => {
|
||||
{/*</Grid.Item>*/}
|
||||
|
||||
|
||||
<Grid.Item text={'关于我们'} onClick={() => navTo('/user/about/index')}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-amber-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Tips className={'text-amber-500'} size="20"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
{/*<Grid.Item text={'关于我们'} onClick={() => navTo('/user/about/index')}>*/}
|
||||
{/* <View className="text-center">*/}
|
||||
{/* <View className="w-12 h-12 bg-amber-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
|
||||
{/* <Tips className={'text-amber-500'} size="20"/>*/}
|
||||
{/* </View>*/}
|
||||
{/* </View>*/}
|
||||
{/*</Grid.Item>*/}
|
||||
|
||||
<Grid.Item text={'安全退出'} onClick={onLogout}>
|
||||
<View className="text-center">
|
||||
|
||||
@@ -14,6 +14,11 @@ function User() {
|
||||
|
||||
const userCardRef = useRef<any>()
|
||||
const themeStyles = useThemeStyles();
|
||||
// 仅覆盖个人中心页顶部背景为红色(不影响全局主题)
|
||||
const pagePrimaryBackground = {
|
||||
...themeStyles.primaryBackground,
|
||||
background: '#ff0000'
|
||||
}
|
||||
// TabBar 页在小程序里通常不会销毁;从“注册/申请”页返回时需要触发子组件重新初始化/拉取最新状态。
|
||||
const [dealerViewKey, setDealerViewKey] = useState(0)
|
||||
|
||||
@@ -42,7 +47,7 @@ function User() {
|
||||
headHeight={60}
|
||||
>
|
||||
{/* 装饰性背景 */}
|
||||
<View className={'h-64 w-full fixed top-0 z-0'} style={themeStyles.primaryBackground}>
|
||||
<View className={'h-64 w-full fixed top-0 z-0'} style={pagePrimaryBackground}>
|
||||
{/* 装饰性背景元素 - 小程序兼容版本 */}
|
||||
<View className="absolute w-32 h-32 rounded-full" style={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||
@@ -61,8 +66,6 @@ function User() {
|
||||
}}></View>
|
||||
</View>
|
||||
<UserCard ref={userCardRef}/>
|
||||
<UserOrder/>
|
||||
<IsDealer key={dealerViewKey}/>
|
||||
<UserGrid/>
|
||||
<UserFooter/>
|
||||
</PullToRefresh>
|
||||
|
||||
Reference in New Issue
Block a user