feat(credit): 添加业务员选择页面并重构客户分配功能

- 在路由配置中添加 pageUser/index 页面路径
- 移除原有的员工弹窗选择组件
- 将权限判断从 superAdmin 改为 admin
- 修改员工列表加载逻辑,查询条件从 isStaff 改为 isAdmin
- 实现新的业务员选择页面,支持搜索、分页和下拉刷新
- 使用页面跳转方式替代弹窗进行业务员选择
- 更新客户分配逻辑以适配新的选择流程
This commit is contained in:
2026-03-20 00:45:20 +08:00
parent d3223224e1
commit ead384344f
3 changed files with 167 additions and 80 deletions

View File

@@ -49,6 +49,7 @@ export default {
"creditMpCustomer/detail", "creditMpCustomer/detail",
"creditMpCustomer/follow-step1", "creditMpCustomer/follow-step1",
"creditMpCustomer/edit", "creditMpCustomer/edit",
"pageUser/index",
"mp-customer/index", "mp-customer/index",
"mp-customer/add", "mp-customer/add",
"mp-customer/detail", "mp-customer/detail",

View File

@@ -129,10 +129,7 @@ export default function CreditCompanyPage() {
const [selectMode, setSelectMode] = useState(false) const [selectMode, setSelectMode] = useState(false)
const [selectedIds, setSelectedIds] = useState<number[]>([]) const [selectedIds, setSelectedIds] = useState<number[]>([])
const [staffPopupVisible, setStaffPopupVisible] = useState(false)
const [staffLoading, setStaffLoading] = useState(false)
const [staffList, setStaffList] = useState<User[]>([]) const [staffList, setStaffList] = useState<User[]>([])
const [staffSelectedId, setStaffSelectedId] = useState<number | undefined>(undefined)
const [assigning, setAssigning] = useState(false) const [assigning, setAssigning] = useState(false)
const currentUser = useMemo(() => { const currentUser = useMemo(() => {
@@ -141,10 +138,10 @@ export default function CreditCompanyPage() {
const canAssign = useMemo(() => { const canAssign = useMemo(() => {
// 超级管理员允许分配并更改客户归属userId // 超级管理员允许分配并更改客户归属userId
if (currentUser?.isSuperAdmin) return true if (currentUser?.isAdmin) return true
if (hasRole('superAdmin')) return true if (hasRole('admin')) return true
return false return false
}, [currentUser?.isSuperAdmin]) }, [currentUser?.isAdmin])
const cityOptions = useMemo(() => { const cityOptions = useMemo(() => {
// NutUI Address options: [{ text, value, children }] // NutUI Address options: [{ text, value, children }]
@@ -338,10 +335,9 @@ export default function CreditCompanyPage() {
if (staffList.length) return staffList if (staffList.length) return staffList
if (staffLoadingPromiseRef.current) return staffLoadingPromiseRef.current if (staffLoadingPromiseRef.current) return staffLoadingPromiseRef.current
setStaffLoading(true)
const p = (async () => { const p = (async () => {
try { try {
const res = await listUsers({ isStaff: true } as any) const res = await listUsers({ isAdmin: true } as any)
const arr = (res || []) as User[] const arr = (res || []) as User[]
setStaffList(arr) setStaffList(arr)
return arr return arr
@@ -350,7 +346,6 @@ export default function CreditCompanyPage() {
Taro.showToast({ title: '加载员工失败', icon: 'none' }) Taro.showToast({ title: '加载员工失败', icon: 'none' })
return [] return []
} finally { } finally {
setStaffLoading(false)
staffLoadingPromiseRef.current = null staffLoadingPromiseRef.current = null
} }
})() })()
@@ -374,19 +369,29 @@ export default function CreditCompanyPage() {
Taro.showToast({ title: '请先勾选客户', icon: 'none' }) Taro.showToast({ title: '请先勾选客户', icon: 'none' })
return return
} }
const staff = await ensureStaffLoaded() Taro.navigateTo({
if (!staff.length) { url: '/credit/pageUser/index?isAdmin=1&title=选择业务员',
Taro.showToast({ title: '暂无可分配员工', icon: 'none' }) events: {
return 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 () => { const submitAssign = async (userId: number, userName?: string) => {
if (!staffSelectedId) {
Taro.showToast({ title: '请选择分配对象', icon: 'none' })
return
}
if (!selectedIds.length) { if (!selectedIds.length) {
Taro.showToast({ title: '请先勾选客户', icon: 'none' }) Taro.showToast({ title: '请先勾选客户', icon: 'none' })
return return
@@ -394,7 +399,7 @@ export default function CreditCompanyPage() {
setAssigning(true) setAssigning(true)
try { try {
const staffName = staffNameMap.get(Number(staffSelectedId)) || `员工${staffSelectedId}` const staffName = String(userName || '').trim() || staffNameMap.get(Number(userId)) || `员工${userId}`
const confirmRes = await Taro.showModal({ const confirmRes = await Taro.showModal({
title: '确认分配', title: '确认分配',
content: `确定将 ${selectedIds.length} 个客户分配给「${staffName}」吗?` content: `确定将 ${selectedIds.length} 个客户分配给「${staffName}」吗?`
@@ -403,10 +408,9 @@ export default function CreditCompanyPage() {
for (const id of selectedIds) { for (const id of selectedIds) {
const detail = await getCreditMpCustomer(Number(id)) 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' }) Taro.showToast({ title: '分配成功', icon: 'success' })
setStaffPopupVisible(false)
setSelectMode(false) setSelectMode(false)
setSelectedIds([]) setSelectedIds([])
await reload(true) await reload(true)
@@ -702,63 +706,6 @@ export default function CreditCompanyPage() {
</View> </View>
</Popup> </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> </ConfigProvider>
</View> </View>
) )

View 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>
)
}