feat(credit): 添加业务员选择页面并重构客户分配功能
- 在路由配置中添加 pageUser/index 页面路径 - 移除原有的员工弹窗选择组件 - 将权限判断从 superAdmin 改为 admin - 修改员工列表加载逻辑,查询条件从 isStaff 改为 isAdmin - 实现新的业务员选择页面,支持搜索、分页和下拉刷新 - 使用页面跳转方式替代弹窗进行业务员选择 - 更新客户分配逻辑以适配新的选择流程
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
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