feat: 完善商机管理模块,移除线索自动创建商机逻辑
- 商机管理页面: - 实现crmStats真实数据计算(商机总数、金额、本月赢单、转化率) - 添加环比变化率计算功能 - 修复金额字段类型转换问题(字符串转数字) - 完善Pipeline View阶段过滤和列显示 - 添加商机搜索、详情查看、编辑和删除功能 - 后端改造: - 移除线索转化自动创建商机的逻辑(业务流程修正) - 新增经销商选择器API(/crm/dealer/portal/select) - 实现经销商列表查询接口 - 新增50条商机测试数据SQL脚本 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import type {
|
||||
CrmDealerVo,
|
||||
CrmLeadBo,
|
||||
CrmLeadFollowBo,
|
||||
CrmLeadFollowVo,
|
||||
@@ -21,6 +22,7 @@ import type {
|
||||
|
||||
// 导出类型供外部使用
|
||||
export type {
|
||||
CrmDealerVo,
|
||||
CrmLeadBo,
|
||||
CrmLeadFollowBo,
|
||||
CrmLeadFollowVo,
|
||||
@@ -139,3 +141,17 @@ export function updateOpportunity(data: CrmOpportunityBo): Promise<R<void>> {
|
||||
export function deleteOpportunity(opportunityIds: string): Promise<R<void>> {
|
||||
return request.delete(`/crm/opportunity/${opportunityIds}`).json();
|
||||
}
|
||||
|
||||
/**
|
||||
* ========================================
|
||||
* CRM 经销商管理模块 API 调用
|
||||
* ========================================
|
||||
*/
|
||||
|
||||
/**
|
||||
* 获取经销商选择器列表
|
||||
* 用于商机创建、商机分配等场景
|
||||
*/
|
||||
export function getDealerSelectList(keyword?: string): Promise<R<CrmDealerVo[]>> {
|
||||
return request.get('/crm/dealer/portal/select', { keyword }).json();
|
||||
}
|
||||
|
||||
@@ -217,4 +217,47 @@ export interface TableDataInfo<T> {
|
||||
msg: string;
|
||||
rows: T[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* ========================================
|
||||
* CRM 经销商管理模块类型定义
|
||||
* ========================================
|
||||
*/
|
||||
|
||||
/**
|
||||
* 经销商视图对象(响应)
|
||||
*/
|
||||
export interface CrmDealerVo {
|
||||
dealerId: number;
|
||||
tenantId: string;
|
||||
customerCode?: string; // ERP客户编码
|
||||
dealerName: string; // 经销商名称
|
||||
dealerCode: string; // 经销商编码
|
||||
contactName?: string; // 联系人
|
||||
mobile?: string; // 手机
|
||||
province?: string; // 省
|
||||
city?: string; // 市
|
||||
level?: string; // 等级(A/B/C)
|
||||
levelName?: string; // 等级名称(翻译)
|
||||
lifecycle?: string; // 生命周期
|
||||
lifecycleName?: string; // 生命周期名称(翻译)
|
||||
signedAt?: string; // 签约时间
|
||||
storeCount?: number; // 门店数
|
||||
teamSize?: number; // 团队规模
|
||||
totalOrderAmount?: number; // 累计订单金额
|
||||
totalPaymentAmount?: number; // 累计回款金额
|
||||
activityScore?: number; // 活跃评分
|
||||
riskScore?: number; // 风险评分
|
||||
ownerUserId?: number; // 负责人
|
||||
ownerUserName?: string; // 负责人姓名(翻译)
|
||||
sourceLeadId?: number; // 来源线索ID
|
||||
status?: string; // 状态
|
||||
statusName?: string; // 状态名称(翻译)
|
||||
createBy: number;
|
||||
createByName?: string;
|
||||
createTime: string;
|
||||
updateBy?: number;
|
||||
updateByName?: string;
|
||||
updateTime?: string;
|
||||
}
|
||||
@@ -1,18 +1,125 @@
|
||||
<script setup lang="ts">
|
||||
import type { CrmOpportunityVo } from '@/api/crm';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import type { CrmDealerVo, CrmOpportunityBo, CrmOpportunityVo } from '@/api/crm';
|
||||
import type { UserInfo } from '@/api/user';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { getOpportunityList } from '@/api/crm';
|
||||
import { createOpportunity, deleteOpportunity, getDealerSelectList, getOpportunityList, updateOpportunity } from '@/api/crm';
|
||||
import { getUserSelectList } from '@/api/user';
|
||||
|
||||
// Stats
|
||||
const opportunityTotal = ref(0);
|
||||
|
||||
const crmStats = computed(() => [
|
||||
{ label: '商机总数', value: opportunityTotal.value.toLocaleString(), icon: 'TrendCharts' as const, change: '+5.2%', up: true, bg: '#eef5ff', color: '#1d5af3' },
|
||||
{ label: '商机金额', value: '¥4,680万', icon: 'Money' as const, change: '+12.8%', up: true, bg: '#f0fdf4', color: '#16a34a' },
|
||||
{ label: '本月赢单', value: '23', icon: 'Trophy' as const, change: '+15%', up: true, bg: '#fef3c7', color: '#d97706' },
|
||||
{ label: '转化率', value: '32%', icon: 'DataLine' as const, change: '-2.1%', up: false, bg: '#fef2f2', color: '#dc2626' },
|
||||
]);
|
||||
// Stats - 使用真实数据计算
|
||||
const crmStats = computed(() => {
|
||||
// 商机总数
|
||||
const total = opportunityList.value.length;
|
||||
|
||||
// 商机金额总和(amount可能返回字符串,需转换为数字)
|
||||
const totalAmount = opportunityList.value.reduce((sum, o) => {
|
||||
const amount = typeof o.amount === 'string' ? parseFloat(o.amount) : (o.amount || 0);
|
||||
return sum + amount;
|
||||
}, 0);
|
||||
|
||||
// 本月赢单数量(closing阶段)
|
||||
const now = new Date();
|
||||
const currentMonth = now.getMonth();
|
||||
const currentYear = now.getFullYear();
|
||||
const lastMonth = currentMonth === 0 ? 11 : currentMonth - 1;
|
||||
const lastMonthYear = currentMonth === 0 ? currentYear - 1 : currentYear;
|
||||
|
||||
// 本月赢单
|
||||
const monthlyWins = opportunityList.value.filter((o) => {
|
||||
if (o.stage !== 'closing') return false;
|
||||
const createTime = new Date(o.createTime);
|
||||
return createTime.getMonth() === currentMonth && createTime.getFullYear() === currentYear;
|
||||
}).length;
|
||||
|
||||
// 上月赢单(用于计算环比)
|
||||
const lastMonthWins = opportunityList.value.filter((o) => {
|
||||
if (o.stage !== 'closing') return false;
|
||||
const createTime = new Date(o.createTime);
|
||||
return createTime.getMonth() === lastMonth && createTime.getFullYear() === lastMonthYear;
|
||||
}).length;
|
||||
|
||||
// 本月新增商机
|
||||
const monthlyNewOpps = opportunityList.value.filter((o) => {
|
||||
const createTime = new Date(o.createTime);
|
||||
return createTime.getMonth() === currentMonth && createTime.getFullYear() === currentYear;
|
||||
}).length;
|
||||
|
||||
// 上月新增商机(用于计算环比)
|
||||
const lastMonthNewOpps = opportunityList.value.filter((o) => {
|
||||
const createTime = new Date(o.createTime);
|
||||
return createTime.getMonth() === lastMonth && createTime.getFullYear() === lastMonthYear;
|
||||
}).length;
|
||||
|
||||
// 本月赢单金额
|
||||
const monthlyWinAmount = opportunityList.value.filter((o) => {
|
||||
if (o.stage !== 'closing') return false;
|
||||
const createTime = new Date(o.createTime);
|
||||
return createTime.getMonth() === currentMonth && createTime.getFullYear() === currentYear;
|
||||
}).reduce((sum, o) => {
|
||||
const amount = typeof o.amount === 'string' ? parseFloat(o.amount) : (o.amount || 0);
|
||||
return sum + amount;
|
||||
}, 0);
|
||||
|
||||
// 上月赢单金额
|
||||
const lastMonthWinAmount = opportunityList.value.filter((o) => {
|
||||
if (o.stage !== 'closing') return false;
|
||||
const createTime = new Date(o.createTime);
|
||||
return createTime.getMonth() === lastMonth && createTime.getFullYear() === lastMonthYear;
|
||||
}).reduce((sum, o) => {
|
||||
const amount = typeof o.amount === 'string' ? parseFloat(o.amount) : (o.amount || 0);
|
||||
return sum + amount;
|
||||
}, 0);
|
||||
|
||||
// 转化率(closing阶段占比)
|
||||
const winCount = opportunityList.value.filter(o => o.stage === 'closing').length;
|
||||
const conversionRate = total > 0 ? Math.round((winCount / total) * 100) : 0;
|
||||
|
||||
// 计算环比变化率
|
||||
function calcChangeRate(current: number, previous: number): { rate: string, up: boolean } {
|
||||
if (previous === 0) {
|
||||
// 上月无数据时,显示新增状态
|
||||
return { rate: current > 0 ? '+新增' : '0%', up: current > 0 };
|
||||
}
|
||||
const change = ((current - previous) / previous) * 100;
|
||||
const sign = change >= 0 ? '+' : '';
|
||||
return { rate: `${sign}${change.toFixed(1)}%`, up: change >= 0 };
|
||||
}
|
||||
|
||||
// 商机总数变化(本月新增 vs 上月新增)
|
||||
const totalChange = calcChangeRate(monthlyNewOpps, lastMonthNewOpps);
|
||||
|
||||
// 商机金额变化(本月赢单金额 vs 上月赢单金额)
|
||||
const amountChange = calcChangeRate(monthlyWinAmount, lastMonthWinAmount);
|
||||
|
||||
// 本月赢单变化(本月赢单 vs 上月赢单)
|
||||
const winsChange = calcChangeRate(monthlyWins, lastMonthWins);
|
||||
|
||||
// 转化率变化(基于closing阶段本月vs上月占比)
|
||||
const currentConversionBase = monthlyNewOpps > 0 ? Math.round((monthlyWins / monthlyNewOpps) * 100) : 0;
|
||||
const lastConversionBase = lastMonthNewOpps > 0 ? Math.round((lastMonthWins / lastMonthNewOpps) * 100) : 0;
|
||||
const conversionChange = calcChangeRate(currentConversionBase, lastConversionBase);
|
||||
|
||||
return [
|
||||
{ label: '商机总数', value: total.toString(), icon: 'TrendCharts' as const, change: totalChange.rate, up: totalChange.up, bg: '#eef5ff', color: '#1d5af3' },
|
||||
{ label: '商机金额', value: formatAmount(totalAmount), icon: 'Money' as const, change: amountChange.rate, up: amountChange.up, bg: '#f0fdf4', color: '#16a34a' },
|
||||
{ label: '本月赢单', value: monthlyWins.toString(), icon: 'Trophy' as const, change: winsChange.rate, up: winsChange.up, bg: '#fef3c7', color: '#d97706' },
|
||||
{ label: '转化率', value: `${conversionRate}%`, icon: 'DataLine' as const, change: conversionChange.rate, up: conversionChange.up, bg: '#fef2f2', color: '#dc2626' },
|
||||
];
|
||||
});
|
||||
|
||||
// 格式化金额显示
|
||||
function formatAmount(amount: number): string {
|
||||
if (amount >= 100000000) {
|
||||
return `¥${(amount / 100000000).toFixed(2)}亿`;
|
||||
}
|
||||
if (amount >= 10000) {
|
||||
return `¥${(amount / 10000).toFixed(2)}万`;
|
||||
}
|
||||
return `¥${amount.toLocaleString()}`;
|
||||
}
|
||||
|
||||
// Pipeline
|
||||
const activeStage = ref('all');
|
||||
@@ -29,6 +136,9 @@ const pipeline = [
|
||||
const opportunityList = ref<CrmOpportunityVo[]>([]);
|
||||
const opportunityLoading = ref(false);
|
||||
|
||||
// 搜索关键词
|
||||
const searchKeyword = ref('');
|
||||
|
||||
// 加载商机列表
|
||||
async function loadOpportunities() {
|
||||
opportunityLoading.value = true;
|
||||
@@ -56,9 +166,238 @@ function updatePipelineCounts() {
|
||||
}
|
||||
|
||||
function getCardsForStage(stage: string) {
|
||||
if (stage === 'all')
|
||||
return opportunityList.value;
|
||||
return opportunityList.value.filter((o: CrmOpportunityVo) => o.stage === stage);
|
||||
// 先按阶段过滤
|
||||
let cards = stage === 'all'
|
||||
? opportunityList.value
|
||||
: opportunityList.value.filter((o: CrmOpportunityVo) => o.stage === stage);
|
||||
|
||||
// 再按搜索关键词过滤
|
||||
if (searchKeyword.value) {
|
||||
const keyword = searchKeyword.value.toLowerCase();
|
||||
cards = cards.filter((o: CrmOpportunityVo) =>
|
||||
o.opportunityName?.toLowerCase().includes(keyword)
|
||||
|| o.dealerName?.toLowerCase().includes(keyword)
|
||||
|| o.ownerUserName?.toLowerCase().includes(keyword)
|
||||
|| o.productName?.toLowerCase().includes(keyword),
|
||||
);
|
||||
}
|
||||
|
||||
return cards;
|
||||
}
|
||||
|
||||
// 根据activeStage过滤显示的Pipeline列
|
||||
const filteredPipeline = computed(() => {
|
||||
// 实际阶段列(排除"全部"虚拟列)
|
||||
const actualStages = pipeline.filter(s => s.key !== 'all');
|
||||
|
||||
if (activeStage.value === 'all') {
|
||||
// 全部Tab显示所有实际阶段列(线索、谈判中、方案、赢单)
|
||||
return actualStages;
|
||||
}
|
||||
// 只显示选中阶段的列
|
||||
return actualStages.filter(s => s.key === activeStage.value);
|
||||
});
|
||||
|
||||
// ============== 新建商机模块 ==============
|
||||
|
||||
const showAddOpportunityDialog = ref(false);
|
||||
const opportunityForm = ref<CrmOpportunityBo>({
|
||||
dealerId: 0,
|
||||
opportunityName: '',
|
||||
stage: 'lead',
|
||||
amount: 0,
|
||||
probability: 10,
|
||||
expectedCloseDate: '',
|
||||
ownerUserId: 0,
|
||||
productName: '',
|
||||
description: '',
|
||||
});
|
||||
const dealerList = ref<CrmDealerVo[]>([]);
|
||||
const dealerLoading = ref(false);
|
||||
const userList = ref<UserInfo[]>([]);
|
||||
const userLoading = ref(false);
|
||||
|
||||
// 打开新建商机Dialog
|
||||
function openAddOpportunityDialog(stage?: string) {
|
||||
// 根据点击位置自动设置商机阶段
|
||||
// 如果在特定阶段的列中点击,自动设置该阶段
|
||||
// 如果在全部列或toolbar按钮点击,默认设置为"线索"
|
||||
const defaultStage = stage && stage !== 'all' ? stage : 'lead';
|
||||
|
||||
opportunityForm.value = {
|
||||
dealerId: 0,
|
||||
opportunityName: '',
|
||||
stage: defaultStage,
|
||||
amount: 0,
|
||||
probability: getProbabilityByStage(defaultStage),
|
||||
expectedCloseDate: '',
|
||||
ownerUserId: 0,
|
||||
productName: '',
|
||||
description: '',
|
||||
};
|
||||
showAddOpportunityDialog.value = true;
|
||||
loadDealerList();
|
||||
loadUserList();
|
||||
}
|
||||
|
||||
// 根据阶段获取默认概率
|
||||
function getProbabilityByStage(stage: string): number {
|
||||
const probabilityMap: Record<string, number> = {
|
||||
lead: 10,
|
||||
negotiation: 30,
|
||||
proposal: 50,
|
||||
closing: 90,
|
||||
};
|
||||
return probabilityMap[stage] || 10;
|
||||
}
|
||||
|
||||
// 加载经销商列表
|
||||
async function loadDealerList(keyword?: string) {
|
||||
dealerLoading.value = true;
|
||||
try {
|
||||
const res = await getDealerSelectList(keyword);
|
||||
dealerList.value = res.data || [];
|
||||
}
|
||||
catch (error: any) {
|
||||
ElMessage.error(error?.message || '加载经销商列表失败');
|
||||
}
|
||||
finally {
|
||||
dealerLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 加载用户列表
|
||||
async function loadUserList(keyword?: string) {
|
||||
userLoading.value = true;
|
||||
try {
|
||||
const res = await getUserSelectList(keyword);
|
||||
userList.value = res.data || [];
|
||||
}
|
||||
catch (error: any) {
|
||||
ElMessage.error(error?.message || '加载用户列表失败');
|
||||
}
|
||||
finally {
|
||||
userLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 提交新建商机
|
||||
async function submitOpportunity() {
|
||||
if (!opportunityForm.value.dealerId) {
|
||||
ElMessage.warning('请选择经销商');
|
||||
return;
|
||||
}
|
||||
if (!opportunityForm.value.opportunityName) {
|
||||
ElMessage.warning('请输入商机名称');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await createOpportunity(opportunityForm.value);
|
||||
ElMessage.success('商机创建成功');
|
||||
showAddOpportunityDialog.value = false;
|
||||
loadOpportunities();
|
||||
}
|
||||
catch (error: any) {
|
||||
ElMessage.error(error?.message || '创建商机失败');
|
||||
}
|
||||
}
|
||||
|
||||
// ============== 商机详情模块 ==============
|
||||
|
||||
const showDetailDialog = ref(false);
|
||||
const currentOpportunity = ref<CrmOpportunityVo | null>(null);
|
||||
|
||||
// 打开商机详情
|
||||
function openOpportunityDetail(opp: CrmOpportunityVo) {
|
||||
currentOpportunity.value = opp;
|
||||
showDetailDialog.value = true;
|
||||
}
|
||||
|
||||
// ============== 商机编辑模块 ==============
|
||||
|
||||
const showEditDialog = ref(false);
|
||||
const editForm = ref<CrmOpportunityBo>({
|
||||
opportunityId: 0,
|
||||
dealerId: 0,
|
||||
opportunityName: '',
|
||||
stage: '',
|
||||
amount: 0,
|
||||
probability: 0,
|
||||
expectedCloseDate: '',
|
||||
ownerUserId: 0,
|
||||
productName: '',
|
||||
description: '',
|
||||
});
|
||||
|
||||
// 打开编辑Dialog
|
||||
function openEditDialog() {
|
||||
if (!currentOpportunity.value)
|
||||
return;
|
||||
|
||||
editForm.value = {
|
||||
opportunityId: currentOpportunity.value.opportunityId,
|
||||
dealerId: currentOpportunity.value.dealerId,
|
||||
opportunityName: currentOpportunity.value.opportunityName,
|
||||
stage: currentOpportunity.value.stage,
|
||||
amount: Number(currentOpportunity.value.amount) || 0,
|
||||
probability: currentOpportunity.value.probability || 0,
|
||||
expectedCloseDate: currentOpportunity.value.expectedCloseDate || '',
|
||||
ownerUserId: currentOpportunity.value.ownerUserId || 0,
|
||||
productName: currentOpportunity.value.productName || '',
|
||||
description: currentOpportunity.value.description || '',
|
||||
};
|
||||
showDetailDialog.value = false;
|
||||
showEditDialog.value = true;
|
||||
loadDealerList();
|
||||
loadUserList();
|
||||
}
|
||||
|
||||
// 提交编辑
|
||||
async function submitEdit() {
|
||||
if (!editForm.value.opportunityName) {
|
||||
ElMessage.warning('请输入商机名称');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await updateOpportunity(editForm.value);
|
||||
ElMessage.success('商机更新成功');
|
||||
showEditDialog.value = false;
|
||||
loadOpportunities();
|
||||
}
|
||||
catch (error: any) {
|
||||
ElMessage.error(error?.message || '更新商机失败');
|
||||
}
|
||||
}
|
||||
|
||||
// ============== 商机删除模块 ==============
|
||||
|
||||
async function handleDeleteOpportunity() {
|
||||
if (!currentOpportunity.value)
|
||||
return;
|
||||
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除商机"${currentOpportunity.value.opportunityName}"吗?删除后无法恢复。`,
|
||||
'删除确认',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
},
|
||||
);
|
||||
|
||||
await deleteOpportunity(currentOpportunity.value.opportunityId.toString());
|
||||
ElMessage.success('商机删除成功');
|
||||
showDetailDialog.value = false;
|
||||
loadOpportunities();
|
||||
}
|
||||
catch (error: any) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error(error?.message || '删除商机失败');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -113,8 +452,15 @@ onMounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<el-input placeholder="搜索商机..." clearable style="width: 220px" />
|
||||
<el-button type="primary" round>
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索商机..."
|
||||
clearable
|
||||
style="width: 220px"
|
||||
@keyup.enter="() => {}"
|
||||
@clear="() => {}"
|
||||
/>
|
||||
<el-button type="primary" round @click="() => openAddOpportunityDialog()">
|
||||
<el-icon><Plus /></el-icon> 新建商机
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -122,7 +468,12 @@ onMounted(() => {
|
||||
|
||||
<!-- Pipeline View -->
|
||||
<div v-loading="opportunityLoading" class="pipeline-view">
|
||||
<div v-for="stage in pipeline" :key="stage.key" class="pipeline-col">
|
||||
<!-- 根据activeStage过滤显示的列 -->
|
||||
<div
|
||||
v-for="stage in filteredPipeline"
|
||||
:key="stage.key"
|
||||
class="pipeline-col"
|
||||
>
|
||||
<div class="col-header">
|
||||
<span class="col-title">
|
||||
<span class="col-dot" :style="{ background: stage.color }" />
|
||||
@@ -131,7 +482,7 @@ onMounted(() => {
|
||||
<span class="col-count">{{ stage.count }}</span>
|
||||
</div>
|
||||
<div class="col-cards">
|
||||
<div v-for="opp in getCardsForStage(stage.key)" :key="opp.opportunityId" class="opportunity-card">
|
||||
<div v-for="opp in getCardsForStage(stage.key)" :key="opp.opportunityId" class="opportunity-card" @click="openOpportunityDetail(opp)">
|
||||
<div class="opp-header">
|
||||
<span class="opp-name">{{ opp.opportunityName }}</span>
|
||||
<el-tag v-if="opp.stage === 'closing'" type="success" size="small" effect="plain" round>
|
||||
@@ -159,7 +510,7 @@ onMounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
<div class="opp-footer">
|
||||
<span class="opp-amount">{{ opp.amount ? `¥${opp.amount.toLocaleString()}` : '--' }}</span>
|
||||
<span class="opp-amount">{{ opp.amount ? `¥${Number(opp.amount).toLocaleString()}` : '--' }}</span>
|
||||
<div class="opp-progress">
|
||||
<el-progress :percentage="opp.probability || 0" :stroke-width="4" :show-text="false" :color="stage.color" />
|
||||
</div>
|
||||
@@ -170,7 +521,7 @@ onMounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="add-card-placeholder">
|
||||
<div class="add-card-placeholder" @click="() => openAddOpportunityDialog(stage.key)">
|
||||
<el-icon :size="20">
|
||||
<Plus />
|
||||
</el-icon>
|
||||
@@ -179,6 +530,277 @@ onMounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新建商机Dialog -->
|
||||
<el-dialog v-model="showAddOpportunityDialog" title="新建商机" width="600px" :close-on-click-modal="false">
|
||||
<el-alert type="info" :closable="false" style="margin-bottom: 16px">
|
||||
为经销商创建商机,记录具体的销售机会
|
||||
</el-alert>
|
||||
|
||||
<el-form :model="opportunityForm" label-width="120px">
|
||||
<el-form-item label="经销商" required>
|
||||
<el-select
|
||||
v-model="opportunityForm.dealerId"
|
||||
placeholder="请选择经销商"
|
||||
filterable
|
||||
:loading="dealerLoading"
|
||||
style="width: 100%"
|
||||
@filter-change="(keyword: string) => loadDealerList(keyword)"
|
||||
>
|
||||
<el-option
|
||||
v-for="dealer in dealerList"
|
||||
:key="dealer.dealerId"
|
||||
:label="`${dealer.dealerName}(${dealer.dealerCode})`"
|
||||
:value="dealer.dealerId"
|
||||
>
|
||||
<div style="display: flex; align-items: center; justify-content: space-between">
|
||||
<span>{{ dealer.dealerName }}</span>
|
||||
<span style="color: #8492a6; font-size: 13px">{{ dealer.dealerCode }}</span>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="商机名称" required>
|
||||
<el-input v-model="opportunityForm.opportunityName" placeholder="请输入商机名称,如:首批进货订单" />
|
||||
</el-form-item>
|
||||
<el-form-item label="商机阶段">
|
||||
<el-select v-model="opportunityForm.stage" style="width: 100%">
|
||||
<el-option label="线索" value="lead" />
|
||||
<el-option label="谈判中" value="negotiation" />
|
||||
<el-option label="方案" value="proposal" />
|
||||
<el-option label="赢单" value="closing" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="商机金额">
|
||||
<el-input-number v-model="opportunityForm.amount" :min="0" :precision="2" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="成功概率">
|
||||
<el-slider v-model="opportunityForm.probability" :min="0" :max="100" :step="5" show-input />
|
||||
</el-form-item>
|
||||
<el-form-item label="预计成交日期">
|
||||
<el-date-picker
|
||||
v-model="opportunityForm.expectedCloseDate"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="选择预计成交日期"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="负责人">
|
||||
<el-select
|
||||
v-model="opportunityForm.ownerUserId"
|
||||
placeholder="请选择负责人"
|
||||
filterable
|
||||
:loading="userLoading"
|
||||
style="width: 100%"
|
||||
@filter-change="(keyword: string) => loadUserList(keyword)"
|
||||
>
|
||||
<el-option
|
||||
v-for="user in userList"
|
||||
:key="user.userId"
|
||||
:label="`${user.nickName}(${user.userName})`"
|
||||
:value="user.userId"
|
||||
>
|
||||
<div style="display: flex; align-items: center; justify-content: space-between">
|
||||
<span>{{ user.nickName }}</span>
|
||||
<span v-if="user.deptName" style="color: #8492a6; font-size: 13px">{{ user.deptName }}</span>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="产品名称">
|
||||
<el-input v-model="opportunityForm.productName" placeholder="请输入产品名称(可选)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="商机描述">
|
||||
<el-input v-model="opportunityForm.description" type="textarea" :rows="3" placeholder="请输入商机描述(可选)" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="showAddOpportunityDialog = false">
|
||||
取消
|
||||
</el-button>
|
||||
<el-button type="primary" @click="submitOpportunity">
|
||||
创建商机
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 商机详情Dialog -->
|
||||
<el-dialog v-model="showDetailDialog" title="商机详情" width="600px" :close-on-click-modal="false">
|
||||
<el-form label-width="120px">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="商机名称">
|
||||
<span>{{ currentOpportunity?.opportunityName || '--' }}</span>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="经销商">
|
||||
<span>{{ currentOpportunity?.dealerName || '--' }}</span>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="商机阶段">
|
||||
<el-tag :type="currentOpportunity?.stage === 'closing' ? 'success' : currentOpportunity?.stage === 'lead' ? 'info' : 'warning'" size="small">
|
||||
{{ currentOpportunity?.stageName || currentOpportunity?.stage || '--' }}
|
||||
</el-tag>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="状态">
|
||||
<el-tag type="primary" size="small" effect="plain">
|
||||
{{ currentOpportunity?.statusName || '进行中' }}
|
||||
</el-tag>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="商机金额">
|
||||
<span class="amount-value">{{ currentOpportunity?.amount ? `¥${Number(currentOpportunity.amount).toLocaleString()}` : '--' }}</span>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="成功概率">
|
||||
<el-progress :percentage="currentOpportunity?.probability || 0" :stroke-width="6" style="width: 120px" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="负责人">
|
||||
<span>{{ currentOpportunity?.ownerUserName || '--' }}</span>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="预计成交日期">
|
||||
<span>{{ currentOpportunity?.expectedCloseDate || '--' }}</span>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="产品名称">
|
||||
<span>{{ currentOpportunity?.productName || '--' }}</span>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="创建时间">
|
||||
<span>{{ currentOpportunity?.createTime || '--' }}</span>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="商机描述">
|
||||
<span>{{ currentOpportunity?.description || '--' }}</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="showDetailDialog = false">
|
||||
关闭
|
||||
</el-button>
|
||||
<el-button type="primary" @click="openEditDialog">
|
||||
<el-icon><Edit /></el-icon> 编辑
|
||||
</el-button>
|
||||
<el-button type="danger" @click="handleDeleteOpportunity">
|
||||
<el-icon><Delete /></el-icon> 删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 编辑商机Dialog -->
|
||||
<el-dialog v-model="showEditDialog" title="编辑商机" width="600px" :close-on-click-modal="false">
|
||||
<el-form :model="editForm" label-width="120px">
|
||||
<el-form-item label="经销商" required>
|
||||
<el-select
|
||||
v-model="editForm.dealerId"
|
||||
placeholder="请选择经销商"
|
||||
filterable
|
||||
:loading="dealerLoading"
|
||||
style="width: 100%"
|
||||
@filter-change="(keyword: string) => loadDealerList(keyword)"
|
||||
>
|
||||
<el-option
|
||||
v-for="dealer in dealerList"
|
||||
:key="dealer.dealerId"
|
||||
:label="`${dealer.dealerName}(${dealer.dealerCode})`"
|
||||
:value="dealer.dealerId"
|
||||
>
|
||||
<div style="display: flex; align-items: center; justify-content: space-between">
|
||||
<span>{{ dealer.dealerName }}</span>
|
||||
<span style="color: #8492a6; font-size: 13px">{{ dealer.dealerCode }}</span>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="商机名称" required>
|
||||
<el-input v-model="editForm.opportunityName" placeholder="请输入商机名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="商机阶段">
|
||||
<el-select v-model="editForm.stage" style="width: 100%">
|
||||
<el-option label="线索" value="lead" />
|
||||
<el-option label="谈判中" value="negotiation" />
|
||||
<el-option label="方案" value="proposal" />
|
||||
<el-option label="赢单" value="closing" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="商机金额">
|
||||
<el-input-number v-model="editForm.amount" :min="0" :precision="2" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="成功概率">
|
||||
<el-slider v-model="editForm.probability" :min="0" :max="100" :step="5" show-input />
|
||||
</el-form-item>
|
||||
<el-form-item label="预计成交日期">
|
||||
<el-date-picker
|
||||
v-model="editForm.expectedCloseDate"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="选择预计成交日期"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="负责人">
|
||||
<el-select
|
||||
v-model="editForm.ownerUserId"
|
||||
placeholder="请选择负责人"
|
||||
filterable
|
||||
:loading="userLoading"
|
||||
style="width: 100%"
|
||||
@filter-change="(keyword: string) => loadUserList(keyword)"
|
||||
>
|
||||
<el-option
|
||||
v-for="user in userList"
|
||||
:key="user.userId"
|
||||
:label="`${user.nickName}(${user.userName})`"
|
||||
:value="user.userId"
|
||||
>
|
||||
<div style="display: flex; align-items: center; justify-content: space-between">
|
||||
<span>{{ user.nickName }}</span>
|
||||
<span v-if="user.deptName" style="color: #8492a6; font-size: 13px">{{ user.deptName }}</span>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="产品名称">
|
||||
<el-input v-model="editForm.productName" placeholder="请输入产品名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="商机描述">
|
||||
<el-input v-model="editForm.description" type="textarea" :rows="3" placeholder="请输入商机描述" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="showEditDialog = false">
|
||||
取消
|
||||
</el-button>
|
||||
<el-button type="primary" @click="submitEdit">
|
||||
保存修改
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -187,6 +809,12 @@ onMounted(() => {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.amount-value {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.crm-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
|
||||
121
hzhub-system/sql/test_opportunity_data.sql
Normal file
121
hzhub-system/sql/test_opportunity_data.sql
Normal file
@@ -0,0 +1,121 @@
|
||||
-- CRM商机测试数据生成脚本
|
||||
-- 使用方法: docker exec -i hzhub-mysql mysql -uroot -phzhub123 hzhub < test_opportunity_data.sql
|
||||
|
||||
-- ========================================
|
||||
-- 1. 先创建经销商测试数据(如果需要)
|
||||
-- ========================================
|
||||
|
||||
-- 使用INSERT IGNORE避免重复插入
|
||||
INSERT IGNORE INTO crm_dealer (dealer_id, tenant_id, dealer_name, dealer_code, contact_name, mobile, province, city, level, lifecycle, owner_user_id, create_by, create_time, del_flag)
|
||||
VALUES
|
||||
(1001, '000000', '华东商贸有限公司', 'DL001', '张经理', '13800001001', '上海', '上海', 'A', 'active', 1, 1, NOW(), 0),
|
||||
(1002, '000000', '华南供应链集团', 'DL002', '李总监', '13800001002', '广东', '广州', 'A', 'active', 1, 1, NOW(), 0),
|
||||
(1003, '000000', '北方商贸股份', 'DL003', '王总', '13800001003', '北京', '北京', 'B', 'active', 2, 1, NOW(), 0),
|
||||
(1004, '000000', '西南贸易公司', 'DL004', '陈经理', '13800001004', '四川', '成都', 'B', 'active', 2, 1, NOW(), 0),
|
||||
(1005, '000000', '东北经销中心', 'DL005', '刘主管', '13800001005', '辽宁', '沈阳', 'C', 'active', 3, 1, NOW(), 0),
|
||||
(1006, '000000', '中部物流商贸', 'DL006', '赵经理', '13800001006', '湖北', '武汉', 'B', 'active', 1, 1, NOW(), 0),
|
||||
(1007, '000000', '沿海贸易集团', 'DL007', '孙总', '13800001007', '浙江', '杭州', 'A', 'active', 2, 1, NOW(), 0),
|
||||
(1008, '000000', '西部商贸公司', 'DL008', '周经理', '13800001008', '陕西', '西安', 'C', 'active', 3, 1, NOW(), 0),
|
||||
(1009, '000000', '南方快销商贸', 'DL009', '吴主管', '13800001009', '福建', '厦门', 'B', 'active', 1, 1, NOW(), 0),
|
||||
(1010, '000000', '京津冀经销站', 'DL010', '郑经理', '13800001010', '河北', '石家庄', 'C', 'active', 2, 1, NOW(), 0);
|
||||
|
||||
-- ========================================
|
||||
-- 2. 创建50条商机测试数据
|
||||
-- ========================================
|
||||
|
||||
-- lead阶段 - 15条(线索阶段,新商机)
|
||||
INSERT IGNORE INTO crm_opportunity (opportunity_id, tenant_id, dealer_id, opportunity_name, stage, amount, probability, expected_close_date, owner_user_id, product_name, description, status, create_by, create_time, update_by, update_time, del_flag)
|
||||
VALUES
|
||||
(2001, '000000', 1001, '首批进货合作意向', 'lead', 50000.00, 10, DATE_ADD(NOW(), INTERVAL 90 DAY), 1, '智能货架系统', '客户对智能货架产品有兴趣,初步接触阶段', 'active', 1, DATE_ADD(NOW(), INTERVAL -5 DAY), 1, NOW(), 0),
|
||||
(2002, '000000', 1002, '季度采购计划', 'lead', 120000.00, 10, DATE_ADD(NOW(), INTERVAL 85 DAY), 2, '仓储管理软件', '大型经销商季度采购需求', 'active', 1, DATE_ADD(NOW(), INTERVAL -3 DAY), 1, NOW(), 0),
|
||||
(2003, '000000', 1003, '新品推广合作', 'lead', 35000.00, 10, DATE_ADD(NOW(), INTERVAL 80 DAY), 1, '营销推广服务', '新品上市推广合作意向', 'active', 2, NOW(), 2, NOW(), 0),
|
||||
(2004, '000000', 1004, '门店升级项目', 'lead', 85000.00, 10, DATE_ADD(NOW(), INTERVAL 75 DAY), 2, '门店装修方案', '门店形象升级需求', 'active', 2, DATE_ADD(NOW(), INTERVAL -2 DAY), 2, NOW(), 0),
|
||||
(2005, '000000', 1005, '区域代理申请', 'lead', 20000.00, 10, DATE_ADD(NOW(), INTERVAL 70 DAY), 3, '区域代理权', '申请区域代理资格', 'active', 3, NOW(), 3, NOW(), 0),
|
||||
(2006, '000000', 1006, '物流系统对接', 'lead', 150000.00, 10, DATE_ADD(NOW(), INTERVAL 95 DAY), 1, '物流追踪系统', '物流管理系统对接需求', 'active', 1, DATE_ADD(NOW(), INTERVAL -7 DAY), 1, NOW(), 0),
|
||||
(2007, '000000', 1007, '年度框架协议', 'lead', 300000.00, 10, DATE_ADD(NOW(), INTERVAL 100 DAY), 2, '综合服务包', '年度合作协议框架', 'active', 2, DATE_ADD(NOW(), INTERVAL -4 DAY), 2, NOW(), 0),
|
||||
(2008, '000000', 1008, '本地化服务需求', 'lead', 45000.00, 10, DATE_ADD(NOW(), INTERVAL 65 DAY), 3, '本地运营支持', '本地化运营服务需求', 'active', 3, NOW(), 3, NOW(), 0),
|
||||
(2009, '000000', 1009, '电商渠道拓展', 'lead', 75000.00, 10, DATE_ADD(NOW(), INTERVAL 60 DAY), 1, '电商平台对接', '电商销售渠道拓展', 'active', 1, DATE_ADD(NOW(), INTERVAL -1 DAY), 1, NOW(), 0),
|
||||
(2010, '000000', 1010, '冷链仓储合作', 'lead', 180000.00, 10, DATE_ADD(NOW(), INTERVAL 88 DAY), 2, '冷链解决方案', '冷链仓储需求', 'active', 2, NOW(), 2, NOW(), 0),
|
||||
(2011, '000000', 1001, '二次采购意向', 'lead', 60000.00, 10, DATE_ADD(NOW(), INTERVAL 92 DAY), 1, '货架配件', '已有合作客户追加采购', 'active', 1, NOW(), 1, NOW(), 0),
|
||||
(2012, '000000', 1003, '促销活动支持', 'lead', 25000.00, 10, DATE_ADD(NOW(), INTERVAL 55 DAY), 2, '促销策划服务', '促销活动策划需求', 'active', 2, DATE_ADD(NOW(), INTERVAL -6 DAY), 2, NOW(), 0),
|
||||
(2013, '000000', 1005, '培训服务需求', 'lead', 15000.00, 10, DATE_ADD(NOW(), INTERVAL 50 DAY), 3, '员工培训课程', '员工技能培训需求', 'active', 3, NOW(), 3, NOW(), 0),
|
||||
(2014, '000000', 1007, '数据分析服务', 'lead', 40000.00, 10, DATE_ADD(NOW(), INTERVAL 45 DAY), 1, 'BI数据分析', '销售数据分析服务', 'active', 1, DATE_ADD(NOW(), INTERVAL -8 DAY), 1, NOW(), 0),
|
||||
(2015, '000000', 1009, '客户管理系统', 'lead', 95000.00, 10, DATE_ADD(NOW(), INTERVAL 58 DAY), 2, 'CRM系统定制', '客户管理需求', 'active', 2, NOW(), 2, NOW(), 0);
|
||||
|
||||
-- negotiation阶段 - 15条(谈判中)
|
||||
INSERT IGNORE INTO crm_opportunity (opportunity_id, tenant_id, dealer_id, opportunity_name, stage, amount, probability, expected_close_date, owner_user_id, product_name, description, status, create_by, create_time, update_by, update_time, del_flag)
|
||||
VALUES
|
||||
(2016, '000000', 1001, '大批量采购谈判', 'negotiation', 280000.00, 30, DATE_ADD(NOW(), INTERVAL 60 DAY), 1, '智能货架批量', '批量采购价格谈判', 'active', 1, DATE_ADD(NOW(), INTERVAL -20 DAY), 1, NOW(), 0),
|
||||
(2017, '000000', 1002, '系统集成方案', 'negotiation', 450000.00, 30, DATE_ADD(NOW(), INTERVAL 55 DAY), 2, '全系统集成', '系统集成方案谈判', 'active', 2, DATE_ADD(NOW(), INTERVAL -18 DAY), 2, NOW(), 0),
|
||||
(2018, '000000', 1003, '品牌合作推广', 'negotiation', 120000.00, 30, DATE_ADD(NOW(), INTERVAL 50 DAY), 1, '品牌联合推广', '品牌推广合作谈判', 'active', 1, DATE_ADD(NOW(), INTERVAL -15 DAY), 1, NOW(), 0),
|
||||
(2019, '000000', 1004, '区域扩展计划', 'negotiation', 200000.00, 30, DATE_ADD(NOW(), INTERVAL 48 DAY), 2, '区域扩张服务', '区域扩展方案谈判', 'active', 2, DATE_ADD(NOW(), INTERVAL -12 DAY), 2, NOW(), 0),
|
||||
(2020, '000000', 1005, '年度供货协议', 'negotiation', 350000.00, 30, DATE_ADD(NOW(), INTERVAL 45 DAY), 3, '年度供货合同', '年度协议谈判', 'active', 3, DATE_ADD(NOW(), INTERVAL -10 DAY), 3, NOW(), 0),
|
||||
(2021, '000000', 1006, '智慧门店方案', 'negotiation', 180000.00, 30, DATE_ADD(NOW(), INTERVAL 42 DAY), 1, '智慧门店系统', '智慧门店方案谈判', 'active', 1, DATE_ADD(NOW(), INTERVAL -25 DAY), 1, NOW(), 0),
|
||||
(2022, '000000', 1007, '营销自动化', 'negotiation', 95000.00, 30, DATE_ADD(NOW(), INTERVAL 40 DAY), 2, '营销自动化工具', '营销工具采购谈判', 'active', 2, DATE_ADD(NOW(), INTERVAL -22 DAY), 2, NOW(), 0),
|
||||
(2023, '000000', 1008, '供应链优化', 'negotiation', 250000.00, 30, DATE_ADD(NOW(), INTERVAL 38 DAY), 3, '供应链优化方案', '供应链优化谈判', 'active', 3, DATE_ADD(NOW(), INTERVAL -19 DAY), 3, NOW(), 0),
|
||||
(2024, '000000', 1009, '电商运营合作', 'negotiation', 150000.00, 30, DATE_ADD(NOW(), INTERVAL 35 DAY), 1, '电商运营服务', '电商运营合作谈判', 'active', 1, DATE_ADD(NOW(), INTERVAL -16 DAY), 1, NOW(), 0),
|
||||
(2025, '000000', 1010, '仓储物流升级', 'negotiation', 320000.00, 30, DATE_ADD(NOW(), INTERVAL 33 DAY), 2, '仓储升级方案', '仓储升级谈判', 'active', 2, DATE_ADD(NOW(), INTERVAL -14 DAY), 2, NOW(), 0),
|
||||
(2026, '000000', 1002, '增值服务采购', 'negotiation', 80000.00, 30, DATE_ADD(NOW(), INTERVAL 30 DAY), 2, '增值服务包', '增值服务采购谈判', 'active', 2, DATE_ADD(NOW(), INTERVAL -28 DAY), 2, NOW(), 0),
|
||||
(2027, '000000', 1004, '定制开发需求', 'negotiation', 220000.00, 30, DATE_ADD(NOW(), INTERVAL 28 DAY), 1, '定制开发服务', '定制开发方案谈判', 'active', 1, DATE_ADD(NOW(), INTERVAL -21 DAY), 1, NOW(), 0),
|
||||
(2028, '000000', 1006, '设备租赁合作', 'negotiation', 65000.00, 30, DATE_ADD(NOW(), INTERVAL 26 DAY), 3, '设备租赁方案', '设备租赁谈判', 'active', 3, DATE_ADD(NOW(), INTERVAL -17 DAY), 3, NOW(), 0),
|
||||
(2029, '000000', 1008, '数据服务合作', 'negotiation', 100000.00, 30, DATE_ADD(NOW(), INTERVAL 24 DAY), 2, '数据分析服务', '数据服务谈判', 'active', 2, DATE_ADD(NOW(), INTERVAL -11 DAY), 2, NOW(), 0),
|
||||
(2030, '000000', 1010, '技术支持服务', 'negotiation', 55000.00, 30, DATE_ADD(NOW(), INTERVAL 22 DAY), 1, '技术支持包', '技术支持谈判', 'active', 1, DATE_ADD(NOW(), INTERVAL -9 DAY), 1, NOW(), 0);
|
||||
|
||||
-- proposal阶段 - 12条(方案阶段)
|
||||
INSERT IGNORE INTO crm_opportunity (opportunity_id, tenant_id, dealer_id, opportunity_name, stage, amount, probability, expected_close_date, owner_user_id, product_name, description, status, create_by, create_time, update_by, update_time, del_flag)
|
||||
VALUES
|
||||
(2031, '000000', 1001, '大型项目方案', 'proposal', 580000.00, 50, DATE_ADD(NOW(), INTERVAL 30 DAY), 1, '全链条解决方案', '已提交方案等待审批', 'active', 1, DATE_ADD(NOW(), INTERVAL -35 DAY), 1, NOW(), 0),
|
||||
(2032, '000000', 1002, '年度战略合作', 'proposal', 850000.00, 50, DATE_ADD(NOW(), INTERVAL 28 DAY), 2, '战略合作方案', '方案已提交等待确认', 'active', 2, DATE_ADD(NOW(), INTERVAL -32 DAY), 2, NOW(), 0),
|
||||
(2033, '000000', 1003, '智慧零售方案', 'proposal', 380000.00, 50, DATE_ADD(NOW(), INTERVAL 25 DAY), 1, '智慧零售系统', '方案评审中', 'active', 1, DATE_ADD(NOW(), INTERVAL -30 DAY), 1, NOW(), 0),
|
||||
(2034, '000000', 1004, '区域代理方案', 'proposal', 250000.00, 50, DATE_ADD(NOW(), INTERVAL 23 DAY), 2, '代理合作方案', '代理方案已提交', 'active', 2, DATE_ADD(NOW(), INTERVAL -27 DAY), 2, NOW(), 0),
|
||||
(2035, '000000', 1005, '仓储改造方案', 'proposal', 420000.00, 50, DATE_ADD(NOW(), INTERVAL 20 DAY), 3, '仓储改造计划', '改造方案等待审批', 'active', 3, DATE_ADD(NOW(), INTERVAL -25 DAY), 3, NOW(), 0),
|
||||
(2036, '000000', 1006, '品牌升级方案', 'proposal', 180000.00, 50, DATE_ADD(NOW(), INTERVAL 18 DAY), 1, '品牌升级服务', '品牌方案评审中', 'active', 1, DATE_ADD(NOW(), INTERVAL -40 DAY), 1, NOW(), 0),
|
||||
(2037, '000000', 1007, '营销整合方案', 'proposal', 320000.00, 50, DATE_ADD(NOW(), INTERVAL 15 DAY), 2, '营销整合服务', '营销方案已提交', 'active', 2, DATE_ADD(NOW(), INTERVAL -38 DAY), 2, NOW(), 0),
|
||||
(2038, '000000', 1008, '供应链方案', 'proposal', 450000.00, 50, DATE_ADD(NOW(), INTERVAL 12 DAY), 3, '供应链整合', '供应链方案等待审批', 'active', 3, DATE_ADD(NOW(), INTERVAL -36 DAY), 3, NOW(), 0),
|
||||
(2039, '000000', 1009, '电商平台方案', 'proposal', 280000.00, 50, DATE_ADD(NOW(), INTERVAL 10 DAY), 1, '电商全案', '电商方案评审中', 'active', 1, DATE_ADD(NOW(), INTERVAL -33 DAY), 1, NOW(), 0),
|
||||
(2040, '000000', 1010, '物流优化方案', 'proposal', 350000.00, 50, DATE_ADD(NOW(), INTERVAL 8 DAY), 2, '物流优化服务', '物流方案已提交', 'active', 2, DATE_ADD(NOW(), INTERVAL -29 DAY), 2, NOW(), 0),
|
||||
(2041, '000000', 1003, '培训体系建设', 'proposal', 120000.00, 50, DATE_ADD(NOW(), INTERVAL 15 DAY), 1, '培训体系建设', '培训方案等待审批', 'active', 1, DATE_ADD(NOW(), INTERVAL -42 DAY), 1, NOW(), 0),
|
||||
(2042, '000000', 1007, '客户运营方案', 'proposal', 200000.00, 50, DATE_ADD(NOW(), INTERVAL 12 DAY), 2, '客户运营服务', '运营方案评审中', 'active', 2, DATE_ADD(NOW(), INTERVAL -44 DAY), 2, NOW(), 0);
|
||||
|
||||
-- closing阶段 - 8条(赢单阶段,本月和上月分布)
|
||||
INSERT IGNORE INTO crm_opportunity (opportunity_id, tenant_id, dealer_id, opportunity_name, stage, amount, probability, expected_close_date, owner_user_id, product_name, description, status, create_by, create_time, update_by, update_time, del_flag)
|
||||
VALUES
|
||||
(2043, '000000', 1001, '首批进货合同', 'closing', 680000.00, 90, DATE_ADD(NOW(), INTERVAL 5 DAY), 1, '智能货架系统', '合同已签订,准备执行', 'active', 1, DATE_ADD(NOW(), INTERVAL -45 DAY), 1, NOW(), 0),
|
||||
(2044, '000000', 1002, '年度框架合同', 'closing', 1200000.00, 90, NOW(), 2, '综合服务包', '年度合同已签订', 'active', 2, DATE_ADD(NOW(), INTERVAL -50 DAY), 2, NOW(), 0),
|
||||
(2045, '000000', 1003, '区域代理合同', 'closing', 350000.00, 90, DATE_ADD(NOW(), INTERVAL -3 DAY), 1, '区域代理权', '代理合同已签订', 'active', 1, DATE_ADD(NOW(), INTERVAL -48 DAY), 1, NOW(), 0),
|
||||
(2046, '000000', 1004, '仓储改造合同', 'closing', 520000.00, 90, DATE_ADD(NOW(), INTERVAL -7 DAY), 2, '仓储改造计划', '改造合同已签订', 'active', 2, DATE_ADD(NOW(), INTERVAL -52 DAY), 2, NOW(), 0),
|
||||
-- 上月赢单(用于计算环比)
|
||||
(2047, '000000', 1005, '上月签约项目A', 'closing', 450000.00, 90, DATE_ADD(NOW(), INTERVAL -35 DAY), 3, '物流方案', '上月赢单案例', 'active', 3, DATE_ADD(NOW(), INTERVAL -55 DAY), 3, NOW(), 0),
|
||||
(2048, '000000', 1006, '上月签约项目B', 'closing', 280000.00, 90, DATE_ADD(NOW(), INTERVAL -38 DAY), 1, '营销服务', '上月赢单案例', 'active', 1, DATE_ADD(NOW(), INTERVAL -58 DAY), 1, NOW(), 0),
|
||||
(2049, '000000', 1007, '上月签约项目C', 'closing', 380000.00, 90, DATE_ADD(NOW(), INTERVAL -40 DAY), 2, '数据服务', '上月赢单案例', 'active', 2, DATE_ADD(NOW(), INTERVAL -62 DAY), 2, NOW(), 0),
|
||||
(2050, '000000', 1008, '上月签约项目D', 'closing', 150000.00, 90, DATE_ADD(NOW(), INTERVAL -42 DAY), 3, '培训服务', '上月赢单案例', 'active', 3, DATE_ADD(NOW(), INTERVAL -65 DAY), 3, NOW(), 0);
|
||||
|
||||
-- ========================================
|
||||
-- 3. 验证数据插入结果
|
||||
-- ========================================
|
||||
SELECT '=== 各阶段商机统计 ===' as title;
|
||||
SELECT
|
||||
stage,
|
||||
COUNT(*) as count,
|
||||
SUM(amount) as total_amount,
|
||||
ROUND(AVG(amount), 2) as avg_amount
|
||||
FROM crm_opportunity
|
||||
WHERE del_flag = 0
|
||||
GROUP BY stage
|
||||
ORDER BY FIELD(stage, 'lead', 'negotiation', 'proposal', 'closing');
|
||||
|
||||
SELECT '=== 赢单月度统计 ===' as title;
|
||||
SELECT
|
||||
CASE
|
||||
WHEN MONTH(create_time) = MONTH(NOW()) AND YEAR(create_time) = YEAR(NOW()) THEN '本月'
|
||||
WHEN MONTH(create_time) = MONTH(DATE_SUB(NOW(), INTERVAL 1 MONTH)) AND YEAR(create_time) = YEAR(DATE_SUB(NOW(), INTERVAL 1 MONTH)) THEN '上月'
|
||||
ELSE '其他'
|
||||
END as month_period,
|
||||
COUNT(*) as win_count,
|
||||
SUM(amount) as win_amount
|
||||
FROM crm_opportunity
|
||||
WHERE stage = 'closing' AND del_flag = 0
|
||||
GROUP BY month_period;
|
||||
|
||||
SELECT '=== 经销商数据确认 ===' as title;
|
||||
SELECT dealer_id, dealer_name, dealer_code FROM crm_dealer WHERE del_flag = 0 LIMIT 10;
|
||||
@@ -0,0 +1,36 @@
|
||||
package org.hzhub.crm.controller;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.hzhub.common.core.domain.R;
|
||||
import org.hzhub.crm.domain.vo.CrmDealerVo;
|
||||
import org.hzhub.crm.service.ICrmDealerService;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* CRM经销商管理 Controller
|
||||
* 员工门户版本(无需Sa-Token权限注解,权限由Gateway控制)
|
||||
*
|
||||
* 注意:Gateway已配置 /crm/** 路由并StripPrefix=1(去除/crm前缀)
|
||||
* 所以Controller使用 /dealer 路径,实际对外接口为 /crm/dealer/**
|
||||
*
|
||||
* @author hzhub
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/dealer")
|
||||
public class CrmDealerController {
|
||||
|
||||
private final ICrmDealerService dealerService;
|
||||
|
||||
/**
|
||||
* 员工门户经销商选择器列表
|
||||
* 用于商机创建、商机分配等场景
|
||||
*/
|
||||
@GetMapping("/portal/select")
|
||||
public R<List<CrmDealerVo>> portalSelect(@RequestParam(required = false) String keyword) {
|
||||
List<CrmDealerVo> dealerList = dealerService.selectDealerList(keyword);
|
||||
return R.ok(dealerList);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@ import org.hzhub.common.mybatis.core.page.TableDataInfo;
|
||||
import org.hzhub.crm.domain.bo.CrmDealerBo;
|
||||
import org.hzhub.crm.domain.vo.CrmDealerVo;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* CRM经销商 Service 接口
|
||||
*
|
||||
@@ -60,4 +62,12 @@ public interface ICrmDealerService {
|
||||
* @return 结果
|
||||
*/
|
||||
boolean checkDealerCodeUnique(CrmDealerBo dealer);
|
||||
|
||||
/**
|
||||
* 查询经销商列表(用于选择器)
|
||||
*
|
||||
* @param keyword 搜索关键词(经销商名称/编码)
|
||||
* @return 经销商列表
|
||||
*/
|
||||
List<CrmDealerVo> selectDealerList(String keyword);
|
||||
}
|
||||
@@ -106,4 +106,18 @@ public class CrmDealerServiceImpl implements ICrmDealerService {
|
||||
.ne(ObjectUtil.isNotNull(dealer.getDealerId()), CrmDealer::getDealerId, dealer.getDealerId()));
|
||||
return !exist;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CrmDealerVo> selectDealerList(String keyword) {
|
||||
LambdaQueryWrapper<CrmDealer> wrapper = Wrappers.lambdaQuery();
|
||||
wrapper.eq(CrmDealer::getDelFlag, SystemConstants.NORMAL)
|
||||
.eq(CrmDealer::getLifecycle, "active") // 只返回活跃状态的经销商
|
||||
.and(StringUtils.isNotBlank(keyword), w -> w
|
||||
.like(CrmDealer::getDealerName, keyword)
|
||||
.or()
|
||||
.like(CrmDealer::getDealerCode, keyword))
|
||||
.orderByDesc(CrmDealer::getCreateTime)
|
||||
.last("LIMIT 100"); // 限制返回100条
|
||||
return dealerMapper.selectVoList(wrapper);
|
||||
}
|
||||
}
|
||||
@@ -290,22 +290,12 @@ public class CrmLeadServiceImpl implements ICrmLeadService {
|
||||
throw new ServiceException("创建经销商失败");
|
||||
}
|
||||
|
||||
// 4. 创建初始商机(线索转化时自动创建)
|
||||
int opportunityResult = opportunityService.createInitialOpportunity(
|
||||
dealer.getDealerId(),
|
||||
lead.getLeadId(),
|
||||
lead.getOwnerUserId()
|
||||
);
|
||||
if (opportunityResult <= 0) {
|
||||
log.warn("创建初始商机失败,但经销商创建成功: dealerId={}", dealer.getDealerId());
|
||||
}
|
||||
|
||||
// 5. 更新线索状态
|
||||
// 4. 更新线索状态
|
||||
lead.setLeadStatus("converted");
|
||||
lead.setConvertedDealerId(dealer.getDealerId());
|
||||
int leadResult = leadMapper.updateById(lead);
|
||||
|
||||
log.info("线索转化成功: leadId={}, dealerId={}, dealerCode={}, opportunityId已创建",
|
||||
log.info("线索转化成功: leadId={}, dealerId={}, dealerCode={}",
|
||||
lead.getLeadId(), dealer.getDealerId(), dealer.getDealerCode());
|
||||
|
||||
return leadResult;
|
||||
|
||||
Reference in New Issue
Block a user