feat(credit): 重构订单管理页面并优化公司模块功能

- 新增 company/add 和 company/edit 页面路由配置
- 移除 Banner.tsx 中的调试日志
- 从 find.tsx 中移除未使用的 total 状态变量
- 更新 credit/order/index.config.ts 页面标题为订单管理并添加自定义导航栏样式
- 从 credit/company/index.tsx 中移除内联添加客户的对话框相关代码
- 将添加客户按钮跳转到独立的 /credit/company/add 页面
- 为公司列表项添加编辑功能点击事件,可跳转至 /credit/company/edit?id=xx
- 优化信用订单页面结构,替换为新的订单管理界面
- 实现订单搜索、筛选、日期范围选择等功能
- 添加订单统计信息展示(总数、本金、利息)
- 实现模拟数据加载和分页功能
This commit is contained in:
2026-03-05 12:50:57 +08:00
parent 31098f889b
commit f4a1fab4cb
11 changed files with 1035 additions and 762 deletions

View File

@@ -122,7 +122,9 @@ export default {
"pages": [
"order/index",
"order/add",
"company/index"
"company/index",
"company/add",
"company/edit"
]
}
],

View File

@@ -0,0 +1,6 @@
export default definePageConfig({
navigationBarTitleText: '添加客户',
navigationBarTextStyle: 'black',
navigationBarBackgroundColor: '#ffffff',
navigationStyle: 'custom'
})

262
src/credit/company/add.tsx Normal file
View File

@@ -0,0 +1,262 @@
import { useMemo, useState } from 'react'
import Taro from '@tarojs/taro'
import { View, Text } from '@tarojs/components'
import { Address, Button, Cell, CellGroup, ConfigProvider, Input } from '@nutui/nutui-react-taro'
import { Close } from '@nutui/icons-react-taro'
import RegionData from '@/api/json/regions-data.json'
import { addCreditCompany, pageCreditCompany } from '@/api/credit/creditCompany'
import type { CreditCompany } from '@/api/credit/creditCompany/model'
import FixedButton from '@/components/FixedButton'
const shortRegion = (label?: string) => {
const v = String(label || '').trim()
if (!v) return ''
return v
.replace(/壮族自治区|回族自治区|维吾尔自治区|自治区/g, '')
.replace(/省|市/g, '')
}
export default function CreditCompanyAddPage() {
const statusBarHeight = useMemo(() => {
try {
const info = Taro.getSystemInfoSync()
return Number(info?.statusBarHeight || 0)
} catch (_e) {
return 0
}
}, [])
const [name, setName] = useState('')
const [code, setCode] = useState('')
const [industry, setIndustry] = useState('')
const [contact, setContact] = useState('')
const [phones, setPhones] = useState<string[]>([''])
const [province, setProvince] = useState('广西壮族自治区')
const [city, setCity] = useState('南宁市')
const [detailAddress, setDetailAddress] = useState('')
const [projectName, setProjectName] = useState('')
const [regionVisible, setRegionVisible] = useState(false)
const [submitting, setSubmitting] = 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 updatePhone = (idx: number, v: string) => {
setPhones(prev => prev.map((p, i) => (i === idx ? v : p)))
}
const addPhone = () => {
setPhones(prev => prev.concat(''))
}
const removePhone = (idx: number) => {
setPhones(prev => prev.filter((_, i) => i !== idx))
}
const submit = async () => {
if (submitting) return
const companyName = name.trim()
if (!companyName) {
Taro.showToast({ title: '请输入客户名称', icon: 'none' })
return
}
const firstPhone = String(phones?.[0] || '').trim()
if (!firstPhone) {
Taro.showToast({ title: '请输入客户联系方式', icon: 'none' })
return
}
setSubmitting(true)
try {
// 先查重:公司名称/统一代码
const keywords = (code.trim() || companyName).trim()
const existsRes = await pageCreditCompany({ page: 1, limit: 1, keywords } as any)
const exists = ((existsRes?.list || []) as CreditCompany[]).some(c => c?.deleted !== 1)
if (exists) {
await Taro.showModal({
title: '提示',
content: '该公司信息已存在,请联系管理员核实',
showCancel: false
})
return
}
const moreTel = phones
.slice(1)
.map(p => String(p || '').trim())
.filter(Boolean)
.join(',')
const payload: CreditCompany = {
name: companyName,
matchName: companyName,
code: code.trim() || undefined,
tel: firstPhone,
moreTel: moreTel || undefined,
province,
city,
address: detailAddress.trim() || undefined,
nationalStandardIndustryCategories: industry.trim() || undefined,
comments: [
contact.trim() ? `联系人:${contact.trim()}` : '',
projectName.trim() ? `项目:${projectName.trim()}` : ''
]
.filter(Boolean)
.join('') || undefined
}
await addCreditCompany(payload)
Taro.showToast({ title: '添加成功', icon: 'success' })
setTimeout(() => {
Taro.navigateBack()
}, 500)
} catch (e) {
console.error('添加客户失败:', e)
Taro.showToast({ title: '添加失败,请重试', icon: 'none' })
} finally {
setSubmitting(false)
}
}
return (
<View className="bg-pink-50 min-h-screen">
<ConfigProvider>
<View
className="fixed z-50 top-0 left-0 right-0 bg-pink-50"
style={{ paddingTop: `${statusBarHeight}px` }}
>
<View className="px-4 h-10 flex items-center justify-between text-sm text-gray-900">
<Text className="font-medium">12:00</Text>
<View className="flex items-center gap-2 text-xs text-gray-600">
<Text></Text>
<Text>Wi-Fi</Text>
<Text></Text>
</View>
</View>
<View className="px-4 pb-2 flex items-center justify-between">
<Text
className="text-sm text-gray-700"
onClick={() => Taro.navigateBack()}
>
</Text>
<Text className="text-base font-semibold text-gray-900"></Text>
<View className="flex items-center gap-3">
<View className="w-7 h-7 rounded-full border border-gray-300 flex items-center justify-center text-gray-700">
<Text>...</Text>
</View>
<View className="w-7 h-7 rounded-full border border-gray-300" />
</View>
</View>
</View>
<View style={{ paddingTop: `${statusBarHeight + 80}px` }} className="max-w-md mx-auto">
<View className="px-3 pt-2 pb-2 text-sm text-gray-500">
<Text></Text>
</View>
<CellGroup>
<Cell title="客户名称*">
<Input value={name} onChange={setName} placeholder="请输入" />
</Cell>
<Cell title="统一代码">
<Input value={code} onChange={setCode} placeholder="请输入" />
</Cell>
<Cell title="所属行业">
<Input value={industry} onChange={setIndustry} placeholder="请输入" />
</Cell>
<Cell title="客户联系人">
<Input value={contact} onChange={setContact} placeholder="请输入" />
</Cell>
<Cell title="客户联系方式*">
<View className="w-full">
{phones.map((p, idx) => (
<View key={idx} className={idx === 0 ? '' : 'mt-2'}>
<View className="flex items-center gap-2">
<View className="flex-1">
<Input value={p} onChange={(v) => updatePhone(idx, v)} placeholder="请输入" />
</View>
{idx > 0 && (
<Button
size="small"
fill="outline"
type="danger"
icon={<Close />}
onClick={() => removePhone(idx)}
>
</Button>
)}
</View>
</View>
))}
<View className="mt-2">
<Text className="text-red-500" onClick={addPhone}>
+
</Text>
</View>
</View>
</Cell>
<Cell
title="所在省份"
description={shortRegion(province) || '请选择'}
onClick={() => setRegionVisible(true)}
/>
<Cell
title="所在地级市"
description={shortRegion(city) || '请选择'}
onClick={() => setRegionVisible(true)}
/>
<Cell title="详细地址">
<Input value={detailAddress} onChange={setDetailAddress} placeholder="请输入" />
</Cell>
<Cell title="项目名称">
<Input value={projectName} onChange={setProjectName} placeholder="请输入" />
</Cell>
</CellGroup>
</View>
<Address
visible={regionVisible}
options={cityOptions as any}
title="选择省市"
onChange={(value: any[]) => {
const arr = (value || []).filter(Boolean)
if (arr[0]) setProvince(arr[0])
if (arr[1]) setCity(arr[1])
setRegionVisible(false)
}}
onClose={() => setRegionVisible(false)}
/>
<FixedButton
text={submitting ? '提交中...' : '确定'}
background="#ef4444"
disabled={submitting}
onClick={submit}
/>
</ConfigProvider>
</View>
)
}

