feat(credit): 添加业务员选择页面并重构客户分配功能
- 在路由配置中添加 pageUser/index 页面路径 - 移除原有的员工弹窗选择组件 - 将权限判断从 superAdmin 改为 admin - 修改员工列表加载逻辑,查询条件从 isStaff 改为 isAdmin - 实现新的业务员选择页面,支持搜索、分页和下拉刷新 - 使用页面跳转方式替代弹窗进行业务员选择 - 更新客户分配逻辑以适配新的选择流程
This commit is contained in:
@@ -49,6 +49,7 @@ export default {
|
||||
"creditMpCustomer/detail",
|
||||
"creditMpCustomer/follow-step1",
|
||||
"creditMpCustomer/edit",
|
||||
"pageUser/index",
|
||||
"mp-customer/index",
|
||||
"mp-customer/add",
|
||||
"mp-customer/detail",
|
||||
|
||||
@@ -129,10 +129,7 @@ export default function CreditCompanyPage() {
|
||||
const [selectMode, setSelectMode] = useState(false)
|
||||
const [selectedIds, setSelectedIds] = useState<number[]>([])
|
||||
|
||||
const [staffPopupVisible, setStaffPopupVisible] = useState(false)
|
||||
const [staffLoading, setStaffLoading] = useState(false)
|
||||
const [staffList, setStaffList] = useState<User[]>([])
|
||||
const [staffSelectedId, setStaffSelectedId] = useState<number | undefined>(undefined)
|
||||
const [assigning, setAssigning] = useState(false)
|
||||
|
||||
const currentUser = useMemo(() => {
|
||||
@@ -141,10 +138,10 @@ export default function CreditCompanyPage() {
|
||||
|
||||
const canAssign = useMemo(() => {
|
||||
// 超级管理员:允许分配并更改客户归属(userId)
|
||||
if (currentUser?.isSuperAdmin) return true
|
||||
if (hasRole('superAdmin')) return true
|
||||
if (currentUser?.isAdmin) return true
|
||||
if (hasRole('admin')) return true
|
||||
return false
|
||||
}, [currentUser?.isSuperAdmin])
|
||||
}, [currentUser?.isAdmin])
|
||||
|
||||
const cityOptions = useMemo(() => {
|
||||
// NutUI Address options: [{ text, value, children }]
|
||||
@@ -338,10 +335,9 @@ export default function CreditCompanyPage() {
|
||||
if (staffList.length) return staffList
|
||||
if (staffLoadingPromiseRef.current) return staffLoadingPromiseRef.current
|
||||
|
||||
setStaffLoading(true)
|
||||
const p = (async () => {
|
||||
try {
|
||||
const res = await listUsers({ isStaff: true } as any)
|
||||
const res = await listUsers({ isAdmin: true } as any)
|
||||
const arr = (res || []) as User[]
|
||||
setStaffList(arr)
|
||||
return arr
|
||||
@@ -350,7 +346,6 @@ export default function CreditCompanyPage() {
|
||||
Taro.showToast({ title: '加载员工失败', icon: 'none' })
|
||||
return []
|
||||
} finally {
|
||||
setStaffLoading(false)
|
||||
staffLoadingPromiseRef.current = null
|
||||
}
|
||||
})()
|
||||
@@ -374,19 +369,29 @@ export default function CreditCompanyPage() {
|
||||
Taro.showToast({ title: '请先勾选客户', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const staff = await ensureStaffLoaded()
|
||||
if (!staff.length) {
|
||||
Taro.showToast({ title: '暂无可分配员工', icon: 'none' })
|
||||
return
|
||||
Taro.navigateTo({
|
||||
url: '/credit/pageUser/index?isAdmin=1&title=选择业务员',
|
||||
events: {
|
||||
userSelected: (payload: any) => {
|
||||
const uid = Number(payload?.userId ?? payload?.user?.userId)
|
||||
if (!Number.isFinite(uid) || uid <= 0) return
|
||||
const u = payload?.user as User | undefined
|
||||
const pickedName = String(
|
||||
payload?.realName ||
|
||||
payload?.nickname ||
|
||||
payload?.username ||
|
||||
u?.realName ||
|
||||
u?.nickname ||
|
||||
u?.username ||
|
||||
''
|
||||
).trim()
|
||||
submitAssign(uid, pickedName).then()
|
||||
}
|
||||
setStaffPopupVisible(true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const submitAssign = async () => {
|
||||
if (!staffSelectedId) {
|
||||
Taro.showToast({ title: '请选择分配对象', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const submitAssign = async (userId: number, userName?: string) => {
|
||||
if (!selectedIds.length) {
|
||||
Taro.showToast({ title: '请先勾选客户', icon: 'none' })
|
||||
return
|
||||
@@ -394,7 +399,7 @@ export default function CreditCompanyPage() {
|
||||
|
||||
setAssigning(true)
|
||||
try {
|
||||
const staffName = staffNameMap.get(Number(staffSelectedId)) || `员工${staffSelectedId}`
|
||||
const staffName = String(userName || '').trim() || staffNameMap.get(Number(userId)) || `员工${userId}`
|
||||
const confirmRes = await Taro.showModal({
|
||||
title: '确认分配',
|
||||
content: `确定将 ${selectedIds.length} 个客户分配给「${staffName}」吗?`
|
||||
@@ -403,10 +408,9 @@ export default function CreditCompanyPage() {
|
||||
|
||||
for (const id of selectedIds) {
|
||||
const detail = await getCreditMpCustomer(Number(id))
|
||||
await updateCreditMpCustomer({ ...(detail || {}), id, userId: staffSelectedId } as any)
|
||||
await updateCreditMpCustomer({ ...(detail || {}), id, userId } as any)
|
||||
}
|
||||
Taro.showToast({ title: '分配成功', icon: 'success' })
|
||||
setStaffPopupVisible(false)
|
||||
setSelectMode(false)
|
||||
setSelectedIds([])
|
||||
await reload(true)
|
||||
@@ -702,63 +706,6 @@ export default function CreditCompanyPage() {
|
||||
</View>
|
||||
</Popup>
|
||||
|
||||
<Popup
|
||||
visible={staffPopupVisible}
|
||||
position="bottom"
|
||||
style={{ height: '65vh' }}
|
||||
onClose={() => {
|
||||
if (assigning) return
|
||||
setStaffPopupVisible(false)
|
||||
}}
|
||||
>
|
||||
<View className="p-4 flex flex-col" style={{ height: '65vh' }}>
|
||||
<View className="flex items-center justify-between mb-3">
|
||||
<Text className="text-base font-medium">选择分配对象</Text>
|
||||
<Text className="text-sm text-gray-500" onClick={() => !assigning && setStaffPopupVisible(false)}>
|
||||
关闭
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View className="flex-1 overflow-y-auto">
|
||||
{staffLoading ? (
|
||||
<View className="py-10 flex justify-center items-center text-gray-500">
|
||||
<Loading />
|
||||
<Text className="ml-2">加载中...</Text>
|
||||
</View>
|
||||
) : (
|
||||
<CellGroup>
|
||||
{staffList.map(u => (
|
||||
<Cell
|
||||
key={u.userId}
|
||||
title={
|
||||
<Text className={u.userId === staffSelectedId ? 'text-blue-600' : ''}>
|
||||
{u.realName || u.nickname || u.username || `员工${u.userId}`}
|
||||
</Text>
|
||||
}
|
||||
description={u.phone || ''}
|
||||
onClick={() => setStaffSelectedId(u.userId)}
|
||||
/>
|
||||
))}
|
||||
{!staffList.length && (
|
||||
<Cell title={<Text className="text-gray-500">暂无员工数据</Text>} />
|
||||
)}
|
||||
</CellGroup>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View className="pt-3">
|
||||
<Button
|
||||
type="primary"
|
||||
block
|
||||
disabled={assigning || !staffSelectedId}
|
||||
onClick={submitAssign}
|
||||
>
|
||||
{assigning ? '分配中...' : `确认分配(${selectedIds.length}个)`}
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</Popup>
|
||||
|
||||
</ConfigProvider>
|
||||
</View>
|
||||
)
|
||||
|
||||
139
src/credit/pageUser/index.tsx
Normal file
139
src/credit/pageUser/index.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import Taro, { useDidShow, useRouter } from '@tarojs/taro'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import { Cell, CellGroup, ConfigProvider, Empty, InfiniteLoading, Loading, PullToRefresh, SearchBar } from '@nutui/nutui-react-taro'
|
||||
|
||||
import { pageUsers } from '@/api/system/user'
|
||||
import type { User, UserParam } from '@/api/system/user/model'
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
|
||||
export default function PageUserSelectPage() {
|
||||
const { params } = useRouter()
|
||||
const isAdmin = useMemo(() => {
|
||||
const n = Number(params?.isAdmin)
|
||||
return Number.isFinite(n) ? n : undefined
|
||||
}, [params?.isAdmin])
|
||||
|
||||
const [list, setList] = useState<User[]>([])
|
||||
const [page, setPage] = useState(1)
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
const [keywords, setKeywords] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [loadingMore, setLoadingMore] = useState(false)
|
||||
|
||||
const fetchPage = useCallback(
|
||||
async (opts: { nextPage: number; replace: boolean }) => {
|
||||
try {
|
||||
if (opts.replace) setLoading(true)
|
||||
else setLoadingMore(true)
|
||||
|
||||
const res = await pageUsers({
|
||||
page: opts.nextPage,
|
||||
limit: PAGE_SIZE,
|
||||
keywords: keywords.trim() || undefined,
|
||||
isAdmin
|
||||
} as UserParam)
|
||||
|
||||
const incoming = (res?.list || []) as User[]
|
||||
const total = Number(res?.count || 0)
|
||||
setPage(opts.nextPage)
|
||||
setList(prev => {
|
||||
const nextList = opts.replace ? incoming : prev.concat(incoming)
|
||||
if (Number.isFinite(total) && total > 0) setHasMore(nextList.length < total)
|
||||
else setHasMore(incoming.length >= PAGE_SIZE)
|
||||
return nextList
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('加载业务员失败:', e)
|
||||
Taro.showToast({ title: (e as any)?.message || '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
setLoadingMore(false)
|
||||
}
|
||||
},
|
||||
[isAdmin, keywords]
|
||||
)
|
||||
|
||||
const reload = useCallback(async () => {
|
||||
await fetchPage({ nextPage: 1, replace: true })
|
||||
}, [fetchPage])
|
||||
|
||||
const loadMore = useCallback(async () => {
|
||||
if (loading || loadingMore || !hasMore) return
|
||||
await fetchPage({ nextPage: page + 1, replace: false })
|
||||
}, [fetchPage, hasMore, loading, loadingMore, page])
|
||||
|
||||
useDidShow(() => {
|
||||
Taro.setNavigationBarTitle({ title: String(params?.title || '选择业务员') })
|
||||
reload()
|
||||
})
|
||||
|
||||
const onPick = (u: User) => {
|
||||
if (!u?.userId) return
|
||||
const pageInst: any = Taro.getCurrentInstance().page
|
||||
const eventChannel = pageInst?.getOpenerEventChannel?.()
|
||||
eventChannel?.emit('userSelected', { userId: u.userId, user: u })
|
||||
Taro.navigateBack()
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen">
|
||||
<ConfigProvider>
|
||||
<View className="py-2">
|
||||
<SearchBar
|
||||
placeholder="搜索姓名/手机号"
|
||||
value={keywords}
|
||||
onChange={setKeywords}
|
||||
onSearch={reload}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<PullToRefresh onRefresh={reload} headHeight={60}>
|
||||
<View className="px-3" style={{ height: 'calc(100vh - 92px)', overflowY: 'auto' }} id="page-user-scroll">
|
||||
{list.length === 0 && !loading ? (
|
||||
<View className="flex flex-col justify-center items-center" style={{ height: 'calc(100vh - 140px)' }}>
|
||||
<Empty description="暂无数据" style={{ backgroundColor: 'transparent' }} />
|
||||
</View>
|
||||
) : (
|
||||
<InfiniteLoading
|
||||
target="page-user-scroll"
|
||||
hasMore={hasMore}
|
||||
onLoadMore={loadMore}
|
||||
loadingText={
|
||||
<View className="flex justify-center items-center py-4">
|
||||
<Loading />
|
||||
<View className="ml-2">加载中...</View>
|
||||
</View>
|
||||
}
|
||||
loadMoreText={
|
||||
<View className="text-center py-4 text-gray-500">
|
||||
{list.length === 0 ? '暂无数据' : '没有更多了'}
|
||||
</View>
|
||||
}
|
||||
>
|
||||
<CellGroup>
|
||||
{list.map(u => (
|
||||
<Cell
|
||||
key={u.userId}
|
||||
title={<Text>{u.realName || u.nickname || u.username || `用户${u.userId}`}</Text>}
|
||||
description={u.phone || u.username || ''}
|
||||
onClick={() => onPick(u)}
|
||||
/>
|
||||
))}
|
||||
</CellGroup>
|
||||
</InfiniteLoading>
|
||||
)}
|
||||
|
||||
{(loading || loadingMore) && list.length === 0 && (
|
||||
<View className="py-10 flex justify-center items-center text-gray-500">
|
||||
<Loading />
|
||||
<Text className="ml-2">加载中...</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</PullToRefresh>
|
||||
</ConfigProvider>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user