Files
shanxia2.0/miniprogram/pages/crm/clue/detail.vue
T
2026-07-18 09:14:02 +08:00

206 lines
6.9 KiB
Vue

<script setup>
import { ref } from 'vue'
import { onLoad, onShow } from '@dcloudio/uni-app'
import {
getClue,
listClueFollow,
addClueFollow,
convertClue,
markClueInvalid
} from '@/api/crm'
import { labelOf, dash } from '@/utils/crmLabels'
import { PAGE_PATHS } from '@/config/api'
const id = ref('')
const detail = ref(null)
const follows = ref([])
const loading = ref(false)
const followContent = ref('')
const followType = ref('电话')
const showFollow = ref(false)
const submitting = ref(false)
const statusLabel = (s) => labelOf('clueStatus', s)
const levelLabel = (s) => labelOf('level', s)
const goBack = () => {
uni.navigateBack({
fail: () => uni.navigateTo({ url: PAGE_PATHS.crmClueList })
})
}
const load = async () => {
if (!id.value) return
loading.value = true
try {
detail.value = await getClue(id.value)
const fl = await listClueFollow(id.value, 1, 50)
follows.value = fl.rows || []
} catch (e) {
uni.showToast({ title: e.message || '加载失败', icon: 'none' })
} finally {
loading.value = false
}
}
const submitFollow = async () => {
if (!followContent.value.trim()) {
uni.showToast({ title: '请填写跟进内容', icon: 'none' })
return
}
submitting.value = true
try {
await addClueFollow({
clueId: id.value,
followType: followType.value,
followContent: followContent.value.trim()
})
uni.showToast({ title: '已保存', icon: 'success' })
showFollow.value = false
followContent.value = ''
await load()
} catch (e) {
uni.showToast({ title: e.message || '保存失败', icon: 'none' })
} finally {
submitting.value = false
}
}
const onConvert = () => {
const d = detail.value || {}
uni.showModal({
title: '转化为客户',
content: `将「${d.clueName || ''}」转为客户,是否继续?`,
success: async (r) => {
if (!r.confirm) return
try {
await convertClue({
id: id.value,
customerName: d.company || d.clueName,
customerType: d.customerType || 'company',
contactName: d.clueName,
contactMobile: d.mobile
})
uni.showToast({ title: '转化成功', icon: 'success' })
load()
} catch (e) {
uni.showToast({ title: e.message || '转化失败', icon: 'none' })
}
}
})
}
const onInvalid = () => {
uni.showModal({
title: '标记无效',
editable: true,
placeholderText: '无效原因(可选)',
success: async (r) => {
if (!r.confirm) return
try {
await markClueInvalid({ id: id.value, remark: r.content || '' })
uni.showToast({ title: '已标记', icon: 'success' })
load()
} catch (e) {
uni.showToast({ title: e.message || '操作失败', icon: 'none' })
}
}
})
}
onLoad((q) => {
id.value = q?.id || ''
})
onShow(() => load())
</script>
<template>
<view class="mp-page mp-page--sub mp-page--with-bottom">
<view class="mp-header">
<text class="mp-subpage-back" @click="goBack">← 返回</text>
<text class="mp-header__title" style="margin-top: 16rpx">线索详情</text>
</view>
<view v-if="detail" class="mp-detail-hero">
<view class="mp-detail-hero__top">
<view>
<text class="mp-detail-hero__title">{{ dash(detail.clueName) }}</text>
<text class="mp-detail-hero__sub">{{ dash(detail.company) }}</text>
</view>
<text class="mp-tag" :class="'mp-tag--' + statusLabel(detail.status).tag">
{{ statusLabel(detail.status).text }}
</text>
</view>
<view class="mp-detail-hero__meta">
<text class="mp-detail-hero__chip">{{ levelLabel(detail.level).text }}</text>
<text class="mp-detail-hero__chip">{{ dash(detail.ownerUserName) }}</text>
</view>
</view>
<view class="mp-block">
<text class="mp-block__title">基本信息</text>
<view class="mp-block__panel">
<view class="mp-kv"><text class="mp-kv__label">手机</text><text class="mp-kv__value">{{ dash(detail?.mobile) }}</text></view>
<view class="mp-kv"><text class="mp-kv__label">职位</text><text class="mp-kv__value">{{ dash(detail?.position) }}</text></view>
<view class="mp-kv"><text class="mp-kv__label">来源</text><text class="mp-kv__value">{{ dash(detail?.source) }}</text></view>
<view class="mp-kv"><text class="mp-kv__label">行业</text><text class="mp-kv__value">{{ dash(detail?.industry) }}</text></view>
<view class="mp-kv"><text class="mp-kv__label">园区</text><text class="mp-kv__value">{{ dash(detail?.parkArea) }}</text></view>
<view class="mp-kv"><text class="mp-kv__label">备注</text><text class="mp-kv__value">{{ dash(detail?.remark) }}</text></view>
</view>
</view>
<view class="mp-block">
<text class="mp-block__title">跟进记录</text>
<mp-empty v-if="!follows.length" compact title="暂无跟进" />
<view v-else class="mp-list">
<view v-for="f in follows" :key="f.id" class="mp-card">
<view class="mp-card__head">
<text class="mp-card__title">{{ dash(f.followType) }}</text>
<text class="mp-card__sub">{{ dash(f.createTime) }}</text>
</view>
<text style="font-size: 26rpx; color: var(--mp-text)">{{ dash(f.followContent) }}</text>
<view class="mp-card__row" style="margin-top: 12rpx; margin-bottom: 0">
<text>跟进人</text>
<text class="mp-card__row-val">{{ dash(f.createByName) }}</text>
</view>
</view>
</view>
</view>
<view v-if="showFollow" class="mp-form-panel">
<view class="mp-form-row">
<text class="mp-form-label">跟进方式</text>
<input v-model="followType" class="mp-form-input" placeholder="如:电话 / 微信 / 拜访" />
</view>
<view class="mp-form-row">
<text class="mp-form-label mp-form-label--req">跟进内容</text>
<textarea v-model="followContent" class="mp-form-textarea" placeholder="填写本次沟通要点" />
</view>
<view class="mp-form-row" style="display: flex; gap: 16rpx">
<view class="mp-btn mp-btn--ghost" style="flex: 1" @click="showFollow = false">取消</view>
<view class="mp-btn mp-btn--primary" style="flex: 1" @click="submitFollow">
{{ submitting ? '提交中…' : '提交跟进' }}
</view>
</view>
</view>
<view class="mp-bottom-bar">
<view class="mp-btn mp-btn--ghost" @click="showFollow = true">跟进</view>
<view
v-if="detail && detail.status !== 'converted' && detail.status !== 'invalid'"
class="mp-btn mp-btn--primary"
@click="onConvert"
>
转客户
</view>
<view
v-if="detail && detail.status !== 'converted' && detail.status !== 'invalid'"
class="mp-btn mp-btn--danger"
@click="onInvalid"
>
无效
</view>
</view>
</view>
</template>