feat: PC端商机跟单新增详情弹窗+跟进时间线
- 点击客户名打开详情弹窗(替代直接编辑) - 详情弹窗: 商机信息 + 跟进时间线 + 添加入口 - 跟进方式 chip: 电话/微信/上门/邮件/其他(色标区分) - 每条跟进记录显示: 图标+日期+方式+内容+作者 - 支持添加和删除跟进记录 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,17 @@ const form = ref<any>({})
|
||||
const statusPick = ref<Record<string, string>>({})
|
||||
const miniStatuses = ['跟进中', '已签约', '已流失']
|
||||
|
||||
// ── Detail + Follow-up logs ──
|
||||
const detailVisible = ref(false)
|
||||
const selectedBusiness = ref<any>(null)
|
||||
const logs = ref<any[]>([])
|
||||
const logForm = ref({ log_date: '', method: '电话', content: '' })
|
||||
const logMethods = ['电话', '微信', '上门', '邮件', '其他']
|
||||
const logMethodIcons: Record<string, string> = { '电话': '📞', '微信': '💬', '上门': '🏢', '邮件': '📧', '其他': '📋' }
|
||||
const logMethodColors: Record<string, string> = { '电话': '#5B7FA5', '微信': '#22c55e', '上门': '#4A6741', '邮件': '#C4934A', '其他': '#7B7568' }
|
||||
const logLoading = ref(false)
|
||||
const logSaving = ref(false)
|
||||
|
||||
// ── Filters ──
|
||||
const filterManager = ref('')
|
||||
const filterStatus = ref('')
|
||||
@@ -149,6 +160,44 @@ async function handleDelete(id: string) {
|
||||
} catch (e: any) { if (e !== 'cancel') ElMessage.error('删除失败') }
|
||||
}
|
||||
|
||||
// ── Detail + Follow-up logs ──
|
||||
async function openDetail(item: any) {
|
||||
selectedBusiness.value = item
|
||||
detailVisible.value = true
|
||||
logForm.value = { log_date: new Date().toISOString().slice(0, 10), method: '电话', content: '' }
|
||||
await loadLogs()
|
||||
}
|
||||
|
||||
async function loadLogs() {
|
||||
logLoading.value = true
|
||||
try {
|
||||
const res = await miniBusinessApi.getLogs(selectedBusiness.value.id)
|
||||
logs.value = res.data || []
|
||||
} catch (_) { logs.value = [] }
|
||||
finally { logLoading.value = false }
|
||||
}
|
||||
|
||||
async function handleCreateLog() {
|
||||
if (!logForm.value.content.trim()) { ElMessage.warning('请输入跟进内容'); return }
|
||||
logSaving.value = true
|
||||
try {
|
||||
await miniBusinessApi.createLog(selectedBusiness.value.id, logForm.value)
|
||||
ElMessage.success('跟进记录已添加')
|
||||
logForm.value = { log_date: new Date().toISOString().slice(0, 10), method: '电话', content: '' }
|
||||
await loadLogs()
|
||||
} catch (e: any) { ElMessage.error('添加失败: ' + (e.response?.data?.detail || e.message)) }
|
||||
finally { logSaving.value = false }
|
||||
}
|
||||
|
||||
async function handleDeleteLog(logId: string) {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定删除该跟进记录?', '确认', { type: 'warning' })
|
||||
await miniBusinessApi.deleteLog(logId)
|
||||
ElMessage.success('已删除')
|
||||
await loadLogs()
|
||||
} catch (e: any) { if (e !== 'cancel') ElMessage.error('删除失败') }
|
||||
}
|
||||
|
||||
async function quickStatusChange(row: any, newStatus: string) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定将状态改为「${newStatus}」?`, '确认', { type: 'warning', confirmButtonText: '确定', cancelButtonText: '取消' })
|
||||
@@ -220,7 +269,7 @@ async function quickStatusChange(row: any, newStatus: string) {
|
||||
<el-table-column type="index" label="序号" width="50" />
|
||||
<el-table-column prop="customer_name" label="客户" width="160">
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" :underline="false" @click="openEdit(row)">{{ row.customer_name }}</el-link>
|
||||
<el-link type="primary" :underline="false" @click="openDetail(row)">{{ row.customer_name }}</el-link>
|
||||
<el-tooltip v-if="row.edit_log?.length > 1" placement="top">
|
||||
<template #content>最后编辑:{{ row.edit_log[row.edit_log.length-1].editor }} · {{ row.edit_log.length-1 }}次修改</template>
|
||||
<span class="edit-indicator" title="有过修改">🕐</span>
|
||||
@@ -300,6 +349,59 @@ async function quickStatusChange(row: any, newStatus: string) {
|
||||
<el-button type="primary" @click="handleSave">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- Detail Dialog with Follow-up Timeline -->
|
||||
<el-dialog v-model="detailVisible" :title="selectedBusiness?.customer_name + ' — 商机跟单'" width="650px" v-if="selectedBusiness">
|
||||
<!-- Business Info -->
|
||||
<div class="detail-info">
|
||||
<div class="detail-row"><span class="detail-label">产品类型</span><span>{{ selectedBusiness.product_type }}</span></div>
|
||||
<div class="detail-row"><span class="detail-label">金额</span><span>{{ selectedBusiness.amount }}</span></div>
|
||||
<div class="detail-row"><span class="detail-label">状态</span><span :style="{ color: selectedBusiness.status === '已签约' ? '#4A6741' : selectedBusiness.status === '已流失' ? '#B8472E' : '#C4934A' }">{{ selectedBusiness.status }}</span></div>
|
||||
<div class="detail-row"><span class="detail-label">预计列收</span><span>{{ selectedBusiness.expected_revenue_date }}</span></div>
|
||||
<div class="detail-row"><span class="detail-label">客户经理</span><span class="mgr-tag" :style="getMgrStyle(selectedBusiness.manager_name, isLight)">{{ selectedBusiness.manager_name }}</span></div>
|
||||
<div class="detail-row" v-if="selectedBusiness.follow_up_detail"><span class="detail-label">商机概述</span><span>{{ selectedBusiness.follow_up_detail }}</span></div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:12px">
|
||||
<el-button size="small" @click="detailVisible = false; openEdit(selectedBusiness)">编辑商机</el-button>
|
||||
</div>
|
||||
|
||||
<!-- Follow-up Timeline -->
|
||||
<div class="log-section">
|
||||
<h4 class="log-title">跟进记录</h4>
|
||||
<div v-loading="logLoading" class="log-list">
|
||||
<div v-if="logs.length === 0 && !logLoading" class="log-empty">暂无跟进记录</div>
|
||||
<div v-for="l in logs" :key="l.id" class="log-item">
|
||||
<span class="log-icon">{{ logMethodIcons[l.method] || '📋' }}</span>
|
||||
<div class="log-body">
|
||||
<div class="log-header">
|
||||
<span class="log-date">{{ l.log_date }}</span>
|
||||
<span class="log-method-chip" :style="{ background: logMethodColors[l.method] || '#7B7568', color: '#fff', padding: '1px 8px', fontSize: '11px' }">{{ l.method }}</span>
|
||||
<span class="log-author">{{ l.created_by_name }}</span>
|
||||
</div>
|
||||
<div class="log-content">{{ l.content }}</div>
|
||||
</div>
|
||||
<el-button size="small" type="danger" text @click="handleDeleteLog(l.id)" class="log-del">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Log Form -->
|
||||
<div class="log-add">
|
||||
<div class="log-add-row">
|
||||
<el-date-picker v-model="logForm.log_date" type="date" size="small" style="width:140px" />
|
||||
<div class="log-method-chips">
|
||||
<button v-for="m in logMethods" :key="m" type="button" class="log-chip" :class="{ 'log-chip--active': logForm.method === m }" :style="logForm.method === m ? { background: logMethodColors[m], borderColor: logMethodColors[m], color: '#fff' } : {}" @click="logForm.method = m">{{ m }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<el-input v-model="logForm.content" type="textarea" :rows="2" placeholder="输入跟进内容..." size="small" style="margin-top:6px" />
|
||||
<el-button type="primary" size="small" :loading="logSaving" @click="handleCreateLog" style="margin-top:6px">添加跟进</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="detailVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -326,4 +428,33 @@ async function quickStatusChange(row: any, newStatus: string) {
|
||||
.quick-create-btn { display: inline-flex; align-items: center; gap: 4px; background: none; border: 1px dashed var(--gold); padding: 6px 12px; color: var(--gold-dark); font-family: var(--font-body); font-size: 12px; cursor: pointer; transition: all 0.2s; }
|
||||
.quick-create-btn:hover { border-color: var(--vermilion); color: var(--vermilion); background: rgba(184,71,46,0.03); }
|
||||
.select-empty-create { padding: 8px 12px; text-align: center; }
|
||||
|
||||
/* ── Detail Dialog ── */
|
||||
.detail-info { background: var(--c-bg-light, #faf9f6); border-radius: 6px; padding: 14px 18px; }
|
||||
.detail-row { display: flex; gap: 12px; padding: 4px 0; font-size: 13px; }
|
||||
.detail-label { color: var(--warm-gray); min-width: 70px; flex-shrink: 0; }
|
||||
|
||||
/* ── Log Timeline ── */
|
||||
.log-section { margin-top: 18px; border-top: 1px solid var(--warm-border); padding-top: 14px; }
|
||||
.log-title { margin: 0 0 12px; font-family: var(--font-heading); font-size: 16px; color: var(--ink); }
|
||||
.log-list { display: flex; flex-direction: column; gap: 10px; max-height: 300px; overflow-y: auto; margin-bottom: 14px; }
|
||||
.log-empty { text-align: center; color: var(--warm-gray); font-size: 13px; padding: 20px 0; }
|
||||
.log-item { display: flex; gap: 10px; align-items: flex-start; padding: 10px 12px; background: var(--surface); border: 1px solid var(--warm-border); }
|
||||
.log-icon { font-size: 18px; flex-shrink: 0; margin-top: 2px; }
|
||||
.log-body { flex: 1; min-width: 0; }
|
||||
.log-header { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
|
||||
.log-date { font-family: var(--font-mono); font-size: 12px; color: var(--ink); }
|
||||
.log-author { font-size: 11px; color: var(--warm-gray); margin-left: auto; }
|
||||
.log-content { font-size: 13px; color: var(--c-text); line-height: 1.6; }
|
||||
.log-del { flex-shrink: 0; opacity: 0.5; }
|
||||
.log-del:hover { opacity: 1; }
|
||||
.log-method-chip { border-radius: 3px; white-space: nowrap; }
|
||||
|
||||
/* ── Add Log Form ── */
|
||||
.log-add { border-top: 1px solid var(--warm-border); padding-top: 12px; }
|
||||
.log-add-row { display: flex; gap: 8px; align-items: center; }
|
||||
.log-method-chips { display: flex; gap: 4px; }
|
||||
.log-chip { padding: 4px 10px; border: 1px solid var(--warm-border); background: var(--surface); font-size: 12px; cursor: pointer; transition: all 0.2s; }
|
||||
.log-chip:hover { border-color: var(--ink); }
|
||||
.log-chip--active { font-weight: 600; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user