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
|
||||
}
|
||||
|
||||
const makeAttachmentId = () => `${Date.now()}-${Math.random().toString(16).slice(2)}`
|
||||
|
||||
const isHttpUrl = (url?: string) => {
|
||||
if (!url) return false
|
||||
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 txt = String(raw || '').trim()
|
||||
if (!txt) return []
|
||||
|
||||
const parsed = safeParseJSON<any>(txt)
|
||||
const nowId = () => `${Date.now()}-${Math.random().toString(16).slice(2)}`
|
||||
|
||||
if (Array.isArray(parsed)) {
|
||||
return parsed
|
||||
return normalizeAttachments(
|
||||
parsed
|
||||
.map((it, idx) => {
|
||||
if (!it) return null
|
||||
if (typeof it === 'string') {
|
||||
const url = String(it).trim()
|
||||
if (!url) return null
|
||||
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()
|
||||
if (!url) return null
|
||||
const name = String(it?.name || guessNameFromUrl(url, `附件${idx + 1}`)).trim()
|
||||
const thumbnail = it?.thumbnail ? String(it.thumbnail) : undefined
|
||||
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[]
|
||||
)
|
||||
}
|
||||
|
||||
// 非 JSON:尝试按分隔符拆分成多条 url
|
||||
@@ -182,12 +209,14 @@ const parseFilesToAttachments = (raw?: any): Attachment[] => {
|
||||
.map(s => s.trim())
|
||||
.filter(Boolean)
|
||||
if (!parts.length) return []
|
||||
return parts.map((url, idx) => ({
|
||||
id: nowId(),
|
||||
url,
|
||||
name: guessNameFromUrl(url, `附件${idx + 1}`),
|
||||
isImage: isImageUrl(url)
|
||||
}))
|
||||
return normalizeAttachments(
|
||||
parts.map((url, idx) => ({
|
||||
id: makeAttachmentId(),
|
||||
url,
|
||||
name: guessNameFromUrl(url, `附件${idx + 1}`),
|
||||
isImage: isImageUrl(url)
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
export default function CreditMpCustomerDetailPage() {
|
||||
@@ -276,21 +305,18 @@ export default function CreditMpCustomerDetailPage() {
|
||||
const imageAttachments = useMemo(() => attachments.filter(a => a.isImage), [attachments])
|
||||
const fileAttachments = useMemo(() => attachments.filter(a => !a.isImage), [attachments])
|
||||
|
||||
const maxAttachments = 30
|
||||
|
||||
const saveAttachments = useCallback(
|
||||
async (next: Attachment[]) => {
|
||||
if (!rowId || !row) return
|
||||
if (savingFiles) return
|
||||
setSavingFiles(true)
|
||||
try {
|
||||
const filesPayload = next.map(a => ({
|
||||
name: a.name,
|
||||
url: a.url,
|
||||
thumbnail: a.thumbnail,
|
||||
isImage: a.isImage
|
||||
}))
|
||||
const nextFiles = filesPayload.length ? JSON.stringify(filesPayload) : undefined
|
||||
const normalized = normalizeAttachments(next).slice(0, maxAttachments)
|
||||
const nextFiles = normalized.length ? JSON.stringify(normalized.map(a => ({ name: a.name, url: a.url, thumbnail: a.thumbnail, isImage: a.isImage }))) : undefined
|
||||
await updateCreditMpCustomer({ ...(row as any), id: rowId, files: nextFiles } as any)
|
||||
setAttachments(next)
|
||||
setAttachments(normalized)
|
||||
setRow(prev => (prev ? ({ ...prev, files: nextFiles } as any) : prev))
|
||||
Taro.showToast({ title: '已更新', icon: 'success' })
|
||||
} catch (e) {
|
||||
@@ -332,10 +358,15 @@ export default function CreditMpCustomerDetailPage() {
|
||||
)
|
||||
|
||||
const chooseAndUploadImages = useCallback(async () => {
|
||||
if (uploading || savingFiles) return
|
||||
if (attachments.length >= maxAttachments) {
|
||||
Taro.showToast({ title: `附件已达上限(${maxAttachments})`, icon: 'none' })
|
||||
return
|
||||
}
|
||||
let res: any
|
||||
try {
|
||||
res = await Taro.chooseImage({
|
||||
count: 9,
|
||||
count: Math.min(9, maxAttachments - attachments.length),
|
||||
sizeType: ['compressed'],
|
||||
sourceType: ['album', 'camera']
|
||||
})
|
||||
@@ -353,29 +384,41 @@ export default function CreditMpCustomerDetailPage() {
|
||||
try {
|
||||
const uploaded: Attachment[] = []
|
||||
for (const p of tempFilePaths) {
|
||||
const record: any = await uploadFileByPath(p)
|
||||
const url = String(record?.url || record?.downloadUrl || record?.thumbnail || record?.path || '').trim()
|
||||
if (!url) continue
|
||||
uploaded.push({
|
||||
id: `${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
||||
name: String(record?.name || guessNameFromUrl(url, '图片')).trim(),
|
||||
url,
|
||||
thumbnail: record?.thumbnail ? String(record.thumbnail) : undefined,
|
||||
isImage: true
|
||||
})
|
||||
try {
|
||||
const record: any = await uploadFileByPath(p)
|
||||
const url = getUploadedUrl(record)
|
||||
if (!url) continue
|
||||
uploaded.push({
|
||||
id: makeAttachmentId(),
|
||||
name: String(record?.name || guessNameFromUrl(url, '图片')).trim(),
|
||||
url,
|
||||
thumbnail: record?.thumbnail ? String(record.thumbnail) : undefined,
|
||||
isImage: true
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('上传图片失败:', e)
|
||||
}
|
||||
}
|
||||
if (!uploaded.length) return
|
||||
await saveAttachments(attachments.concat(uploaded))
|
||||
} catch (e) {
|
||||
console.error('上传图片失败:', e)
|
||||
Taro.showToast({ title: (e as any)?.message || '上传失败', icon: 'none' })
|
||||
} finally {
|
||||
setUploading(false)
|
||||
}
|
||||
}, [attachments, saveAttachments])
|
||||
}, [attachments, maxAttachments, saveAttachments, savingFiles, uploading])
|
||||
|
||||
const chooseAndUploadFiles = useCallback(async () => {
|
||||
if (uploading || savingFiles) return
|
||||
if (attachments.length >= maxAttachments) {
|
||||
Taro.showToast({ title: `附件已达上限(${maxAttachments})`, icon: 'none' })
|
||||
return
|
||||
}
|
||||
let res: any
|
||||
try {
|
||||
// @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) {
|
||||
const msg = String((e as any)?.errMsg || (e as any)?.message || e || '')
|
||||
if (msg.includes('cancel')) return
|
||||
@@ -395,23 +438,30 @@ export default function CreditMpCustomerDetailPage() {
|
||||
try {
|
||||
const uploaded: Attachment[] = []
|
||||
for (const f of picked) {
|
||||
const record: any = await uploadFileByPath(f.path)
|
||||
const url = String(record?.url || record?.downloadUrl || record?.thumbnail || record?.path || '').trim()
|
||||
if (!url) continue
|
||||
uploaded.push({
|
||||
id: `${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
||||
name: String(f?.name || record?.name || guessNameFromUrl(url, '文件')).trim(),
|
||||
url,
|
||||
thumbnail: record?.thumbnail ? String(record.thumbnail) : undefined,
|
||||
isImage: false
|
||||
})
|
||||
try {
|
||||
const record: any = await uploadFileByPath(f.path)
|
||||
const url = getUploadedUrl(record)
|
||||
if (!url) continue
|
||||
uploaded.push({
|
||||
id: makeAttachmentId(),
|
||||
name: String(f?.name || record?.name || guessNameFromUrl(url, '文件')).trim(),
|
||||
url,
|
||||
thumbnail: record?.thumbnail ? String(record.thumbnail) : undefined,
|
||||
isImage: false
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('上传文件失败:', e)
|
||||
}
|
||||
}
|
||||
if (!uploaded.length) return
|
||||
await saveAttachments(attachments.concat(uploaded))
|
||||
} catch (e) {
|
||||
console.error('上传文件失败:', e)
|
||||
Taro.showToast({ title: (e as any)?.message || '上传失败', icon: 'none' })
|
||||
} finally {
|
||||
setUploading(false)
|
||||
}
|
||||
}, [attachments, saveAttachments])
|
||||
}, [attachments, maxAttachments, saveAttachments, savingFiles, uploading])
|
||||
|
||||
const onAttachmentAction = useCallback(
|
||||
async (a: Attachment) => {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '客户跟进',
|
||||
navigationBarTextStyle: 'black',
|
||||
navigationBarBackgroundColor: '#ffffff',
|
||||
navigationStyle: 'custom'
|
||||
navigationBarBackgroundColor: '#ffffff'
|
||||
})
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import { useCallback, useMemo, useState } from 'react'
|
||||
import Taro, { useDidShow, useRouter } from '@tarojs/taro'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import { Button, ConfigProvider, Empty, Loading, TextArea } from '@nutui/nutui-react-taro'
|
||||
import { Setting } from '@nutui/icons-react-taro'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
import { getCreditMpCustomer, updateCreditMpCustomer } from '@/api/credit/creditMpCustomer'
|
||||
@@ -153,6 +152,7 @@ export default function CreditMpCustomerFollowStep1Page() {
|
||||
return Number.isFinite(id) && id > 0 ? id : undefined
|
||||
}, [router?.params])
|
||||
|
||||
// @ts-ignore
|
||||
const statusBarHeight = useMemo(() => {
|
||||
try {
|
||||
const info = Taro.getSystemInfoSync()
|
||||
@@ -465,48 +465,11 @@ export default function CreditMpCustomerFollowStep1Page() {
|
||||
}
|
||||
}
|
||||
|
||||
const headerOffset = statusBarHeight + 80
|
||||
const headerOffset = 12
|
||||
|
||||
return (
|
||||
<View className="bg-pink-50 min-h-screen">
|
||||
<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">
|
||||
{loading ? (
|
||||
<View className="py-16 flex justify-center items-center text-gray-500">
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
SearchBar,
|
||||
Tag
|
||||
} 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 { getCreditMpCustomer, pageCreditMpCustomer, updateCreditMpCustomer } from '@/api/credit/creditMpCustomer'
|
||||
@@ -382,6 +382,7 @@ export default function CreditCompanyPage() {
|
||||
Taro.showToast({ title: `已复制${unique.length}个电话`, icon: 'success' })
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const callPhone = async (phone?: string) => {
|
||||
const num = String(phone || '').trim()
|
||||
if (!num) {
|
||||
@@ -637,13 +638,11 @@ export default function CreditCompanyPage() {
|
||||
<Button
|
||||
size="small"
|
||||
fill="outline"
|
||||
icon={<Phone />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
callPhone(primaryPhone)
|
||||
onClick={() => {
|
||||
Taro.navigateTo({url: `/credit/mp-customer/follow-step1?id=${c.id}`})
|
||||
}}
|
||||
>
|
||||
拨打
|
||||
跟进
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
Reference in New Issue
Block a user