feat(add): 新增多页面新增和编辑表单功能

- 添加编辑和新增收货地址页面,支持表单数据加载和提交
- 新增应用密钥凭证、新增应用操作动态、新增应用成员、新增应用版本页面配置
- 实现文章新增及编辑页面,包含图片上传及多种文章属性配置
- 增加注册会员页面,支持头像上传、手机号获取和邀请人关系处理
- 引入统一表单提交成功和失败处理,支持编辑模式数据回显
- 配置统一eslint和editorconfig规则,增强代码规范和编辑体验
- 新增.gitignore规则,屏蔽无关文件和目录,优化版本管理
This commit is contained in:
2026-04-11 12:22:29 +08:00
commit 07f5c92f4b
627 changed files with 85725 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '配送中心'
})

0
src/developer/index.scss Normal file
View File

295
src/developer/index.tsx Normal file
View File

@@ -0,0 +1,295 @@
import React from 'react'
import {View, Text} from '@tarojs/components'
import {ConfigProvider, Button, Grid, Avatar} from '@nutui/nutui-react-taro'
import {
User,
Shopping,
Dongdong,
ArrowRight,
Purse,
People
} from '@nutui/icons-react-taro'
import {useDealerUser} from '@/hooks/useDealerUser'
import { useThemeStyles } from '@/hooks/useTheme'
import {businessGradients, cardGradients, gradientUtils} from '@/styles/gradients'
import Taro from '@tarojs/taro'
const DealerIndex: React.FC = () => {
const {
dealerUser,
error,
refresh,
} = useDealerUser()
// 使用主题样式
const themeStyles = useThemeStyles()
// 导航到各个功能页面
const navigateToPage = (url: string) => {
Taro.navigateTo({url})
}
// 格式化金额
const formatMoney = (money?: string) => {
if (!money) return '0.00'
return parseFloat(money).toFixed(2)
}
// 格式化时间
const formatTime = (time?: string) => {
if (!time) return '-'
return new Date(time).toLocaleDateString()
}
// 获取用户主题
const userTheme = gradientUtils.getThemeByUserId(dealerUser?.userId)
// 获取渐变背景
const getGradientBackground = (themeColor?: string) => {
if (themeColor) {
const darkerColor = gradientUtils.adjustColorBrightness(themeColor, -30)
return gradientUtils.createGradient(themeColor, darkerColor)
}
return userTheme.background
}
console.log(getGradientBackground(),'getGradientBackground()')
if (error) {
return (
<View className="p-4">
<View className="bg-red-50 border border-red-200 rounded-lg p-4 mb-4">
<Text className="text-red-600">{error}</Text>
</View>
<Button type="primary" onClick={refresh}>
</Button>
</View>
)
}
return (
<View className="bg-gray-100 min-h-screen">
<View>
{/*头部信息*/}
{dealerUser && (
<View className="px-4 py-6 relative overflow-hidden" style={themeStyles.primaryBackground}>
{/* 装饰性背景元素 - 小程序兼容版本 */}
<View className="absolute w-32 h-32 rounded-full" style={{
backgroundColor: 'rgba(255, 255, 255, 0.1)',
top: '-16px',
right: '-16px'
}}></View>
<View className="absolute w-24 h-24 rounded-full" style={{
backgroundColor: 'rgba(255, 255, 255, 0.08)',
bottom: '-12px',
left: '-12px'
}}></View>
<View className="absolute w-16 h-16 rounded-full" style={{
backgroundColor: 'rgba(255, 255, 255, 0.05)',
top: '60px',
left: '120px'
}}></View>
<View className="flex items-center justify-between relative z-10 mb-4">
<Avatar
size="50"
src={dealerUser?.qrcode}
icon={<User/>}
className="mr-4"
style={{
border: '2px solid rgba(255, 255, 255, 0.3)'
}}
/>
<View className="flex-1 flex-col">
<View className="text-white text-lg font-bold mb-1" style={{
}}>
{dealerUser?.realName || '分销商'}
</View>
<View className="text-sm" style={{
color: 'rgba(255, 255, 255, 0.8)'
}}>
ID: {dealerUser.userId}
</View>
</View>
<View className="text-right hidden">
<Text className="text-xs" style={{
color: 'rgba(255, 255, 255, 0.9)'
}}></Text>
<Text className="text-xs" style={{
color: 'rgba(255, 255, 255, 0.7)'
}}>
{formatTime(dealerUser.createTime)}
</Text>
</View>
</View>
</View>
)}
{/* 佣金统计卡片 */}
{dealerUser && (
<View className="mx-4 -mt-6 rounded-xl p-4 relative z-10" style={cardGradients.elevated}>
<View className="mb-4">
<Text className="font-semibold text-gray-800"></Text>
</View>
<View className="grid grid-cols-3 gap-3">
<View className="text-center p-3 rounded-lg flex flex-col" style={{
background: businessGradients.money.available
}}>
<Text className="text-lg font-bold mb-1 text-white">
{formatMoney(dealerUser.money)}
</Text>
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}></Text>
</View>
<View className="text-center p-3 rounded-lg flex flex-col" style={{
background: businessGradients.money.frozen
}}>
<Text className="text-lg font-bold mb-1 text-white">
{formatMoney(dealerUser.freezeMoney)}
</Text>
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}></Text>
</View>
<View className="text-center p-3 rounded-lg flex flex-col" style={{
background: businessGradients.money.total
}}>
<Text className="text-lg font-bold mb-1 text-white">
{formatMoney(dealerUser.totalMoney)}
</Text>
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}></Text>
</View>
</View>
</View>
)}
{/* 团队统计 */}
{dealerUser && (
<View className="bg-white mx-4 mt-4 rounded-xl p-4 hidden">
<View className="flex items-center justify-between mb-4">
<Text className="font-semibold text-gray-800"></Text>
<View
className="text-gray-400 text-sm flex items-center"
onClick={() => navigateToPage('/dealer/team/index')}
>
<Text></Text>
<ArrowRight size="12"/>
</View>
</View>
<View className="grid grid-cols-3 gap-4">
<View className="text-center grid">
<Text className="text-xl font-bold text-purple-500 mb-1">
{dealerUser.firstNum || 0}
</Text>
<Text className="text-xs text-gray-500"></Text>
</View>
<View className="text-center grid">
<Text className="text-xl font-bold text-indigo-500 mb-1">
{dealerUser.secondNum || 0}
</Text>
<Text className="text-xs text-gray-500"></Text>
</View>
<View className="text-center grid">
<Text className="text-xl font-bold text-pink-500 mb-1">
{dealerUser.thirdNum || 0}
</Text>
<Text className="text-xs text-gray-500"></Text>
</View>
</View>
</View>
)}
{/* 功能导航 */}
<View className="bg-white mx-4 mt-4 rounded-xl p-4">
<View className="font-semibold mb-4 text-gray-800"></View>
<ConfigProvider>
<Grid
columns={4}
className="no-border-grid"
style={{
'--nutui-grid-border-color': 'transparent',
'--nutui-grid-item-border-width': '0px',
border: 'none'
} as React.CSSProperties}
>
<Grid.Item text="配送订单" onClick={() => navigateToPage('/rider/orders/index')}>
<View className="text-center">
<View className="w-12 h-12 bg-blue-500 rounded-xl flex items-center justify-center mx-auto mb-2">
<Shopping color="#3b82f6" size="20"/>
</View>
</View>
</Grid.Item>
<Grid.Item text={'工资明细'} onClick={() => navigateToPage('/rider/withdraw/index')}>
<View className="text-center">
<View className="w-12 h-12 bg-green-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<Purse color="#10b981" size="20"/>
</View>
</View>
</Grid.Item>
<Grid.Item text={'配送小区'} onClick={() => navigateToPage('/rider/team/index')}>
<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={() => navigateToPage('/rider/qrcode/index')}>
<View className="text-center">
<View className="w-12 h-12 bg-orange-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<Dongdong color="#f59e0b" size="20"/>
</View>
</View>
</Grid.Item>
</Grid>
{/* 第二行功能 */}
{/*<Grid*/}
{/* columns={4}*/}
{/* className="no-border-grid mt-4"*/}
{/* style={{*/}
{/* '--nutui-grid-border-color': 'transparent',*/}
{/* '--nutui-grid-item-border-width': '0px',*/}
{/* border: 'none'*/}
{/* } as React.CSSProperties}*/}
{/*>*/}
{/* <Grid.Item text={'邀请统计'} onClick={() => navigateToPage('/dealer/invite-stats/index')}>*/}
{/* <View className="text-center">*/}
{/* <View className="w-12 h-12 bg-indigo-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
{/* <Presentation color="#6366f1" size="20"/>*/}
{/* </View>*/}
{/* </View>*/}
{/* </Grid.Item>*/}
{/* /!* 预留其他功能位置 *!/*/}
{/* <Grid.Item text={''}>*/}
{/* <View className="text-center">*/}
{/* <View className="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
{/* </View>*/}
{/* </View>*/}
{/* </Grid.Item>*/}
{/* <Grid.Item text={''}>*/}
{/* <View className="text-center">*/}
{/* <View className="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
{/* </View>*/}
{/* </View>*/}
{/* </Grid.Item>*/}
{/* <Grid.Item text={''}>*/}
{/* <View className="text-center">*/}
{/* <View className="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
{/* </View>*/}
{/* </View>*/}
{/* </Grid.Item>*/}
{/*</Grid>*/}
</ConfigProvider>
</View>
</View>
{/* 底部安全区域 */}
<View className="h-20"></View>
</View>
)
}
export default DealerIndex

