feat(credit): 客户详情页附件功能优化及界面调整
- 添加附件ID生成函数makeAttachmentId用于唯一标识附件 - 实现getUploadedUrl函数统一处理各种URL格式的附件获取 - 创建normalizeAttachments函数去重并标准化附件数据结构 - 更新parseFilesToAttachments函数整合附件标准化逻辑 - 设置最大附件数量限制为30个并添加相应提示 - 优化图片和文件上传逻辑增加错误处理和进度控制 - 移除自定义导航栏样式简化follow-step1页面头部 - 将拨打电话按钮改为客户跟进功能跳转到跟进页面 - 注释掉未使用的Phone图标导入和相关拨打功能
This commit is contained in:
@@ -75,6 +75,8 @@ type Attachment = {
|
|||||||
thumbnail?: string
|
thumbnail?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const makeAttachmentId = () => `${Date.now()}-${Math.random().toString(16).slice(2)}`
|
||||||
|
|
||||||
const isHttpUrl = (url?: string) => {
|
const isHttpUrl = (url?: string) => {
|
||||||
if (!url) return false
|
if (!url) return false
|
||||||
return /^https?:\/\//i.test(url)
|
return /^https?:\/\//i.test(url)
|
||||||
@@ -149,31 +151,56 @@ const loadFollowHistoryIds = (customerId: number): number[] => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getUploadedUrl = (record: any) => {
|
||||||
|
if (!record) return ''
|
||||||
|
if (typeof record === 'string') return String(record).trim()
|
||||||
|
return String(record?.url || record?.downloadUrl || record?.thumbnail || record?.path || '').trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeAttachments = (arr: Attachment[]) => {
|
||||||
|
const seen = new Set<string>()
|
||||||
|
const out: Attachment[] = []
|
||||||
|
for (const a of arr) {
|
||||||
|
const url = String(a?.url || '').trim()
|
||||||
|
if (!url) continue
|
||||||
|
if (seen.has(url)) continue
|
||||||
|
seen.add(url)
|
||||||
|
out.push({
|
||||||
|
...a,
|
||||||
|
id: a?.id || makeAttachmentId(),
|
||||||
|
name: String(a?.name || guessNameFromUrl(url, '附件')).trim() || guessNameFromUrl(url, '附件'),
|
||||||
|
url
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
const parseFilesToAttachments = (raw?: any): Attachment[] => {
|
const parseFilesToAttachments = (raw?: any): Attachment[] => {
|
||||||
const txt = String(raw || '').trim()
|
const txt = String(raw || '').trim()
|
||||||
if (!txt) return []
|
if (!txt) return []
|
||||||
|
|
||||||
const parsed = safeParseJSON<any>(txt)
|
const parsed = safeParseJSON<any>(txt)
|
||||||
const nowId = () => `${Date.now()}-${Math.random().toString(16).slice(2)}`
|
|
||||||
|
|
||||||
if (Array.isArray(parsed)) {
|
if (Array.isArray(parsed)) {
|
||||||
return parsed
|
return normalizeAttachments(
|
||||||
|
parsed
|
||||||
.map((it, idx) => {
|
.map((it, idx) => {
|
||||||
if (!it) return null
|
if (!it) return null
|
||||||
if (typeof it === 'string') {
|
if (typeof it === 'string') {
|
||||||
const url = String(it).trim()
|
const url = String(it).trim()
|
||||||
if (!url) return null
|
if (!url) return null
|
||||||
const name = guessNameFromUrl(url, `附件${idx + 1}`)
|
const name = guessNameFromUrl(url, `附件${idx + 1}`)
|
||||||
return { id: nowId(), name, url, isImage: isImageUrl(url) } as Attachment
|
return { id: makeAttachmentId(), name, url, isImage: isImageUrl(url) } as Attachment
|
||||||
}
|
}
|
||||||
const url = String(it?.url || it?.downloadUrl || it?.thumbnail || it?.path || '').trim()
|
const url = String(it?.url || it?.downloadUrl || it?.thumbnail || it?.path || '').trim()
|
||||||
if (!url) return null
|
if (!url) return null
|
||||||
const name = String(it?.name || guessNameFromUrl(url, `附件${idx + 1}`)).trim()
|
const name = String(it?.name || guessNameFromUrl(url, `附件${idx + 1}`)).trim()
|
||||||
const thumbnail = it?.thumbnail ? String(it.thumbnail) : undefined
|
const thumbnail = it?.thumbnail ? String(it.thumbnail) : undefined
|
||||||
const isImage = it?.isImage !== undefined ? !!it.isImage : isImageUrl(url)
|
const isImage = it?.isImage !== undefined ? !!it.isImage : isImageUrl(url)
|
||||||
return { id: nowId(), name, url, thumbnail, isImage } as Attachment
|
return { id: makeAttachmentId(), name, url, thumbnail, isImage } as Attachment
|
||||||
})
|
})
|
||||||
.filter(Boolean) as Attachment[]
|
.filter(Boolean) as Attachment[]
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 非 JSON:尝试按分隔符拆分成多条 url
|
// 非 JSON:尝试按分隔符拆分成多条 url
|
||||||
@@ -182,12 +209,14 @@ const parseFilesToAttachments = (raw?: any): Attachment[] => {
|
|||||||
.map(s => s.trim())
|
.map(s => s.trim())
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
if (!parts.length) return []
|
if (!parts.length) return []
|
||||||
return parts.map((url, idx) => ({
|
return normalizeAttachments(
|
||||||
id: nowId(),
|
parts.map((url, idx) => ({
|
||||||
url,
|
id: makeAttachmentId(),
|
||||||
name: guessNameFromUrl(url, `附件${idx + 1}`),
|
url,
|
||||||
isImage: isImageUrl(url)
|
name: guessNameFromUrl(url, `附件${idx + 1}`),
|
||||||
}))
|
isImage: isImageUrl(url)
|
||||||
|
}))
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CreditMpCustomerDetailPage() {
|
export default function CreditMpCustomerDetailPage() {
|
||||||
@@ -276,21 +305,18 @@ export default function CreditMpCustomerDetailPage() {
|
|||||||
const imageAttachments = useMemo(() => attachments.filter(a => a.isImage), [attachments])
|
const imageAttachments = useMemo(() => attachments.filter(a => a.isImage), [attachments])
|
||||||
const fileAttachments = useMemo(() => attachments.filter(a => !a.isImage), [attachments])
|
const fileAttachments = useMemo(() => attachments.filter(a => !a.isImage), [attachments])
|
||||||
|
|
||||||
|
const maxAttachments = 30
|
||||||
|
|
||||||
const saveAttachments = useCallback(
|
const saveAttachments = useCallback(
|
||||||
async (next: Attachment[]) => {
|
async (next: Attachment[]) => {
|
||||||
if (!rowId || !row) return
|
if (!rowId || !row) return
|
||||||
if (savingFiles) return
|
if (savingFiles) return
|
||||||
setSavingFiles(true)
|
setSavingFiles(true)
|
||||||
try {
|
try {
|
||||||
const filesPayload = next.map(a => ({
|
const normalized = normalizeAttachments(next).slice(0, maxAttachments)
|
||||||
name: a.name,
|
const nextFiles = normalized.length ? JSON.stringify(normalized.map(a => ({ name: a.name, url: a.url, thumbnail: a.thumbnail, isImage: a.isImage }))) : undefined
|
||||||
url: a.url,
|
|
||||||
thumbnail: a.thumbnail,
|
|
||||||
isImage: a.isImage
|
|
||||||
}))
|
|
||||||
const nextFiles = filesPayload.length ? JSON.stringify(filesPayload) : undefined
|
|
||||||
await updateCreditMpCustomer({ ...(row as any), id: rowId, files: nextFiles } as any)
|
await updateCreditMpCustomer({ ...(row as any), id: rowId, files: nextFiles } as any)
|
||||||
setAttachments(next)
|
setAttachments(normalized)
|
||||||
setRow(prev => (prev ? ({ ...prev, files: nextFiles } as any) : prev))
|
setRow(prev => (prev ? ({ ...prev, files: nextFiles } as any) : prev))
|
||||||
Taro.showToast({ title: '已更新', icon: 'success' })
|
Taro.showToast({ title: '已更新', icon: 'success' })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -332,10 +358,15 @@ export default function CreditMpCustomerDetailPage() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const chooseAndUploadImages = useCallback(async () => {
|
const chooseAndUploadImages = useCallback(async () => {
|
||||||
|
if (uploading || savingFiles) return
|
||||||
|
if (attachments.length >= maxAttachments) {
|
||||||
|
Taro.showToast({ title: `附件已达上限(${maxAttachments})`, icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
let res: any
|
let res: any
|
||||||
try {
|
try {
|
||||||
res = await Taro.chooseImage({
|
res = await Taro.chooseImage({
|
||||||
count: 9,
|
count: Math.min(9, maxAttachments - attachments.length),
|
||||||
sizeType: ['compressed'],
|
sizeType: ['compressed'],
|
||||||
sourceType: ['album', 'camera']
|
sourceType: ['album', 'camera']
|
||||||
})
|
})
|
||||||
@@ -353,29 +384,41 @@ export default function CreditMpCustomerDetailPage() {
|
|||||||
try {
|
try {
|
||||||
const uploaded: Attachment[] = []
|
const uploaded: Attachment[] = []
|
||||||
for (const p of tempFilePaths) {
|
for (const p of tempFilePaths) {
|
||||||
const record: any = await uploadFileByPath(p)
|
try {
|
||||||
const url = String(record?.url || record?.downloadUrl || record?.thumbnail || record?.path || '').trim()
|
const record: any = await uploadFileByPath(p)
|
||||||
if (!url) continue
|
const url = getUploadedUrl(record)
|
||||||
uploaded.push({
|
if (!url) continue
|
||||||
id: `${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
uploaded.push({
|
||||||
name: String(record?.name || guessNameFromUrl(url, '图片')).trim(),
|
id: makeAttachmentId(),
|
||||||
url,
|
name: String(record?.name || guessNameFromUrl(url, '图片')).trim(),
|
||||||
thumbnail: record?.thumbnail ? String(record.thumbnail) : undefined,
|
url,
|
||||||
isImage: true
|
thumbnail: record?.thumbnail ? String(record.thumbnail) : undefined,
|
||||||
})
|
isImage: true
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.error('上传图片失败:', e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!uploaded.length) return
|
if (!uploaded.length) return
|
||||||
await saveAttachments(attachments.concat(uploaded))
|
await saveAttachments(attachments.concat(uploaded))
|
||||||
|
} catch (e) {
|
||||||
|
console.error('上传图片失败:', e)
|
||||||
|
Taro.showToast({ title: (e as any)?.message || '上传失败', icon: 'none' })
|
||||||
} finally {
|
} finally {
|
||||||
setUploading(false)
|
setUploading(false)
|
||||||
}
|
}
|
||||||
}, [attachments, saveAttachments])
|
}, [attachments, maxAttachments, saveAttachments, savingFiles, uploading])
|
||||||
|
|
||||||
const chooseAndUploadFiles = useCallback(async () => {
|
const chooseAndUploadFiles = useCallback(async () => {
|
||||||
|
if (uploading || savingFiles) return
|
||||||
|
if (attachments.length >= maxAttachments) {
|
||||||
|
Taro.showToast({ title: `附件已达上限(${maxAttachments})`, icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
let res: any
|
let res: any
|
||||||
try {
|
try {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
res = await Taro.chooseMessageFile({ count: 9, type: 'file' })
|
res = await Taro.chooseMessageFile({ count: Math.min(9, maxAttachments - attachments.length), type: 'file' })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const msg = String((e as any)?.errMsg || (e as any)?.message || e || '')
|
const msg = String((e as any)?.errMsg || (e as any)?.message || e || '')
|
||||||
if (msg.includes('cancel')) return
|
if (msg.includes('cancel')) return
|
||||||
@@ -395,23 +438,30 @@ export default function CreditMpCustomerDetailPage() {
|
|||||||
try {
|
try {
|
||||||
const uploaded: Attachment[] = []
|
const uploaded: Attachment[] = []
|
||||||
for (const f of picked) {
|
for (const f of picked) {
|
||||||
const record: any = await uploadFileByPath(f.path)
|
try {
|
||||||
const url = String(record?.url || record?.downloadUrl || record?.thumbnail || record?.path || '').trim()
|
const record: any = await uploadFileByPath(f.path)
|
||||||
if (!url) continue
|
const url = getUploadedUrl(record)
|
||||||
uploaded.push({
|
if (!url) continue
|
||||||
id: `${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
uploaded.push({
|
||||||
name: String(f?.name || record?.name || guessNameFromUrl(url, '文件')).trim(),
|
id: makeAttachmentId(),
|
||||||
url,
|
name: String(f?.name || record?.name || guessNameFromUrl(url, '文件')).trim(),
|
||||||
thumbnail: record?.thumbnail ? String(record.thumbnail) : undefined,
|
url,
|
||||||
isImage: false
|
thumbnail: record?.thumbnail ? String(record.thumbnail) : undefined,
|
||||||
})
|
isImage: false
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.error('上传文件失败:', e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!uploaded.length) return
|
if (!uploaded.length) return
|
||||||
await saveAttachments(attachments.concat(uploaded))
|
await saveAttachments(attachments.concat(uploaded))
|
||||||
|
} catch (e) {
|
||||||
|
console.error('上传文件失败:', e)
|
||||||
|
Taro.showToast({ title: (e as any)?.message || '上传失败', icon: 'none' })
|
||||||
} finally {
|
} finally {
|
||||||
setUploading(false)
|
setUploading(false)
|
||||||
}
|
}
|
||||||
}, [attachments, saveAttachments])
|
}, [attachments, maxAttachments, saveAttachments, savingFiles, uploading])
|
||||||
|
|
||||||
const onAttachmentAction = useCallback(
|
const onAttachmentAction = useCallback(
|
||||||
async (a: Attachment) => {
|
async (a: Attachment) => {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
export default definePageConfig({
|
export default definePageConfig({
|
||||||
navigationBarTitleText: '客户跟进',
|
navigationBarTitleText: '客户跟进',
|
||||||
navigationBarTextStyle: 'black',
|
navigationBarTextStyle: 'black',
|
||||||
navigationBarBackgroundColor: '#ffffff',
|
navigationBarBackgroundColor: '#ffffff'
|
||||||
navigationStyle: 'custom'
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { useCallback, useMemo, useState } from 'react'
|
|||||||
import Taro, { useDidShow, useRouter } from '@tarojs/taro'
|
import Taro, { useDidShow, useRouter } from '@tarojs/taro'
|
||||||
import { View, Text } from '@tarojs/components'
|
import { View, Text } from '@tarojs/components'
|
||||||
import { Button, ConfigProvider, Empty, Loading, TextArea } from '@nutui/nutui-react-taro'
|
import { Button, ConfigProvider, Empty, Loading, TextArea } from '@nutui/nutui-react-taro'
|
||||||
import { Setting } from '@nutui/icons-react-taro'
|
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
import { getCreditMpCustomer, updateCreditMpCustomer } from '@/api/credit/creditMpCustomer'
|
import { getCreditMpCustomer, updateCreditMpCustomer } from '@/api/credit/creditMpCustomer'
|
||||||
@@ -153,6 +152,7 @@ export default function CreditMpCustomerFollowStep1Page() {
|
|||||||
return Number.isFinite(id) && id > 0 ? id : undefined
|
return Number.isFinite(id) && id > 0 ? id : undefined
|
||||||
}, [router?.params])
|
}, [router?.params])
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
const statusBarHeight = useMemo(() => {
|
const statusBarHeight = useMemo(() => {
|
||||||
try {
|
try {
|
||||||
const info = Taro.getSystemInfoSync()
|
const info = Taro.getSystemInfoSync()
|
||||||
@@ -465,48 +465,11 @@ export default function CreditMpCustomerFollowStep1Page() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const headerOffset = statusBarHeight + 80
|
const headerOffset = 12
|
||||||
|
|
||||||
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="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) reload()
|
|
||||||
} 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">
|
|
||||||
<Setting size={14} />
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={{ paddingTop: `${headerOffset}px` }} className="max-w-md mx-auto px-4 pb-28">
|
<View style={{ paddingTop: `${headerOffset}px` }} className="max-w-md mx-auto px-4 pb-28">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<View className="py-16 flex justify-center items-center text-gray-500">
|
<View className="py-16 flex justify-center items-center text-gray-500">
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
SearchBar,
|
SearchBar,
|
||||||
Tag
|
Tag
|
||||||
} from '@nutui/nutui-react-taro'
|
} from '@nutui/nutui-react-taro'
|
||||||
import { Phone } from '@nutui/icons-react-taro'
|
// import { Phone } from '@nutui/icons-react-taro'
|
||||||
|
|
||||||
import RegionData from '@/api/json/regions-data.json'
|
import RegionData from '@/api/json/regions-data.json'
|
||||||
import { getCreditMpCustomer, pageCreditMpCustomer, updateCreditMpCustomer } from '@/api/credit/creditMpCustomer'
|
import { getCreditMpCustomer, pageCreditMpCustomer, updateCreditMpCustomer } from '@/api/credit/creditMpCustomer'
|
||||||
@@ -382,6 +382,7 @@ export default function CreditCompanyPage() {
|
|||||||
Taro.showToast({ title: `已复制${unique.length}个电话`, icon: 'success' })
|
Taro.showToast({ title: `已复制${unique.length}个电话`, icon: 'success' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
const callPhone = async (phone?: string) => {
|
const callPhone = async (phone?: string) => {
|
||||||
const num = String(phone || '').trim()
|
const num = String(phone || '').trim()
|
||||||
if (!num) {
|
if (!num) {
|
||||||
@@ -637,13 +638,11 @@ export default function CreditCompanyPage() {
|
|||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
fill="outline"
|
fill="outline"
|
||||||
icon={<Phone />}
|
onClick={() => {
|
||||||
onClick={(e) => {
|
Taro.navigateTo({url: `/credit/mp-customer/follow-step1?id=${c.id}`})
|
||||||
e.stopPropagation()
|
|
||||||
callPhone(primaryPhone)
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
拨打
|
跟进
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
Reference in New Issue
Block a user