feat(credit): 新增客户管理页面和数据查询统计功能
- 添加客户管理页面,支持搜索、筛选、查看跟进状态等功能 - 新增数据查询统计页面,包含订单完成情况、回款金额等图表展示 - 在应用配置中注册新的页面路由 - 更新导航栏标题和样式配置 - 移除订单管理页面的自定义导航栏样式 - 调整首页按钮间距和用户页面背景色
This commit is contained in:
@@ -120,6 +120,8 @@ export default {
|
|||||||
{
|
{
|
||||||
"root": "credit",
|
"root": "credit",
|
||||||
"pages": [
|
"pages": [
|
||||||
|
"data/index",
|
||||||
|
"customer/index",
|
||||||
"order/index",
|
"order/index",
|
||||||
"order/add",
|
"order/add",
|
||||||
"company/index",
|
"company/index",
|
||||||
|
|||||||
@@ -1,11 +1,654 @@
|
|||||||
|
import { useCallback, useMemo, useRef, useState } from 'react'
|
||||||
import Taro, { useDidShow } from '@tarojs/taro'
|
import Taro, { useDidShow } from '@tarojs/taro'
|
||||||
import { View, Text } from '@tarojs/components'
|
import { View, Text } from '@tarojs/components'
|
||||||
|
import {
|
||||||
|
Address,
|
||||||
|
Button,
|
||||||
|
Cell,
|
||||||
|
CellGroup,
|
||||||
|
Checkbox,
|
||||||
|
ConfigProvider,
|
||||||
|
Empty,
|
||||||
|
InfiniteLoading,
|
||||||
|
Loading,
|
||||||
|
Popup,
|
||||||
|
PullToRefresh,
|
||||||
|
SearchBar,
|
||||||
|
Tag
|
||||||
|
} from '@nutui/nutui-react-taro'
|
||||||
|
import { Copy, Phone } from '@nutui/icons-react-taro'
|
||||||
|
|
||||||
export default function index() {
|
import RegionData from '@/api/json/regions-data.json'
|
||||||
|
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'
|
||||||
|
import { hasRole } from '@/utils/permission'
|
||||||
|
|
||||||
|
const PAGE_SIZE = 10
|
||||||
|
|
||||||
|
type FollowStatus = '全部' | '未联系' | '加微前沟通' | '跟进中' | '已成交' | '无意向'
|
||||||
|
|
||||||
|
const FOLLOW_STATUS_OPTIONS: FollowStatus[] = ['全部', '未联系', '加微前沟通', '跟进中', '已成交', '无意向']
|
||||||
|
|
||||||
|
const FOLLOW_MAP_STORAGE_KEY = 'credit_company_follow_status_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 getCompanyIdKey = (c: CreditCompany) => String(c?.id || '')
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
const arr = [...splitPhones(c.tel), ...splitPhones(c.moreTel)]
|
||||||
|
return Array.from(new Set(arr))
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCompanyIndustry = (c: CreditCompany) => {
|
||||||
|
// 兼容:不同数据源字段可能不一致,优先取更具体的大类
|
||||||
|
return String(
|
||||||
|
c.nationalStandardIndustryCategories6 ||
|
||||||
|
c.nationalStandardIndustryCategories2 ||
|
||||||
|
c.nationalStandardIndustryCategories ||
|
||||||
|
c.institutionType ||
|
||||||
|
''
|
||||||
|
).trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CreditCompanyPage() {
|
||||||
|
const serverPageRef = useRef(1)
|
||||||
|
const [list, setList] = useState<CreditCompany[]>([])
|
||||||
|
const [hasMore, setHasMore] = useState(true)
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const [searchValue, setSearchValue] = useState('')
|
||||||
|
|
||||||
|
const [cityVisible, setCityVisible] = useState(false)
|
||||||
|
const [cityText, setCityText] = useState<string>('全部')
|
||||||
|
|
||||||
|
const [followVisible, setFollowVisible] = useState(false)
|
||||||
|
const [followStatus, setFollowStatus] = useState<FollowStatus>('全部')
|
||||||
|
|
||||||
|
const [industryVisible, setIndustryVisible] = useState(false)
|
||||||
|
const [industryText, setIndustryText] = useState<string>('全部')
|
||||||
|
|
||||||
|
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 [followMap, setFollowMap] = useState<Record<string, FollowStatus>>(() => {
|
||||||
|
const raw = Taro.getStorageSync(FOLLOW_MAP_STORAGE_KEY)
|
||||||
|
return safeParseJSON<Record<string, FollowStatus>>(raw) || {}
|
||||||
|
})
|
||||||
|
|
||||||
|
const currentUser = useMemo(() => {
|
||||||
|
return safeParseJSON<User>(Taro.getStorageSync('User')) || ({} as User)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const canAssign = useMemo(() => {
|
||||||
|
// 超级管理员:允许分配并更改客户归属(userId)
|
||||||
|
if (currentUser?.isSuperAdmin) return true
|
||||||
|
if (hasRole('superAdmin')) return true
|
||||||
|
return false
|
||||||
|
}, [currentUser?.isSuperAdmin])
|
||||||
|
|
||||||
|
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 staffNameMap = useMemo(() => {
|
||||||
|
const map = new Map<number, string>()
|
||||||
|
for (const u of staffList) {
|
||||||
|
if (!u?.userId) continue
|
||||||
|
const name = String(u.realName || u.nickname || u.username || `员工${u.userId}`)
|
||||||
|
map.set(u.userId, name)
|
||||||
|
}
|
||||||
|
return map
|
||||||
|
}, [staffList])
|
||||||
|
|
||||||
|
const getFollowStatus = useCallback(
|
||||||
|
(c: CreditCompany): FollowStatus => {
|
||||||
|
const k = getCompanyIdKey(c)
|
||||||
|
const stored = k ? followMap[k] : undefined
|
||||||
|
if (stored) return stored
|
||||||
|
return '未联系'
|
||||||
|
},
|
||||||
|
[followMap]
|
||||||
|
)
|
||||||
|
|
||||||
|
const setFollowStatusFor = async (c: CreditCompany) => {
|
||||||
|
if (!c?.id) return
|
||||||
|
try {
|
||||||
|
const res = await Taro.showActionSheet({
|
||||||
|
itemList: FOLLOW_STATUS_OPTIONS.filter(s => s !== '全部')
|
||||||
|
})
|
||||||
|
const next = FOLLOW_STATUS_OPTIONS.filter(s => s !== '全部')[res.tapIndex] as FollowStatus | undefined
|
||||||
|
if (!next) return
|
||||||
|
|
||||||
|
setFollowMap(prev => {
|
||||||
|
const k = getCompanyIdKey(c)
|
||||||
|
const merged = { ...prev, [k]: next }
|
||||||
|
Taro.setStorageSync(FOLLOW_MAP_STORAGE_KEY, merged)
|
||||||
|
return merged
|
||||||
|
})
|
||||||
|
|
||||||
|
// 若当前正在按状态筛选,则即时移除不匹配项,避免“改完状态但列表不变”的割裂感。
|
||||||
|
if (followStatus !== '全部' && next !== followStatus && c.id) {
|
||||||
|
const cid = Number(c.id)
|
||||||
|
setList(prev => prev.filter(x => Number(x.id) !== cid))
|
||||||
|
setSelectedIds(prev => prev.filter(id => id !== cid))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
const msg = String((e as any)?.errMsg || (e as any)?.message || e || '')
|
||||||
|
if (msg.includes('cancel')) return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const applyFilters = useCallback(
|
||||||
|
(incoming: CreditCompany[]) => {
|
||||||
|
const city = cityText === '全部' ? '' : cityText
|
||||||
|
const industry = industryText === '全部' ? '' : industryText
|
||||||
|
const follow = followStatus === '全部' ? '' : followStatus
|
||||||
|
|
||||||
|
return incoming.filter(c => {
|
||||||
|
if (c?.deleted === 1) return false
|
||||||
|
|
||||||
|
if (city) {
|
||||||
|
const full = [c.province, c.city, c.region].filter(Boolean).join(' ')
|
||||||
|
if (!full.includes(city) && String(c.city || '') !== city) return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (industry) {
|
||||||
|
const ind = getCompanyIndustry(c)
|
||||||
|
if (!ind.includes(industry)) return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (follow) {
|
||||||
|
if (getFollowStatus(c) !== follow) return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[cityText, followStatus, getFollowStatus, industryText]
|
||||||
|
)
|
||||||
|
|
||||||
|
const reload = useCallback(
|
||||||
|
async (resetPage = false) => {
|
||||||
|
if (loading) return
|
||||||
|
setLoading(true)
|
||||||
|
setError(null)
|
||||||
|
|
||||||
|
if (resetPage) {
|
||||||
|
serverPageRef.current = 1
|
||||||
|
setHasMore(true)
|
||||||
|
setSelectedIds([])
|
||||||
|
setSelectMode(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
let nextList = resetPage ? [] : list
|
||||||
|
let page = serverPageRef.current
|
||||||
|
let serverHasMore = true
|
||||||
|
|
||||||
|
// 当筛选条件较严格时,可能需要多拉几页才能拿到“至少一条匹配数据”
|
||||||
|
const MAX_PAGE_TRIES = 8
|
||||||
|
let tries = 0
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (tries < MAX_PAGE_TRIES) {
|
||||||
|
tries += 1
|
||||||
|
const params: any = {
|
||||||
|
page,
|
||||||
|
limit: PAGE_SIZE,
|
||||||
|
keywords: searchValue?.trim() || undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await pageCreditCompany(params)
|
||||||
|
const incoming = (res?.list || []) as CreditCompany[]
|
||||||
|
const filtered = applyFilters(incoming)
|
||||||
|
|
||||||
|
if (resetPage) {
|
||||||
|
nextList = filtered
|
||||||
|
resetPage = false
|
||||||
|
} else {
|
||||||
|
nextList = nextList.concat(filtered)
|
||||||
|
}
|
||||||
|
|
||||||
|
page += 1
|
||||||
|
|
||||||
|
// 服务端无更多页:终止
|
||||||
|
if (incoming.length < PAGE_SIZE) {
|
||||||
|
serverHasMore = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// 已拿到可展示数据:先返回,让用户继续滚动触发下一次加载
|
||||||
|
if (filtered.length > 0) break
|
||||||
|
}
|
||||||
|
|
||||||
|
serverPageRef.current = page
|
||||||
|
setList(nextList)
|
||||||
|
setHasMore(serverHasMore)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('加载客户列表失败:', e)
|
||||||
|
setError('加载失败,请重试')
|
||||||
|
setHasMore(false)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[applyFilters, list, loading, searchValue]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleRefresh = useCallback(async () => {
|
||||||
|
await reload(true)
|
||||||
|
}, [reload])
|
||||||
|
|
||||||
|
const loadMore = useCallback(async () => {
|
||||||
|
if (loading || !hasMore) return
|
||||||
|
await reload(false)
|
||||||
|
}, [hasMore, loading, reload])
|
||||||
|
|
||||||
|
useDidShow(() => {
|
||||||
|
reload(true).then()
|
||||||
|
// 预加载员工列表,保证“跟进人(realName)”可展示
|
||||||
|
ensureStaffLoaded().then()
|
||||||
|
})
|
||||||
|
|
||||||
|
const visibleIndustryOptions = useMemo(() => {
|
||||||
|
const uniq = new Set<string>()
|
||||||
|
for (const m of list) {
|
||||||
|
const ind = getCompanyIndustry(m)
|
||||||
|
if (ind) uniq.add(ind)
|
||||||
|
}
|
||||||
|
const arr = Array.from(uniq)
|
||||||
|
arr.sort()
|
||||||
|
return ['全部'].concat(arr)
|
||||||
|
}, [list])
|
||||||
|
|
||||||
|
const toggleSelectId = (companyId?: number, checked?: boolean) => {
|
||||||
|
if (!companyId) return
|
||||||
|
setSelectedIds(prev => {
|
||||||
|
if (checked) return prev.includes(companyId) ? prev : prev.concat(companyId)
|
||||||
|
return prev.filter(id => id !== companyId)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const copyPhones = async () => {
|
||||||
|
const pool = selectMode && selectedIds.length ? list.filter(c => selectedIds.includes(Number(c.id))) : list
|
||||||
|
const phones = pool.flatMap(c => getCompanyPhones(c))
|
||||||
|
const unique = Array.from(new Set(phones)).filter(Boolean)
|
||||||
|
if (!unique.length) {
|
||||||
|
Taro.showToast({ title: '暂无可复制的电话', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await Taro.setClipboardData({ data: unique.join('\n') })
|
||||||
|
Taro.showToast({ title: `已复制${unique.length}个电话`, icon: 'success' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const callPhone = async (phone?: string) => {
|
||||||
|
const num = String(phone || '').trim()
|
||||||
|
if (!num) {
|
||||||
|
Taro.showToast({ title: '暂无电话', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await Taro.makePhoneCall({ phoneNumber: num })
|
||||||
|
} catch (e) {
|
||||||
|
console.error('拨号失败:', e)
|
||||||
|
Taro.showToast({ title: '拨号失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openAddCustomer = () => {
|
||||||
|
Taro.navigateTo({ url: '/credit/company/add' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const ensureStaffLoaded = async () => {
|
||||||
|
if (staffLoading) return
|
||||||
|
if (staffList.length) return
|
||||||
|
setStaffLoading(true)
|
||||||
|
try {
|
||||||
|
const res = await listUsers({ isAdmin: true } as any)
|
||||||
|
setStaffList((res || []) as User[])
|
||||||
|
} catch (e) {
|
||||||
|
console.error('加载员工列表失败:', e)
|
||||||
|
Taro.showToast({ title: '加载员工失败', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
setStaffLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openAssign = async () => {
|
||||||
|
if (!canAssign) {
|
||||||
|
Taro.showToast({ title: '当前角色无分配权限', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!selectMode) {
|
||||||
|
setSelectMode(true)
|
||||||
|
setSelectedIds([])
|
||||||
|
Taro.showToast({ title: '请选择要分配的客户', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!selectedIds.length) {
|
||||||
|
Taro.showToast({ title: '请先勾选客户', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await ensureStaffLoaded()
|
||||||
|
setStaffPopupVisible(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitAssign = async () => {
|
||||||
|
if (!staffSelectedId) {
|
||||||
|
Taro.showToast({ title: '请选择分配对象', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!selectedIds.length) {
|
||||||
|
Taro.showToast({ title: '请先勾选客户', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setAssigning(true)
|
||||||
|
try {
|
||||||
|
for (const id of selectedIds) {
|
||||||
|
await updateCreditCompany({ id, userId: staffSelectedId } as any)
|
||||||
|
}
|
||||||
|
Taro.showToast({ title: '分配成功', icon: 'success' })
|
||||||
|
setStaffPopupVisible(false)
|
||||||
|
setSelectMode(false)
|
||||||
|
setSelectedIds([])
|
||||||
|
await reload(true)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('分配失败:', e)
|
||||||
|
Taro.showToast({ title: '分配失败,请重试', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
setAssigning(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className="bg-gray-50 min-h-screen">
|
<View className="bg-gray-50 min-h-screen">
|
||||||
|
<ConfigProvider>
|
||||||
|
<View className="py-2">
|
||||||
|
<SearchBar
|
||||||
|
placeholder="公司名称 / 统一代码"
|
||||||
|
value={searchValue}
|
||||||
|
onChange={setSearchValue}
|
||||||
|
onSearch={() => reload(true)}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="px-3 pb-2 flex items-center gap-2">
|
||||||
|
<Button size="small" fill="outline" onClick={() => setCityVisible(true)}>
|
||||||
|
{cityText === '全部' ? '地区' : cityText}
|
||||||
|
</Button>
|
||||||
|
<Button size="small" fill="outline" onClick={() => setFollowVisible(true)}>
|
||||||
|
{followStatus === '全部' ? '跟进状态' : followStatus}
|
||||||
|
</Button>
|
||||||
|
<Button size="small" fill="outline" onClick={() => setIndustryVisible(true)}>
|
||||||
|
{industryText === '全部' ? '行业' : industryText}
|
||||||
|
</Button>
|
||||||
|
<View className="flex-1" />
|
||||||
|
<Button size="small" fill="outline" icon={<Copy />} onClick={copyPhones}>
|
||||||
|
复制电话
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{!!error && (
|
||||||
|
<View className="px-4 pb-2 text-sm text-red-500">
|
||||||
|
{error}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<PullToRefresh onRefresh={handleRefresh} headHeight={60}>
|
||||||
|
<View className="px-3" style={{ height: 'calc(100vh - 190px)', overflowY: 'auto' }} id="credit-company-scroll">
|
||||||
|
{list.length === 0 && !loading ? (
|
||||||
|
<View className="flex flex-col justify-center items-center" style={{ height: 'calc(100vh - 240px)' }}>
|
||||||
|
<Empty description="暂无客户数据" style={{ backgroundColor: 'transparent' }} />
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<InfiniteLoading
|
||||||
|
target="credit-company-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>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{list.map((c, idx) => {
|
||||||
|
const id = Number(c.id)
|
||||||
|
const selected = !!id && selectedIds.includes(id)
|
||||||
|
const follow = getFollowStatus(c)
|
||||||
|
const ownerName =
|
||||||
|
// 兼容后端可能直接下发跟进人字段
|
||||||
|
(c as any)?.realName ||
|
||||||
|
(c as any)?.userRealName ||
|
||||||
|
(c as any)?.followRealName ||
|
||||||
|
(c.userId ? staffNameMap.get(Number(c.userId)) : undefined)
|
||||||
|
const name = c.matchName || c.name || `企业${c.id || ''}`
|
||||||
|
const industry = getCompanyIndustry(c)
|
||||||
|
const phones = getCompanyPhones(c)
|
||||||
|
const primaryPhone = phones[0]
|
||||||
|
return (
|
||||||
|
<CellGroup key={c.id || idx} className="mb-3">
|
||||||
|
<Cell
|
||||||
|
onClick={() => {
|
||||||
|
if (selectMode) return
|
||||||
|
if (!c?.id) return
|
||||||
|
Taro.navigateTo({ url: `/credit/company/detail?id=${c.id}` })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View className="flex gap-3 items-start w-full">
|
||||||
|
{selectMode && (
|
||||||
|
<View
|
||||||
|
className="pt-1"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
checked={selected}
|
||||||
|
onChange={(checked) => toggleSelectId(id, checked)}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<View className="flex-1 min-w-0">
|
||||||
|
<View className="flex items-start justify-between gap-2">
|
||||||
|
<View className="min-w-0 flex-1">
|
||||||
|
<View className="text-base font-bold text-gray-900 truncate">
|
||||||
|
{name}
|
||||||
|
</View>
|
||||||
|
{!!industry && (
|
||||||
|
<View className="text-xs text-gray-500 truncate mt-1">
|
||||||
|
{industry}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
<Tag
|
||||||
|
type={follow === '无意向' ? 'danger' : follow === '已成交' ? 'success' : 'primary'}
|
||||||
|
onClick={(e: any) => {
|
||||||
|
e?.stopPropagation?.()
|
||||||
|
setFollowStatusFor(c)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{follow}
|
||||||
|
</Tag>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="mt-2 flex items-center justify-between gap-2">
|
||||||
|
<View className="text-xs text-gray-600 truncate">
|
||||||
|
<Text className="mr-2">
|
||||||
|
{primaryPhone ? primaryPhone : '暂无电话'}
|
||||||
|
</Text>
|
||||||
|
<Text className="text-gray-500">
|
||||||
|
跟进人:{ownerName || '未分配'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
fill="outline"
|
||||||
|
icon={<Phone />}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
callPhone(primaryPhone)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
拨打
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{!!(c.province || c.city || c.region) && (
|
||||||
|
<View className="mt-2 text-xs text-gray-500 truncate">
|
||||||
|
{[c.province, c.city, c.region].filter(Boolean).join(' ')}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Cell>
|
||||||
|
</CellGroup>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</InfiniteLoading>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</PullToRefresh>
|
||||||
|
|
||||||
|
<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 items-center gap-3">
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
style={{ background: '#ef4444' }}
|
||||||
|
className="flex-1"
|
||||||
|
onClick={openAddCustomer}
|
||||||
|
>
|
||||||
|
添加客户
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
style={{ background: canAssign ? '#3b82f6' : '#94a3b8' }}
|
||||||
|
disabled={!canAssign || assigning}
|
||||||
|
className="flex-1"
|
||||||
|
onClick={openAssign}
|
||||||
|
>
|
||||||
|
{selectMode ? '分配所选' : '分配客户'}
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
{selectMode && (
|
||||||
|
<View className="mt-2 flex items-center justify-between text-xs text-gray-500">
|
||||||
|
<Text>已选 {selectedIds.length} 个</Text>
|
||||||
|
<View className="flex items-center justify-end gap-3">
|
||||||
|
<Text
|
||||||
|
className="text-blue-600"
|
||||||
|
onClick={() => copyPhones()}
|
||||||
|
>
|
||||||
|
复制所选电话
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
className="text-gray-500"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectMode(false)
|
||||||
|
setSelectedIds([])
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Address
|
||||||
|
visible={cityVisible}
|
||||||
|
options={cityOptions as any}
|
||||||
|
title="选择地区(到城市)"
|
||||||
|
onChange={(value: any[]) => {
|
||||||
|
const txt = value.filter(Boolean).slice(0, 2).join(' ')
|
||||||
|
setCityText(txt || '全部')
|
||||||
|
setCityVisible(false)
|
||||||
|
reload(true).then()
|
||||||
|
}}
|
||||||
|
onClose={() => setCityVisible(false)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Popup
|
||||||
|
visible={followVisible}
|
||||||
|
position="bottom"
|
||||||
|
style={{ height: '45vh' }}
|
||||||
|
onClose={() => setFollowVisible(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={() => setFollowVisible(false)}>
|
||||||
|
关闭
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<CellGroup>
|
||||||
|
{FOLLOW_STATUS_OPTIONS.map(s => (
|
||||||
|
<Cell
|
||||||
|
key={s}
|
||||||
|
title={<Text className={s === followStatus ? 'text-blue-600' : ''}>{s}</Text>}
|
||||||
|
onClick={() => {
|
||||||
|
setFollowStatus(s)
|
||||||
|
setFollowVisible(false)
|
||||||
|
reload(true).then()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</CellGroup>
|
||||||
|
</View>
|
||||||
|
</Popup>
|
||||||
|
|
||||||
|
</ConfigProvider>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export default definePageConfig({
|
export default definePageConfig({
|
||||||
navigationBarTitleText: '数据查询',
|
navigationBarTitleText: '数据查询统计',
|
||||||
navigationBarTextStyle: 'black',
|
navigationBarTextStyle: 'black',
|
||||||
navigationBarBackgroundColor: '#ffffff'
|
navigationBarBackgroundColor: '#ffffff'
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,11 +1,273 @@
|
|||||||
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import Taro, { useDidShow } from '@tarojs/taro'
|
import Taro, { useDidShow } from '@tarojs/taro'
|
||||||
import { View, Text } from '@tarojs/components'
|
import { View, Text } from '@tarojs/components'
|
||||||
|
import { ConfigProvider, DatePicker } from '@nutui/nutui-react-taro'
|
||||||
|
import { EChart } from 'echarts-taro3-react'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
export default function index() {
|
type RangePreset = '近6个月' | '近12个月'
|
||||||
|
type TimeMode = '当日' | '当月' | '当年'
|
||||||
|
|
||||||
|
const formatYmd = (d?: Date | null) => {
|
||||||
|
if (!d) return ''
|
||||||
|
return dayjs(d).format('YYYY-MM-DD')
|
||||||
|
}
|
||||||
|
|
||||||
|
const makeMonthLabels = (count: number) => {
|
||||||
|
const now = dayjs()
|
||||||
|
const arr: string[] = []
|
||||||
|
for (let i = count - 1; i >= 0; i--) {
|
||||||
|
arr.push(`${now.subtract(i, 'month').month() + 1}月`)
|
||||||
|
}
|
||||||
|
return arr
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildLineOption = (months: string[], series: { name: string; data: number[]; color: string }[]) => {
|
||||||
|
return {
|
||||||
|
grid: { left: 36, right: 18, top: 28, bottom: 32, containLabel: false },
|
||||||
|
tooltip: { trigger: 'axis' },
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: months,
|
||||||
|
boundaryGap: false,
|
||||||
|
axisLine: { lineStyle: { color: '#e5e7eb' } },
|
||||||
|
axisTick: { show: false },
|
||||||
|
axisLabel: { color: '#6b7280', fontSize: 10 }
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
min: 0,
|
||||||
|
max: 600000,
|
||||||
|
splitNumber: 3,
|
||||||
|
axisLine: { show: false },
|
||||||
|
axisTick: { show: false },
|
||||||
|
axisLabel: {
|
||||||
|
color: '#6b7280',
|
||||||
|
fontSize: 10,
|
||||||
|
formatter: (v: number) => (v >= 10000 ? `${Math.round(v / 10000)}w` : `${v}`)
|
||||||
|
},
|
||||||
|
splitLine: { lineStyle: { color: '#f3f4f6' } }
|
||||||
|
},
|
||||||
|
series: series.map(s => ({
|
||||||
|
name: s.name,
|
||||||
|
type: 'line',
|
||||||
|
data: s.data,
|
||||||
|
symbol: 'circle',
|
||||||
|
symbolSize: 6,
|
||||||
|
showSymbol: true,
|
||||||
|
lineStyle: { width: 2, color: s.color },
|
||||||
|
itemStyle: { color: s.color },
|
||||||
|
label: { show: true, color: s.color, fontSize: 10, position: 'top' }
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Annotation({
|
||||||
|
num,
|
||||||
|
text,
|
||||||
|
className
|
||||||
|
}: {
|
||||||
|
num: 1 | 2 | 3 | 4
|
||||||
|
text: string
|
||||||
|
className: string
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<View className="bg-gray-50 min-h-screen">
|
<View className={`absolute ${className} bg-yellow-200 border border-yellow-300 rounded-lg px-2 py-2 text-xs text-gray-900`}>
|
||||||
|
<View className="flex items-start gap-2">
|
||||||
|
<View className="w-5 h-5 rounded-full bg-yellow-300 flex items-center justify-center text-xs font-semibold text-gray-900">
|
||||||
|
<Text>{num}</Text>
|
||||||
|
</View>
|
||||||
|
<View className="flex-1">
|
||||||
|
<Text>{text}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CreditDataPage() {
|
||||||
|
|
||||||
|
const [rangePreset, setRangePreset] = useState<RangePreset>('近6个月')
|
||||||
|
const months = useMemo(() => makeMonthLabels(rangePreset === '近6个月' ? 6 : 12), [rangePreset])
|
||||||
|
|
||||||
|
const [timeMode, setTimeMode] = useState<TimeMode>('当月')
|
||||||
|
const [picking, setPicking] = useState<'start' | 'end'>('start')
|
||||||
|
const [datePickerVisible, setDatePickerVisible] = useState(false)
|
||||||
|
const [startDate, setStartDate] = useState<Date | null>(null)
|
||||||
|
const [endDate, setEndDate] = useState<Date | null>(null)
|
||||||
|
|
||||||
|
const chartRef = useRef<any>()
|
||||||
|
|
||||||
|
const chartOption = useMemo(() => {
|
||||||
|
const len = months.length
|
||||||
|
const scale = rangePreset === '近6个月' ? 1 : 0.9
|
||||||
|
|
||||||
|
// 近似还原示意图:5月附近峰值,最后一个月回落。
|
||||||
|
const mk = (base: number, peak: number) => {
|
||||||
|
const out: number[] = []
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
const t = i / Math.max(1, len - 1)
|
||||||
|
const wave = Math.sin(t * Math.PI) * peak
|
||||||
|
out.push(Math.round((base + wave) * scale))
|
||||||
|
}
|
||||||
|
if (out.length) out[out.length - 1] = Math.round(out[out.length - 1] * 0.55)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
const series = [
|
||||||
|
{ name: '订单完成情况', data: mk(180000, 220000), color: '#ec4899' },
|
||||||
|
{ name: '回款金额情况', data: mk(120000, 160000), color: '#22c55e' },
|
||||||
|
{ name: '提成获取情况', data: mk(90000, 120000), color: '#3b82f6' }
|
||||||
|
]
|
||||||
|
return buildLineOption(months, series)
|
||||||
|
}, [months, rangePreset])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (chartRef.current) chartRef.current.refresh(chartOption)
|
||||||
|
}, [chartOption])
|
||||||
|
|
||||||
|
useDidShow(() => {
|
||||||
|
if (chartRef.current) chartRef.current.refresh(chartOption)
|
||||||
|
})
|
||||||
|
|
||||||
|
const pickRangePreset = async () => {
|
||||||
|
try {
|
||||||
|
const options: RangePreset[] = ['近6个月', '近12个月']
|
||||||
|
const res = await Taro.showActionSheet({ itemList: options })
|
||||||
|
const next = options[res.tapIndex]
|
||||||
|
if (next) setRangePreset(next)
|
||||||
|
} catch (e) {
|
||||||
|
const msg = String((e as any)?.errMsg || (e as any)?.message || e || '')
|
||||||
|
if (msg.includes('cancel')) return
|
||||||
|
console.error('选择时间范围失败:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const followSteps = useMemo(() => {
|
||||||
|
return [
|
||||||
|
{ name: '跟进第一步', count: 50 },
|
||||||
|
{ name: '跟进第二步', count: 20 },
|
||||||
|
{ name: '跟进第三步', count: 20 },
|
||||||
|
{ name: '跟进第四步', count: 20 },
|
||||||
|
{ name: '跟进第五步', count: 20 },
|
||||||
|
{ name: '跟进第六步', count: 20 }
|
||||||
|
]
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className="bg-pink-50 min-h-screen">
|
||||||
|
<ConfigProvider>
|
||||||
|
<View className="max-w-md mx-auto px-4 pb-24">
|
||||||
|
<View className="bg-white rounded-xl border border-pink-100 p-4 relative">
|
||||||
|
<View className="flex items-center justify-between">
|
||||||
|
<Text className="text-sm font-semibold text-gray-900">数据查询统计</Text>
|
||||||
|
<View
|
||||||
|
className="text-xs text-gray-600 border border-gray-200 rounded-full px-3 py-1"
|
||||||
|
onClick={pickRangePreset}
|
||||||
|
>
|
||||||
|
<Text>{rangePreset}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="mt-3 relative" style={{ height: '260px' }}>
|
||||||
|
<EChart ref={chartRef} canvasId="credit-data-line-canvas" />
|
||||||
|
|
||||||
|
<Annotation
|
||||||
|
num={1}
|
||||||
|
text="编号1:订单完成情况(粉色折线)"
|
||||||
|
className="right-2 top-2 w-40"
|
||||||
|
/>
|
||||||
|
<Annotation
|
||||||
|
num={2}
|
||||||
|
text="编号2:回款金额情况(绿色折线)"
|
||||||
|
className="right-2 top-24 w-40"
|
||||||
|
/>
|
||||||
|
<Annotation
|
||||||
|
num={3}
|
||||||
|
text="编号3:提成获取情况(蓝色折线)"
|
||||||
|
className="right-2 top-44 w-40"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="mt-4 bg-white rounded-xl border border-pink-100 p-4 relative">
|
||||||
|
<View className="flex items-center justify-between">
|
||||||
|
<Text className="text-sm font-semibold text-gray-900">跟进统计数据</Text>
|
||||||
|
<View className="flex items-center gap-2 text-xs">
|
||||||
|
{(['当日', '当月', '当年'] as TimeMode[]).map(m => (
|
||||||
|
<View
|
||||||
|
key={m}
|
||||||
|
className={`px-3 py-1 rounded-full border ${timeMode === m ? 'bg-pink-500 border-pink-500' : 'bg-white border-gray-200'}`}
|
||||||
|
onClick={() => setTimeMode(m)}
|
||||||
|
>
|
||||||
|
<Text className={timeMode === m ? 'text-white' : 'text-gray-700'}>{m}</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="mt-3 flex items-center gap-3">
|
||||||
|
<View
|
||||||
|
className="flex-1 border border-gray-200 rounded-lg px-3 py-2 text-xs text-gray-600"
|
||||||
|
onClick={() => {
|
||||||
|
setPicking('start')
|
||||||
|
setDatePickerVisible(true)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text>{formatYmd(startDate) || '请选择开始时间'}</Text>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
className="flex-1 border border-gray-200 rounded-lg px-3 py-2 text-xs text-gray-600"
|
||||||
|
onClick={() => {
|
||||||
|
setPicking('end')
|
||||||
|
setDatePickerVisible(true)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text>{formatYmd(endDate) || '请选择结束时间'}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="mt-4">
|
||||||
|
{followSteps.map(s => (
|
||||||
|
<View key={s.name} className="flex items-center justify-between py-2 border-b border-gray-50">
|
||||||
|
<Text className="text-sm text-gray-800">{s.name}</Text>
|
||||||
|
<Text className="text-sm text-gray-600">{s.count}个</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Annotation
|
||||||
|
num={4}
|
||||||
|
text="编号4:跟进日期选择(开始/结束)会导致统计数量变化"
|
||||||
|
className="right-2 top-24 w-44"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<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)
|
||||||
|
}
|
||||||
|
setDatePickerVisible(false)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ConfigProvider>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
export default definePageConfig({
|
export default definePageConfig({
|
||||||
navigationBarTitleText: '订单管理',
|
navigationBarTitleText: '订单管理',
|
||||||
navigationBarTextStyle: 'black',
|
navigationBarTextStyle: 'black',
|
||||||
navigationBarBackgroundColor: '#ffffff',
|
navigationBarBackgroundColor: '#ffffff'
|
||||||
navigationStyle: 'custom'
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useMemo, useState } from 'react'
|
|||||||
import Taro from '@tarojs/taro'
|
import Taro from '@tarojs/taro'
|
||||||
import { View, Text } from '@tarojs/components'
|
import { View, Text } from '@tarojs/components'
|
||||||
import { Button, ConfigProvider, DatePicker, Empty, Input, Popup } 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 { Search } from '@nutui/icons-react-taro'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
type PayStatus = '全部回款' | '部分回款' | '未回款'
|
type PayStatus = '全部回款' | '部分回款' | '未回款'
|
||||||
@@ -79,14 +79,6 @@ const makeMockOrders = (page: number): OrderItem[] => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function CreditOrderPage() {
|
export default function CreditOrderPage() {
|
||||||
const statusBarHeight = useMemo(() => {
|
|
||||||
try {
|
|
||||||
const info = Taro.getSystemInfoSync()
|
|
||||||
return Number(info?.statusBarHeight || 0)
|
|
||||||
} catch (_e) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const [rawList, setRawList] = useState<OrderItem[]>(() => makeMockOrders(1))
|
const [rawList, setRawList] = useState<OrderItem[]>(() => makeMockOrders(1))
|
||||||
const [mockPage, setMockPage] = useState(1)
|
const [mockPage, setMockPage] = useState(1)
|
||||||
@@ -193,62 +185,10 @@ export default function CreditOrderPage() {
|
|||||||
Taro.showToast({ title: '已加载更多', icon: 'none' })
|
Taro.showToast({ title: '已加载更多', icon: 'none' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const headerOffset = statusBarHeight + 80
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className="bg-pink-50 min-h-screen">
|
<View className="bg-pink-50 min-h-screen">
|
||||||
<ConfigProvider>
|
<ConfigProvider>
|
||||||
<View className="fixed z-50 top-0 left-0 right-0 bg-pink-50" style={{ paddingTop: `${statusBarHeight}px` }}>
|
<View className="max-w-md mx-auto">
|
||||||
<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
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text>...</Text>
|
|
||||||
</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' })}
|
|
||||||
>
|
|
||||||
<Setting size={14} />
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={{ paddingTop: `${headerOffset}px` }} className="max-w-md mx-auto">
|
|
||||||
<View className="px-4 pt-2">
|
<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">
|
<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" />
|
<Search size={16} className="text-gray-400" />
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ function Home() {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
{admin && (
|
{admin && (
|
||||||
<View className='ctaWrap gap-2'>
|
<View className='ctaWrap gap-3'>
|
||||||
<View className='ctaBtn' onClick={() => navTo('/credit/company/index', true)}>
|
<View className='ctaBtn' onClick={() => navTo('/credit/company/index', true)}>
|
||||||
<Text className='ctaBtnText'>客户管理</Text>
|
<Text className='ctaBtnText'>客户管理</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ function User() {
|
|||||||
// 仅覆盖个人中心页顶部背景为红色(不影响全局主题)
|
// 仅覆盖个人中心页顶部背景为红色(不影响全局主题)
|
||||||
const pagePrimaryBackground = {
|
const pagePrimaryBackground = {
|
||||||
...themeStyles.primaryBackground,
|
...themeStyles.primaryBackground,
|
||||||
background: '#ff0000'
|
background: '#cf1313'
|
||||||
}
|
}
|
||||||
// TabBar 页在小程序里通常不会销毁;从“注册/申请”页返回时需要触发子组件重新初始化/拉取最新状态。
|
// TabBar 页在小程序里通常不会销毁;从“注册/申请”页返回时需要触发子组件重新初始化/拉取最新状态。
|
||||||
const [dealerViewKey, setDealerViewKey] = useState(0)
|
const [dealerViewKey, setDealerViewKey] = useState(0)
|
||||||
|
|||||||
Reference in New Issue
Block a user