View File

@@ -0,0 +1,4 @@
export default {
navigationBarTitleText: '配送订单',
navigationBarTextStyle: 'black'
}

View File

@@ -0,0 +1,390 @@
import {useCallback, useEffect, useMemo, useRef, useState} from 'react'
import Taro from '@tarojs/taro'
import { Tabs, TabPane, Cell, Space, Button, Dialog, Image, Empty, InfiniteLoading} from '@nutui/nutui-react-taro'
import {View, Text} from '@tarojs/components'
import dayjs from 'dayjs'
import {pageShopOrder, updateShopOrder} from '@/api/shop/shopOrder'
import type {ShopOrder, ShopOrderParam} from '@/api/shop/shopOrder/model'
import {uploadFile} from '@/api/system/file'
export default function RiderOrders() {
const riderId = useMemo(() => {
const v = Number(Taro.getStorageSync('UserId'))
return Number.isFinite(v) && v > 0 ? v : undefined
}, [])
const pageRef = useRef(1)
const [tabIndex, setTabIndex] = useState(0)
const [list, setList] = useState<ShopOrder[]>([])
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<ShopOrder | null>(null)
const [deliverImg, setDeliverImg] = useState<string | undefined>(undefined)
// 前端展示用:后台可配置实际自动确认收货时长
const AUTO_CONFIRM_RECEIVE_HOURS_FALLBACK = 24
const riderTabs = useMemo(
() => [
{index: 0, title: '全部', statusFilter: -1},
{index: 1, title: '配送中', statusFilter: 3}, // 后端deliveryStatus=20
{index: 2, title: '待客户确认', statusFilter: 3}, // 同上,前端再按 sendEndTime 细分
{index: 3, title: '已完成', statusFilter: 5}, // 后端orderStatus=1
],
[]
)
const isAbnormalOrder = (order: ShopOrder) => {
const s = order.orderStatus
return s === 2 || s === 3 || s === 4 || s === 5 || s === 6 || s === 7
}
const getOrderStatusText = (order: ShopOrder) => {
if (order.orderStatus === 2) return '已取消'
if (order.orderStatus === 3) return '取消中'
if (order.orderStatus === 4) return '退款申请中'
if (order.orderStatus === 5) return '退款被拒绝'
if (order.orderStatus === 6) return '退款成功'
if (order.orderStatus === 7) return '客户申请退款'
if (!order.payStatus) return '未付款'
if (order.orderStatus === 1) return '已完成'
// 配送员页:用 sendEndTime 表示“已送达收货点”
if (order.deliveryStatus === 20) {
if (order.sendEndTime) return '待客户确认收货'
return '配送中'
}
if (order.deliveryStatus === 10) return '待发货'
if (order.deliveryStatus === 30) return '部分发货'
return '处理中'
}
const getOrderStatusColor = (order: ShopOrder) => {
if (isAbnormalOrder(order)) return 'text-orange-500'
if (order.orderStatus === 1) return 'text-green-600'
if (order.sendEndTime) return 'text-purple-600'
return 'text-blue-600'
}
const canConfirmDelivered = (order: ShopOrder) => {
if (!order.payStatus) return false
if (order.orderStatus === 1) return false
if (isAbnormalOrder(order)) return false
// 只允许在“配送中”阶段确认送达
if (order.deliveryStatus !== 20) return false
return !order.sendEndTime
}
const filterByTab = useCallback(
(orders: ShopOrder[]) => {
if (tabIndex === 1) {
// 配送中:未确认送达
return orders.filter(o => o.deliveryStatus === 20 && !o.sendEndTime && !isAbnormalOrder(o) && o.orderStatus !== 1)
}
if (tabIndex === 2) {
// 待客户确认:已确认送达
return orders.filter(o => o.deliveryStatus === 20 && !!o.sendEndTime && !isAbnormalOrder(o) && o.orderStatus !== 1)
}
if (tabIndex === 3) {
return orders.filter(o => o.orderStatus === 1)
}
return orders
},
[tabIndex]
)
const reload = useCallback(
async (resetPage = false) => {
if (!riderId) return
setLoading(true)
setError(null)
const currentPage = resetPage ? 1 : pageRef.current
const currentTab = riderTabs.find(t => t.index === tabIndex) || riderTabs[0]
const params: ShopOrderParam = {
page: currentPage,
riderId,
statusFilter: currentTab.statusFilter,
}
try {
const res = await pageShopOrder(params)
const incoming = (res?.list || []) as ShopOrder[]
setList(prev => (resetPage ? incoming : prev.concat(incoming)))
setHasMore(incoming.length >= 10)
pageRef.current = currentPage
} catch (e) {
console.error('加载配送订单失败:', e)
setError('加载失败,请重试')
setHasMore(false)
} finally {
setLoading(false)
}
},
[riderId, riderTabs, tabIndex]
)
const reloadMore = useCallback(async () => {
if (loading || !hasMore) return
pageRef.current += 1
await reload(false)
}, [hasMore, loading, reload])
const openDeliverDialog = (order: ShopOrder) => {
setDeliverOrder(order)
setDeliverImg(order.sendEndImg)
setDeliverDialogVisible(true)
}
const handleChooseDeliverImg = async () => {
try {
const file = await uploadFile()
setDeliverImg(file?.url)
} catch (e) {
console.error('上传送达照片失败:', e)
Taro.showToast({title: '上传失败,请重试', icon: 'none'})
}
}
const handleConfirmDelivered = async () => {
if (!deliverOrder?.orderId) return
if (deliverSubmitting) return
setDeliverSubmitting(true)
try {
await updateShopOrder({
orderId: deliverOrder.orderId,
// 用于前端/后端识别“配送员已送达收货点”
sendEndTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
sendEndImg: deliverImg,
})
Taro.showToast({title: '已确认送达', icon: 'success'})
setDeliverDialogVisible(false)
setDeliverOrder(null)
setDeliverImg(undefined)
pageRef.current = 1
await reload(true)
} catch (e) {
console.error('确认送达失败:', e)
Taro.showToast({title: '确认送达失败', icon: 'none'})
} finally {
setDeliverSubmitting(false)
}
}
useEffect(() => {
}, [])
useEffect(() => {
pageRef.current = 1
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">
<View>
<Tabs
align="left"
className="fixed left-0"
style={{zIndex: 998, borderBottom: '1px solid #e5e5e5'}}
tabStyle={{backgroundColor: '#ffffff'}}
value={tabIndex}
onChange={(paneKey) => setTabIndex(Number(paneKey))}
>
{riderTabs.map(t => (
<TabPane key={t.index} title={loading && tabIndex === t.index ? `${t.title}...` : t.title}></TabPane>
))}
</Tabs>
<View style={{height: '84vh', width: '100%', padding: '0', overflowY: 'auto', overflowX: 'hidden'}} id="rider-order-scroll">
{error ? (
<View className="flex flex-col items-center justify-center h-64">
<Text className="text-gray-500 mb-4">{error}</Text>
<Button size="small" type="primary" onClick={() => reload(true)}>
</Button>
</View>
) : (
<InfiniteLoading
target="rider-order-scroll"
hasMore={hasMore}
onLoadMore={reloadMore}
loadingText={<></>}
loadMoreText={
displayList.length === 0 ? (
<Empty style={{backgroundColor: 'transparent'}} description="暂无配送订单"/>
) : (
<View className="h-24"></View>
)
}
>
{displayList.map((o, idx) => {
const phoneToCall = o.phone || o.mobile
const flow1Done = !!o.riderId
const flow2Done = o.deliveryStatus === 20 || o.deliveryStatus === 30
const flow3Done = !!o.sendEndTime
const flow4Done = o.orderStatus === 1
// 直接使用订单分页接口返回的 orderGoods
const goodsList = o.orderGoods || []
const goodsNameList = goodsList
.map(g => g?.goodsName || (g as any)?.goodsTitle || (g as any)?.title || (g as any)?.name)
.filter(Boolean) as string[]
const goodsSummary = goodsNameList.length
? `${goodsNameList.slice(0, 3).join('、')}${goodsList.length > 3 ? `${goodsList.length}` : ''}`
: (o.title || '-')
const autoConfirmAt = o.sendEndTime
? dayjs(o.sendEndTime).add(AUTO_CONFIRM_RECEIVE_HOURS_FALLBACK, 'hour')
: null
const autoConfirmLeftMin = autoConfirmAt ? autoConfirmAt.diff(dayjs(), 'minute') : null
return (
<Cell
key={`${o.orderId || idx}`}
style={{padding: '16px'}}
onClick={() => o.orderId && Taro.navigateTo({url: `/shop/orderDetail/index?orderId=${o.orderId}`})}
>
<View className="w-full">
<View className="flex justify-between items-center">
<Text className="text-gray-800 font-bold text-sm">{o.orderNo || `订单#${o.orderId}`}</Text>
<Text className={`${getOrderStatusColor(o)} text-sm font-medium`}>{getOrderStatusText(o)}</Text>
</View>
<View className="text-gray-400 text-xs mt-1">
{o.createTime ? dayjs(o.createTime).format('YYYY-MM-DD HH:mm') : '-'}
</View>
<View className="mt-3 bg-white rounded-lg">
<View className="text-sm text-gray-700">
<Text className="text-gray-500"></Text>
<Text>{o.selfTakeMerchantName || o.address || '-'}</Text>
</View>
<View className="text-sm text-gray-700 mt-1">
<Text className="text-gray-500"></Text>
<Text>{o.realName || '-'} {o.phone ? `(${o.phone})` : ''}</Text>
</View>
<View className="text-sm text-gray-700 mt-1">
<Text className="text-gray-500"></Text>
<Text>{o.payPrice || o.totalPrice || '-'}</Text>
<Text className="text-gray-500 ml-3"></Text>
<Text>{o.totalNum ?? '-'}</Text>
</View>
<View className="text-sm text-gray-700 mt-1">
<Text className="text-gray-500"></Text>
<Text>{goodsSummary}</Text>
</View>
{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>
)}
</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 ? (flow3Done ? 'text-green-600 font-medium' : 'text-blue-600 font-medium') : 'text-gray-400'}>2 </Text>
<Text className="mx-1 text-gray-400">{'>'}</Text>
<Text className={flow3Done ? (flow4Done ? 'text-green-600 font-medium' : '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>
{o.sendEndTime && o.orderStatus !== 1 && autoConfirmAt && (
<View className="mt-1 text-gray-500">
{autoConfirmAt.format('YYYY-MM-DD HH:mm')}
{typeof autoConfirmLeftMin === 'number' && autoConfirmLeftMin > 0 ? `,约剩余 ${Math.ceil(autoConfirmLeftMin / 60)} 小时` : ''}
</View>
)}
</View>
<View className="mt-3 flex justify-end">
<Space>
{!!phoneToCall && (
<Button
size="small"
onClick={(e) => {
e.stopPropagation()
Taro.makePhoneCall({phoneNumber: phoneToCall})
}}
>
</Button>
)}
{canConfirmDelivered(o) && (
<Button
size="small"
type="primary"
onClick={(e) => {
e.stopPropagation()
openDeliverDialog(o)
}}
>
</Button>
)}
</Space>
</View>
</View>
</Cell>
)
})}
</InfiniteLoading>
)}
</View>
</View>
<Dialog
title="确认送达"
visible={deliverDialogVisible}
confirmText={deliverSubmitting ? '提交中...' : '确认送达'}
cancelText="取消"
onConfirm={handleConfirmDelivered}
onCancel={() => {
if (deliverSubmitting) return
setDeliverDialogVisible(false)
setDeliverOrder(null)
setDeliverImg(undefined)
}}
>
<View className="text-sm text-gray-700">
<View></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>
</Dialog>
</View>
)
}