View File

@@ -0,0 +1,7 @@
export default definePageConfig({
navigationBarTitleText: '修改客户信息',
navigationBarTextStyle: 'black',
navigationBarBackgroundColor: '#ffffff',
navigationStyle: 'custom'
})

364
src/credit/company/edit.tsx Normal file
View File

@@ -0,0 +1,364 @@
import { useCallback, useMemo, useState } from 'react'
import Taro, { useDidShow, useRouter } from '@tarojs/taro'
import { View, Text } from '@tarojs/components'
import { Cell, CellGroup, ConfigProvider, Popup, Tag, TextArea } from '@nutui/nutui-react-taro'
import { getCreditCompany } from '@/api/credit/creditCompany'
import type { CreditCompany } from '@/api/credit/creditCompany/model'
import { listUsers } from '@/api/system/user'
import type { User } from '@/api/system/user/model'
import FixedButton from '@/components/FixedButton'
type CustomerStatus = '保护期内' | '已签约' | '已完成' | '保护期外'
const STATUS_OPTIONS: CustomerStatus[] = ['保护期内', '已签约', '已完成', '保护期外']
const PENDING_EDIT_STORAGE_KEY = 'credit_company_pending_edit_map'
const safeParseJSON = <T,>(v: any): T | null => {
try {
if (!v) return null
if (typeof v === 'object') return v as T
if (typeof v === 'string') return JSON.parse(v) as T
return null
} catch (_e) {
return null
}
}
const splitPhones = (raw?: string) => {
const text = String(raw || '').trim()
if (!text) return []
return text
.split(/[\s,;;、\n\r]+/g)
.map(s => s.trim())
.filter(Boolean)
}
const getCompanyPhones = (c?: CreditCompany | null) => {
if (!c) return []
const arr = [...splitPhones(c.tel), ...splitPhones(c.moreTel)]
return Array.from(new Set(arr))
}
const getCompanyIndustry = (c?: CreditCompany | null) => {
if (!c) return ''
return String(
c.nationalStandardIndustryCategories6 ||
c.nationalStandardIndustryCategories2 ||
c.nationalStandardIndustryCategories ||
c.institutionType ||
''
).trim()
}
type PendingEdit = {
status: CustomerStatus
remark: string
submittedAt: string
}
const loadPendingMap = (): Record<string, PendingEdit> => {
try {
const raw = Taro.getStorageSync(PENDING_EDIT_STORAGE_KEY)
return safeParseJSON<Record<string, PendingEdit>>(raw) || {}
} catch (_e) {
return {}
}
}
const savePendingMap = (map: Record<string, PendingEdit>) => {
try {
Taro.setStorageSync(PENDING_EDIT_STORAGE_KEY, map)
} catch (_e) {
// ignore
}
}
export default function CreditCompanyEditPage() {
const router = useRouter()
const companyId = Number(router?.params?.id)
const statusBarHeight = useMemo(() => {
try {
const info = Taro.getSystemInfoSync()
return Number(info?.statusBarHeight || 0)
} catch (_e) {
return 0
}
}, [])
const [loading, setLoading] = useState(false)
const [company, setCompany] = useState<CreditCompany | null>(null)
const [staffList, setStaffList] = useState<User[]>([])
const [statusPickerVisible, setStatusPickerVisible] = useState(false)
const [customerStatus, setCustomerStatus] = useState<CustomerStatus>('保护期内')
const [remark, setRemark] = useState('')
const [pending, setPending] = useState(false)
const [pendingAt, setPendingAt] = useState<string | undefined>(undefined)
const [submitting, setSubmitting] = useState(false)
const ensureStaffLoaded = useCallback(async () => {
if (staffList.length) return
try {
const res = await listUsers({ isStaff: true } as any)
setStaffList((res || []) as User[])
} catch (_e) {
// ignore (只影响“跟进人”展示)
}
}, [staffList.length])
const staffNameMap = useMemo(() => {
const map = new Map<number, string>()
for (const u of staffList) {
if (!u?.userId) continue
map.set(u.userId, String(u.realName || u.nickname || u.username || `员工${u.userId}`))
}
return map
}, [staffList])
const followRealName = useMemo(() => {
if (!company?.userId) return ''
return staffNameMap.get(Number(company.userId)) || ''
}, [company?.userId, staffNameMap])
const assignDate = useMemo(() => {
// 兼容:接口未提供“分配日期”字段时,用 updateTime/createTime 兜底展示
return String(company?.updateTime || company?.createTime || '')
}, [company?.createTime, company?.updateTime])
const pendingKey = useMemo(() => (Number.isFinite(companyId) && companyId > 0 ? String(companyId) : ''), [companyId])
const loadCompany = useCallback(async () => {
if (!Number.isFinite(companyId) || companyId <= 0) {
Taro.showToast({ title: '参数错误', icon: 'none' })
return
}
setLoading(true)
try {
const data = await getCreditCompany(companyId)
setCompany(data)
// 从本地 pending map 恢复“待审核”状态与内容(纯前端模拟)
const map = loadPendingMap()
const pendingEdit = pendingKey ? map[pendingKey] : undefined
if (pendingEdit) {
setCustomerStatus(pendingEdit.status)
setRemark(pendingEdit.remark)
setPending(true)
setPendingAt(pendingEdit.submittedAt)
} else {
setCustomerStatus('保护期内')
setRemark('')
setPending(false)
setPendingAt(undefined)
}
} catch (e) {
console.error('加载客户信息失败:', e)
Taro.showToast({ title: '加载失败', icon: 'none' })
} finally {
setLoading(false)
}
}, [companyId, pendingKey])
useDidShow(() => {
loadCompany().then()
ensureStaffLoaded().then()
})
const submitOrWithdraw = async () => {
if (submitting) return
if (!pendingKey) return
// 撤回修改
if (pending) {
setSubmitting(true)
try {
const map = loadPendingMap()
delete map[pendingKey]
savePendingMap(map)
setPending(false)
setPendingAt(undefined)
Taro.showToast({ title: '已撤回,可重新修改', icon: 'success' })
} finally {
setSubmitting(false)
}
return
}
// 提交修改(仅备注 + 状态),进入待审核(纯前端模拟)
const r = remark.trim()
if (!r) {
Taro.showToast({ title: '请填写备注信息', icon: 'none' })
return
}
setSubmitting(true)
try {
const map = loadPendingMap()
const now = new Date().toISOString()
map[pendingKey] = { status: customerStatus, remark: r, submittedAt: now }
savePendingMap(map)
setPending(true)
setPendingAt(now)
Taro.showToast({ title: '提交成功,等待审核', icon: 'success' })
} finally {
setSubmitting(false)
}
}
const phones = useMemo(() => getCompanyPhones(company), [company])
const industry = useMemo(() => getCompanyIndustry(company), [company])
const companyName = String(company?.matchName || company?.name || '')
return (
<View className="bg-pink-50 min-h-screen">
<ConfigProvider>
<View className="fixed z-50 top-0 left-0 right-0 bg-pink-50" style={{ paddingTop: `${statusBarHeight}px` }}>
<View className="px-4 h-10 flex items-center justify-between text-sm text-gray-900">
<Text className="font-medium">12:00</Text>
<View className="flex items-center gap-2 text-xs text-gray-600">
<Text></Text>
<Text>Wi-Fi</Text>
<Text></Text>
</View>
</View>
<View className="px-4 pb-2 flex items-center justify-between">
<Text className="text-sm text-gray-700" onClick={() => Taro.navigateBack()}>
</Text>
<Text className="text-base font-semibold text-gray-900"></Text>
<View className="flex items-center gap-3">
<View className="w-7 h-7 rounded-full border border-gray-300 flex items-center justify-center text-gray-700">
<Text>...</Text>
</View>
<View className="w-7 h-7 rounded-full border border-gray-300" />
</View>
</View>
</View>
<View style={{ paddingTop: `${statusBarHeight + 80}px` }} className="max-w-md mx-auto px-3">
{loading ? (
<View className="py-12 text-center text-gray-500">
<Text>...</Text>
</View>
) : (
<>
<CellGroup>
<Cell title="客户名称" description={companyName || '-'} />
<Cell title="统一代码" description={String(company?.code || '-')} />
<Cell
title="所属行业"
description={
<Text className="text-red-500">
{industry || '-'}
</Text>
}
/>
<Cell title="客户联系人" description={String((company as any)?.contactName || '-')} />
<Cell
title="客户联系方式"
description={
phones.length ? (
<View className="flex flex-col">
{phones.map(p => (
<Text key={p} className="text-gray-600">
{p}
</Text>
))}
</View>
) : (
'-'
)
}
/>
<Cell title="地址" description={String(company?.address || '-')} />
<Cell
title="跟进人"
description={
<Text className="text-blue-600">
{followRealName || '未分配'}
</Text>
}
/>
<Cell title="分配日期" description={assignDate || '-'} />
<Cell
title="客户状态"
description={
<View className="flex items-center gap-2">
<Text className={customerStatus === '保护期内' ? 'text-green-600' : 'text-gray-700'}>
{customerStatus}
</Text>
{pending && <Tag type="warning"></Tag>}
</View>
}
onClick={() => {
if (pending) return
setStatusPickerVisible(true)
}}
/>
</CellGroup>
<View className="mt-3 px-1">
<View className="text-sm text-gray-700 mb-2"></View>
<TextArea
placeholder="请输入客户最新信息情况,管理员审核通过后,客户信息才可变更!"
value={remark}
onChange={(v) => setRemark(v)}
rows={4}
maxLength={500}
showCount
autoHeight
disabled={pending}
/>
{pending && (
<View className="mt-2 text-xs text-gray-500">
{pendingAt ? `${pendingAt}` : ''}
</View>
)}
</View>
</>
)}
</View>
<Popup
visible={statusPickerVisible}
position="bottom"
style={{ height: '45vh' }}
onClose={() => setStatusPickerVisible(false)}
>
<View className="p-4">
<View className="flex items-center justify-between mb-3">
<Text className="text-base font-medium"></Text>
<Text className="text-sm text-gray-500" onClick={() => setStatusPickerVisible(false)}>
</Text>
</View>
<CellGroup>
{STATUS_OPTIONS.map(s => (
<Cell
key={s}
title={<Text className={s === customerStatus ? 'text-blue-600' : ''}>{s}</Text>}
onClick={() => {
setCustomerStatus(s)
setStatusPickerVisible(false)
}}
/>
))}
</CellGroup>
</View>
</Popup>
<FixedButton
text={submitting ? '处理中...' : pending ? '撤回修改' : '确定修改'}
background="#ef4444"
disabled={loading || submitting}
onClick={submitOrWithdraw}
/>
</ConfigProvider>
</View>
)
}

