feat(credit): 客户详情页附件功能优化及界面调整

- 添加附件ID生成函数makeAttachmentId用于唯一标识附件
- 实现getUploadedUrl函数统一处理各种URL格式的附件获取
- 创建normalizeAttachments函数去重并标准化附件数据结构
- 更新parseFilesToAttachments函数整合附件标准化逻辑
- 设置最大附件数量限制为30个并添加相应提示
- 优化图片和文件上传逻辑增加错误处理和进度控制
- 移除自定义导航栏样式简化follow-step1页面头部
- 将拨打电话按钮改为客户跟进功能跳转到跟进页面
- 注释掉未使用的Phone图标导入和相关拨打功能
This commit is contained in:
2026-03-20 17:40:08 +08:00
parent 287e91db79
commit ad39a9c1aa
4 changed files with 100 additions and 89 deletions

View File

@@ -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(),
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) {
try {
const record: any = await uploadFileByPath(p)
const url = String(record?.url || record?.downloadUrl || record?.thumbnail || record?.path || '').trim()
const url = getUploadedUrl(record)
if (!url) continue
uploaded.push({
id: `${Date.now()}-${Math.random().toString(16).slice(2)}`,
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) {
try {
const record: any = await uploadFileByPath(f.path)
const url = String(record?.url || record?.downloadUrl || record?.thumbnail || record?.path || '').trim()
const url = getUploadedUrl(record)
if (!url) continue
uploaded.push({
id: `${Date.now()}-${Math.random().toString(16).slice(2)}`,
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) => {

View File

@@ -1,7 +1,6 @@
export default definePageConfig({
navigationBarTitleText: '客户跟进',
navigationBarTextStyle: 'black',
navigationBarBackgroundColor: '#ffffff',
navigationStyle: 'custom'
navigationBarBackgroundColor: '#ffffff'
})

View File

@@ -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">

View File

@@ -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>