View File

@@ -8,10 +8,8 @@ import {
CellGroup,
Checkbox,
ConfigProvider,
Dialog,
Empty,
InfiniteLoading,
Input,
Loading,
Popup,
PullToRefresh,
@@ -21,7 +19,7 @@ import {
import { Copy, Phone } from '@nutui/icons-react-taro'
import RegionData from '@/api/json/regions-data.json'
import { addCreditCompany, pageCreditCompany, updateCreditCompany } from '@/api/credit/creditCompany'
import { pageCreditCompany, updateCreditCompany } from '@/api/credit/creditCompany'
import type { CreditCompany } from '@/api/credit/creditCompany/model'
import { listUsers } from '@/api/system/user'
import type { User } from '@/api/system/user/model'
@@ -101,17 +99,6 @@ export default function CreditCompanyPage() {
const [staffSelectedId, setStaffSelectedId] = useState<number | undefined>(undefined)
const [assigning, setAssigning] = useState(false)
const [addDialogVisible, setAddDialogVisible] = useState(false)
const [addStep, setAddStep] = useState<'search' | 'form'>('search')
const [addChecking, setAddChecking] = useState(false)
const [addSubmitting, setAddSubmitting] = useState(false)
const [addName, setAddName] = useState('')
const [addCode, setAddCode] = useState('')
const [addPhone, setAddPhone] = useState('')
const [addCityVisible, setAddCityVisible] = useState(false)
const [addCityText, setAddCityText] = useState('')
const [addIndustry, setAddIndustry] = useState('')
const [followMap, setFollowMap] = useState<Record<string, FollowStatus>>(() => {
const raw = Taro.getStorageSync(FOLLOW_MAP_STORAGE_KEY)
return safeParseJSON<Record<string, FollowStatus>>(raw) || {}
@@ -350,83 +337,7 @@ export default function CreditCompanyPage() {
}
const openAddCustomer = () => {
setAddStep('search')
setAddName('')
setAddCode('')
setAddPhone('')
setAddCityText('')
setAddIndustry('')
setAddDialogVisible(true)
}
const checkCompanyExists = async () => {
if (addChecking) return
const name = addName.trim()
const code = addCode.trim()
const keywords = (name || code).trim()
if (!keywords) {
Taro.showToast({ title: '请输入公司名称或统一代码', icon: 'none' })
return
}
setAddChecking(true)
try {
const res = await pageCreditCompany({ page: 1, limit: 1, keywords } as any)
const exists = ((res?.list || []) as CreditCompany[]).some(c => c?.deleted !== 1)
if (exists) {
await Taro.showModal({
title: '提示',
content: '该公司信息已存在,请联系管理员核实',
showCancel: false
})
return
}
// 未存在:进入录入表单
setAddStep('form')
} catch (e) {
console.error('查询公司失败:', e)
Taro.showToast({ title: '查询失败,请重试', icon: 'none' })
} finally {
setAddChecking(false)
}
}
const submitAddCustomer = async () => {
if (addSubmitting) return
const name = addName.trim()
const code = addCode.trim()
const phone = addPhone.trim()
if (!name) {
Taro.showToast({ title: '请输入公司名称', icon: 'none' })
return
}
setAddSubmitting(true)
try {
const regionParts = addCityText.split(' ').map(s => s.trim()).filter(Boolean)
const province = regionParts.length >= 2 ? regionParts[0] : undefined
const city = regionParts.length >= 2 ? regionParts[1] : (regionParts[0] || undefined)
const payload: CreditCompany = {
name,
matchName: name,
code: code || undefined,
tel: phone || undefined,
province,
city,
nationalStandardIndustryCategories: addIndustry || undefined
}
await addCreditCompany(payload)
Taro.showToast({ title: '添加成功', icon: 'success' })
setAddDialogVisible(false)
await reload(true)
} catch (e) {
console.error('添加客户失败:', e)
Taro.showToast({ title: '添加失败,请重试', icon: 'none' })
} finally {
setAddSubmitting(false)
}
Taro.navigateTo({ url: '/credit/company/add' })
}
const ensureStaffLoaded = async () => {
@@ -564,10 +475,21 @@ export default function CreditCompanyPage() {
const primaryPhone = phones[0]
return (
<CellGroup key={c.id || idx} className="mb-3">
<Cell>
<Cell
onClick={() => {
if (selectMode) return
if (!c?.id) return
Taro.navigateTo({ url: `/credit/company/edit?id=${c.id}` })
}}
>
<View className="flex gap-3 items-start w-full">
{selectMode && (
<View className="pt-1">
<View
className="pt-1"
onClick={(e) => {
e.stopPropagation()
}}
>
<Checkbox
checked={selected}
onChange={(checked) => toggleSelectId(id, checked)}
@@ -641,11 +563,11 @@ export default function CreditCompanyPage() {
<View className="h-20 w-full" />
<View className="fixed z-50 bottom-0 left-0 right-0 bg-white border-t border-gray-200 px-3 py-3 safe-area-bottom">
<View className="flex justify-between items-center gap-3">
<View className="flex items-center gap-3">
<Button
type="primary"
style={{ background: '#ef4444' }}
block
className="flex-1"
onClick={openAddCustomer}
>
@@ -654,7 +576,7 @@ export default function CreditCompanyPage() {
type="primary"
style={{ background: canAssign ? '#3b82f6' : '#94a3b8' }}
disabled={!canAssign || assigning}
block
className="flex-1"
onClick={openAssign}
>
{selectMode ? '分配所选' : '分配客户'}
@@ -812,67 +734,6 @@ export default function CreditCompanyPage() {
</View>
</Popup>
<Dialog
title={addStep === 'search' ? '添加客户(查重)' : '添加客户'}
visible={addDialogVisible}
confirmText={
addStep === 'search'
? (addChecking ? '查询中...' : '查询')
: (addSubmitting ? '提交中...' : '提交')
}
cancelText="取消"
onCancel={() => {
if (addChecking || addSubmitting) return
setAddDialogVisible(false)
}}
onConfirm={() => {
if (addStep === 'search') return checkCompanyExists()
return submitAddCustomer()
}}
>
<View className="text-sm text-gray-700">
<View className="text-xs text-gray-500 mb-2">
</View>
<CellGroup>
<Cell title="公司名称">
<Input value={addName} onChange={setAddName} placeholder="请输入公司名称" />
</Cell>
<Cell title="统一代码">
<Input value={addCode} onChange={setAddCode} placeholder="请输入统一代码(可选)" />
</Cell>
{addStep === 'form' && (
<>
<Cell title="电话">
<Input value={addPhone} onChange={setAddPhone} placeholder="请输入联系电话(可选)" />
</Cell>
<Cell
title="地区"
description={addCityText || '选择到城市'}
onClick={() => setAddCityVisible(true)}
/>
<Cell title="行业">
<Input value={addIndustry} onChange={setAddIndustry} placeholder="请输入行业(可选)" />
</Cell>
</>
)}
</CellGroup>
<Address
visible={addCityVisible}
options={cityOptions as any}
title="选择地区(到城市)"
onChange={(value: any[]) => {
const txt = value.filter(Boolean).slice(0, 2).join(' ')
setAddCityText(txt)
setAddCityVisible(false)
}}
onClose={() => setAddCityVisible(false)}
/>
</View>
</Dialog>
</ConfigProvider>
</View>
)

View File

@@ -1,5 +1,6 @@
export default definePageConfig({
navigationBarTitleText: '需求列表',
navigationBarTitleText: '订单管理',
navigationBarTextStyle: 'black',
navigationBarBackgroundColor: '#ffffff'
navigationBarBackgroundColor: '#ffffff',
navigationStyle: 'custom'
})

View File

@@ -1,636 +1,408 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import Taro, { useDidShow } from '@tarojs/taro'
import { useMemo, useState } from 'react'
import Taro 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 { Button, ConfigProvider, DatePicker, Empty, Input, Popup } from '@nutui/nutui-react-taro'
import { Search, Setting } from '@nutui/icons-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'
type PayStatus = '全部回款' | '部分回款' | '未回款'
type AmountSort = '默认' | '从高到低' | '从低到高'
const PAGE_SIZE = 10
type OrderItem = {
id: string
orderNo: string
companyName: string
unifiedCode: string
projectName: string
follower: string
payStatus: PayStatus
date: string // YYYY-MM-DD
principal: number
interest: number
}
type DeliverConfirmMode = 'photoComplete' | 'waitCustomerConfirm'
const formatYmd = (d?: Date | null) => {
if (!d) return ''
return dayjs(d).format('YYYY-MM-DD')
}
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 getStatusStyle = (s: PayStatus) => {
if (s === '全部回款') return 'bg-green-500'
if (s === '部分回款') return 'bg-orange-500'
return 'bg-red-500'
}
const makeMockOrders = (page: number): OrderItem[] => {
const companies = [
{ name: '广西万宇工程建设有限公司', code: '91450100MA00000001' },
{ name: '广西远恒建筑有限公司', code: '91450100MA00000002' },
{ name: '南宁宏达工程有限公司', code: '91450100MA00000003' },
{ name: '桂林鑫盛建设有限公司', code: '91450100MA00000004' }
]
const followers = ['罗天东', '张三', '李四', '王五']
const suffix = ['一期', '二期', '改扩建', '配套', '市政', '装饰', '机电']
// page=1给出 16 条示例,统计更直观(项目=订单维度,而非公司维度)
const size = page === 1 ? 16 : 6
const baseNo = 9099009999 + (page - 1) * 100
const list: OrderItem[] = []
for (let i = 0; i < size; i++) {
const c = companies[(page + i) % companies.length]
const follower = followers[(i + page) % followers.length]
const payStatus: PayStatus =
page === 1 ? '全部回款' : (['全部回款', '部分回款', '未回款'][(i + page) % 3] as PayStatus)
const orderNo = String(baseNo + i)
const date = dayjs('2025-10-10').subtract((page - 1) * 7 + (i % 6), 'day').format('YYYY-MM-DD')
// page=1让本金/利息合计更规整(本金 2000利息 200
const principal = page === 1 ? (i < 8 ? 100 : 150) : 200 + ((i + page) % 5) * 50
const interest = page === 1 ? (i < 8 ? 10 : 15) : 20 + ((i + page) % 4) * 5
const projectName = `${c.name}${suffix[(i + page) % suffix.length]}项目`
list.push({
id: `${page}-${i}-${orderNo}`,
orderNo,
companyName: c.name,
unifiedCode: c.code,
projectName,
follower,
payStatus,
date,
principal,
interest
})
}
return list
}
export default function CreditOrderPage() {
const statusBarHeight = useMemo(() => {
try {
const info = Taro.getSystemInfoSync()
return Number(info?.statusBarHeight || 0)
} catch (_e) {
return 0
}
}, [])
const pageRef = useRef(1)
const listRef = useRef<GltTicketOrder[]>([])
const [rawList, setRawList] = useState<OrderItem[]>(() => makeMockOrders(1))
const [mockPage, setMockPage] = useState(1)
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 [searchValue, setSearchValue] = useState('')
const [amountSort, setAmountSort] = useState<AmountSort>('默认')
const [payFilter, setPayFilter] = useState<PayStatus>('全部回款')
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 [datePopupVisible, setDatePopupVisible] = useState(false)
const [datePickerVisible, setDatePickerVisible] = useState(false)
const [picking, setPicking] = useState<'start' | 'end'>('start')
const [startDate, setStartDate] = useState<Date | null>(null)
const [endDate, setEndDate] = useState<Date | null>(null)
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 filteredList = useMemo(() => {
let list = rawList.slice()
const q = searchValue.trim()
if (q) {
const qq = q.toLowerCase()
list = list.filter(o => {
return (
String(o.companyName || '').toLowerCase().includes(qq) ||
String(o.unifiedCode || '').toLowerCase().includes(qq) ||
String(o.orderNo || '').toLowerCase().includes(qq)
)
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'
if (payFilter) {
list = list.filter(o => o.payStatus === payFilter)
}
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
if (startDate || endDate) {
const start = startDate ? dayjs(startDate).startOf('day') : null
const end = endDate ? dayjs(endDate).endOf('day') : null
list = list.filter(o => {
const t = dayjs(o.date, 'YYYY-MM-DD')
if (start && t.isBefore(start)) return false
if (end && t.isAfter(end)) return false
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')
if (amountSort !== '默认') {
list.sort((a, b) => {
const av = Number(a.principal || 0)
const bv = Number(b.principal || 0)
return amountSort === '从高到低' ? bv - av : av - bv
})
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)
return list
}, [amountSort, endDate, payFilter, rawList, searchValue, startDate])
const stats = useMemo(() => {
// 业务说明:一个公司可能对应多个项目(多条订单),因此“订单数量”=项目/订单条数,而不是公司数。
const total = filteredList.length
const principal = filteredList.reduce((sum, o) => sum + Number(o.principal || 0), 0)
const interest = filteredList.reduce((sum, o) => sum + Number(o.interest || 0), 0)
return { total, principal, interest }
}, [filteredList])
const timeText = useMemo(() => {
const s = formatYmd(startDate)
const e = formatYmd(endDate)
if (!s && !e) return '时间筛选'
if (s && e) return `${s}~${e}`
return s ? `${s}~` : `~${e}`
}, [endDate, startDate])
const pickAmountSort = async () => {
try {
// 送达时同步记录配送员当前位置(用于门店/后台跟踪骑手位置)
const loc = await getCurrentLngLat('确认送达需要记录您的当前位置,请在设置中开启定位权限后重试。')
if (!loc) return
const options: AmountSort[] = ['默认', '从高到低', '从低到高']
const res = await Taro.showActionSheet({ itemList: options })
const next = options[res.tapIndex]
if (next) setAmountSort(next)
} catch (e) {
const msg = String((e as any)?.errMsg || (e as any)?.message || e || '')
if (msg.includes('cancel')) return
console.error('选择金额排序失败:', e)
}
}
const pickPayFilter = async () => {
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 })
}
const options: PayStatus[] = ['全部回款', '部分回款', '未回款']
const res = await Taro.showActionSheet({ itemList: options })
const next = options[res.tapIndex]
if (next) setPayFilter(next)
} 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)
const msg = String((e as any)?.errMsg || (e as any)?.message || e || '')
if (msg.includes('cancel')) return
console.error('选择回款筛选失败:', e)
}
}
useEffect(() => {
listRef.current = list
}, [list])
const loadMore = () => {
const nextPage = mockPage + 1
const incoming = makeMockOrders(nextPage)
setMockPage(nextPage)
setRawList(prev => prev.concat(incoming))
console.log('加载更多:', { nextPage, appended: incoming.length })
Taro.showToast({ title: '已加载更多', icon: 'none' })
}
useDidShow(() => {
pageRef.current = 1
listRef.current = []
setList([])
setHasMore(true)
void reload(true)
// eslint-disable-next-line react-hooks/exhaustive-deps
})
const headerOffset = statusBarHeight + 80
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 className="bg-pink-50 min-h-screen">
<ConfigProvider>
<View className="fixed z-50 top-0 left-0 right-0 bg-pink-50" style={{ paddingTop: `${statusBarHeight}px` }}>
<View className="px-4 h-10 flex items-center justify-between text-sm text-gray-900">
<Text className="font-medium">12:00</Text>
<View className="flex items-center gap-2 text-xs text-gray-600">
<Text></Text>
<Text>Wi-Fi</Text>
<Text></Text>
</View>
)
</View>
<View className="px-4 pb-2 flex items-center justify-between">
<Text className="text-sm text-gray-700" onClick={() => Taro.navigateBack()}>
</Text>
<Text className="text-base font-semibold text-gray-900"></Text>
<View className="flex items-center gap-3">
<View
className="w-7 h-7 rounded-full border border-gray-300 flex items-center justify-center text-gray-700"
onClick={async () => {
try {
const res = await Taro.showActionSheet({ itemList: ['新建订单', '刷新'] })
if (res.tapIndex === 0) Taro.navigateTo({ url: '/credit/order/add' })
if (res.tapIndex === 1) {
setSearchValue('')
setAmountSort('默认')
setPayFilter('全部回款')
setStartDate(null)
setEndDate(null)
setMockPage(1)
setRawList(makeMockOrders(1))
Taro.showToast({ title: '已刷新', icon: 'none' })
}
} catch (e) {
const msg = String((e as any)?.errMsg || (e as any)?.message || e || '')
if (msg.includes('cancel')) return
}
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>
<Text>...</Text>
</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>
)
}
<View
className="w-7 h-7 rounded-full border border-gray-300 flex items-center justify-center text-gray-700"
onClick={() => Taro.showToast({ title: '设置(示意)', icon: 'none' })}
>
{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>
<Setting size={14} />
</View>
</View>
</View>
</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 style={{ paddingTop: `${headerOffset}px` }} className="max-w-md mx-auto">
<View className="px-4 pt-2">
<View className="bg-white rounded-full border border-pink-100 px-3 py-2 flex items-center gap-2">
<Search size={16} className="text-gray-400" />
<View className="flex-1">
<Input
value={searchValue}
onChange={setSearchValue}
placeholder="请输入公司名称、统一代码查询、订单号查询"
/>
</View>
<View className="text-sm text-gray-700 mt-1">
<Text className="text-gray-500"></Text>
</View>
<View className="mt-3 grid grid-cols-3 gap-2">
<View
className="bg-white rounded-xl border border-pink-100 px-2 py-2 text-xs text-gray-700 flex items-center justify-center"
onClick={pickAmountSort}
>
<Text className="mr-1"></Text>
<Text className="text-gray-400">{amountSort === '从高到低' ? '↓' : amountSort === '从低到高' ? '↑' : '↕'}</Text>
</View>
<View
className="bg-white rounded-xl border border-pink-100 px-2 py-2 text-xs text-gray-700 flex items-center justify-center"
onClick={pickPayFilter}
>
<Text>{payFilter}</Text>
</View>
<View
className="bg-white rounded-xl border border-pink-100 px-2 py-2 text-xs text-gray-700 flex items-center justify-center"
onClick={() => setDatePopupVisible(true)}
>
<Text className="truncate">{timeText}</Text>
</View>
</View>
<View className="mt-2 text-xs text-gray-400 flex items-center justify-between">
<Text>{stats.total}</Text>
<Text>{stats.principal}</Text>
<Text>{stats.interest}</Text>
</View>
</View>
<View className="px-4 mt-3 pb-6">
{!filteredList.length ? (
<View className="bg-white rounded-xl border border-pink-100 py-10">
<Empty description="暂无订单" />
</View>
) : (
filteredList.map(o => (
<View key={o.id} className="bg-white rounded-xl border border-pink-100 p-3 mb-3">
<View className="flex items-center justify-between">
<Text className="text-xs text-gray-500">{o.orderNo}</Text>
<View className={`px-2 py-1 rounded-full ${getStatusStyle(o.payStatus)}`}>
<Text className="text-xs text-white">{o.payStatus}</Text>
</View>
</View>
{/* 项目名称是核心分组/统计维度(一个公司可有多个项目=多条订单),因此需要突出显示 */}
<View className="mt-2">
<Text className="text-sm font-semibold text-rose-600">{o.projectName}</Text>
</View>
<View className="mt-2 flex flex-wrap gap-x-4 gap-y-1 text-xs text-gray-600">
<Text>{o.companyName}</Text>
<Text>{o.follower}</Text>
<Text>{o.date}</Text>
</View>
<View className="mt-2 flex items-center gap-6 text-xs text-gray-700">
<Text>
{o.nickname || '-'} {o.phone ? `(${o.phone})` : ''}
<Text className="text-gray-400"></Text>
{o.principal}
</Text>
<Text>
<Text className="text-gray-400"></Text>
{o.interest}
</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 className="mt-2 flex justify-center">
<Button
fill="none"
size="small"
style={{ color: '#bdbdbd' }}
onClick={loadMore}
>
</Button>
</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>
<Popup
visible={datePopupVisible}
position="bottom"
onClose={() => setDatePopupVisible(false)}
style={{ borderTopLeftRadius: '12px', borderTopRightRadius: '12px' }}
>
<View className="px-4 py-4 bg-white">
<View className="text-base font-semibold text-gray-900"></View>
<View className="mt-3 text-sm text-gray-700">
<View className="flex items-center justify-between py-2" onClick={() => { setPicking('start'); setDatePickerVisible(true) }}>
<Text></Text>
<Text className="text-gray-500">{formatYmd(startDate) || '未选择'}</Text>
</View>
<View className="flex items-center justify-between py-2" onClick={() => { setPicking('end'); setDatePickerVisible(true) }}>
<Text></Text>
<Text className="text-gray-500">{formatYmd(endDate) || '未选择'}</Text>
</View>
</View>
<View className="mt-3 flex justify-end">
<Space>
{!!phoneToCall && (
<View className="mt-4 flex items-center justify-between gap-3">
<Button
size="small"
onClick={e => {
e.stopPropagation()
Taro.makePhoneCall({ phoneNumber: phoneToCall })
fill="outline"
onClick={() => {
setStartDate(null)
setEndDate(null)
}}
>
</Button>
)}
{!!addr && addr !== '-' && (
<Button
size="small"
onClick={e => {
e.stopPropagation()
void Taro.setClipboardData({ data: addr })
Taro.showToast({ title: '地址已复制', icon: 'none' })
}}
>
<Button type="primary" onClick={() => setDatePopupVisible(false)}>
</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>
</Popup>
<Dialog
title="确认送达"
visible={deliverDialogVisible}
confirmText={
deliverSubmitting
? '提交中...'
: deliverConfirmMode === 'photoComplete'
? '拍照完成'
: '确认送达'
<DatePicker
visible={datePickerVisible}
title={picking === 'start' ? '选择开始日期' : '选择结束日期'}
type="date"
value={(picking === 'start' ? startDate : endDate) || new Date()}
startDate={dayjs('2020-01-01').toDate()}
endDate={dayjs('2035-12-31').toDate()}
onClose={() => setDatePickerVisible(false)}
onCancel={() => setDatePickerVisible(false)}
onConfirm={(_options, selectedValue) => {
const [y, m, d] = (selectedValue || []).map(v => Number(v))
const next = new Date(y, (m || 1) - 1, d || 1, 0, 0, 0)
if (picking === 'start') {
setStartDate(next)
if (endDate && dayjs(endDate).isBefore(next, 'day')) setEndDate(null)
} else {
setEndDate(next)
if (startDate && dayjs(next).isBefore(startDate, 'day')) setStartDate(null)
}
cancelText="取消"
onConfirm={handleConfirmDelivered}
onCancel={() => {
if (deliverSubmitting) return
setDeliverDialogVisible(false)
setDeliverOrder(null)
setDeliverImg(undefined)
setDeliverConfirmMode('photoComplete')
setDatePickerVisible(false)
}}
>
<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>
/>
</ConfigProvider>
</View>
)
}

View File

@@ -56,7 +56,6 @@ const Find = () => {
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 pageRef = useRef(1)
@@ -86,7 +85,6 @@ const Find = () => {
latestListRef.current = []
setStoreList([])
setHasMore(true)
setTotal(0)
}
try {
@@ -112,7 +110,6 @@ const Find = () => {
setStoreList(nextList)
const count = typeof res?.count === 'number' ? res.count : nextList.length
setTotal(count)
setHasMore(nextList.length < count)
if (resList.length > 0) {

View File

@@ -16,6 +16,7 @@ const MyPage = () => {
}
const bannerHeight = toCssSize(ad?.height)
console.log(bannerHeight)
const reload = async () => {
const flash = await getCmsAdByCode('flash')

View File

@@ -19,7 +19,7 @@ function User() {
}
// TabBar 页在小程序里通常不会销毁;从“注册/申请”页返回时需要触发子组件重新初始化/拉取最新状态。
const [dealerViewKey, setDealerViewKey] = useState(0)
console.log(dealerViewKey)
// 下拉刷新处理
const handleRefresh = async () => {
if (userCardRef.current?.handleRefresh) {