Files
shanxia2.0/web-admin/src/components/OpportunitySelector.vue
T
2026-08-23 09:47:38 +08:00

275 lines
9.3 KiB
Vue

<template>
<el-dialog
:title="dialogTitle"
v-model="dialogVisible"
width="1200px"
align-center
class="dialog-form-layout"
modal-class="order-module-dialog-mask"
:close-on-click-modal="false"
append-to-body
>
<!-- 提示信息 -->
<el-alert v-if="props.customerId" :title="`已筛选客户的商机`" type="info" :closable="false" class="mb-3" show-icon />
<!-- 查询表单 -->
<el-form :model="queryParams" ref="queryFormRef" :inline="true" class="mb-3 search-form-inline">
<el-form-item label="商机名称" prop="opportunityName">
<el-input v-model="queryParams.opportunityName" placeholder="请输入商机名称" clearable @keyup.enter="handleQuery" style="width: 180px" />
</el-form-item>
<el-form-item label="商机编号" prop="opportunityNo">
<el-input v-model="queryParams.opportunityNo" placeholder="请输入商机编号" clearable @keyup.enter="handleQuery" style="width: 150px" />
</el-form-item>
<el-form-item label="商机阶段" prop="stage">
<el-select v-model="queryParams.stage" placeholder="请选择商机阶段" clearable style="width: 150px">
<el-option v-for="dict in dictStore.getOptions('crm_opportunity_stage')" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="商机状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable style="width: 120px">
<el-option v-for="dict in dictStore.getOptions('crm_opportunity_status')" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item class="form-buttons-right">
<el-button type="primary" :loading="loading" @click="handleQuery">
<el-icon><Search /></el-icon>搜索
</el-button>
<el-button :loading="loading" @click="resetQuery">
<el-icon><Refresh /></el-icon>重置
</el-button>
</el-form-item>
</el-form>
<!-- 商机列表 -->
<div class="panel dialog-list-panel">
<div class="list-body list-wrap dialog-list-body-only">
<el-table
v-loading="loading"
:data="opportunityList"
class="list-table"
stripe
highlight-current-row
@current-change="handleCurrentChange"
:max-height="400"
>
<el-table-column type="index" label="序号" width="60" align="center" header-align="center" />
<el-table-column label="商机编号" prop="opportunityNo" min-width="140" align="center" header-align="center" :show-overflow-tooltip="true" />
<el-table-column label="商机名称" prop="opportunityName" min-width="180" align="center" header-align="center" :show-overflow-tooltip="true" />
<el-table-column label="客户名称" prop="customerName" min-width="150" align="center" header-align="center" :show-overflow-tooltip="true" />
<el-table-column label="商机阶段" prop="stage" min-width="120" align="center" header-align="center">
<template #default="scope">
<DictTag dict-type="crm_opportunity_stage" :value="scope.row.stage" />
</template>
</el-table-column>
<el-table-column label="预计成交率" prop="winRate" min-width="100" align="center" header-align="center">
<template #default="scope">
<el-progress :percentage="scope.row.winRate || 0" :color="getWinRateColor(scope.row.winRate)" />
</template>
</el-table-column>
<el-table-column label="预计成交日期" prop="expectedCloseDate" min-width="120" align="center" header-align="center">
<template #default="scope">
<span>{{ scope.row.expectedCloseDate || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="负责人" prop="ownerUserName" min-width="100" align="center" header-align="center" :show-overflow-tooltip="true" />
<el-table-column label="状态" prop="status" min-width="80" align="center" header-align="center">
<template #default="scope">
<DictTag dict-type="crm_opportunity_status" :value="scope.row.status" />
</template>
</el-table-column>
</el-table>
<ListPagination
v-show="total > 0"
:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</div>
</div>
<!-- 当前选中商机 -->
<template v-if="currentOpportunity">
<el-divider content-position="left">当前选中商机</el-divider>
<div class="selected-opportunity">
<el-descriptions :column="3" border>
<el-descriptions-item label="商机名称">{{ currentOpportunity.opportunityName }}</el-descriptions-item>
<el-descriptions-item label="商机编号">{{ currentOpportunity.opportunityNo }}</el-descriptions-item>
<el-descriptions-item label="客户名称">{{ currentOpportunity.customerName }}</el-descriptions-item>
<el-descriptions-item label="商机阶段">
<DictTag dict-type="crm_opportunity_stage" :value="currentOpportunity.stage" />
</el-descriptions-item>
<el-descriptions-item label="预计成交率">{{ currentOpportunity.winRate || 0 }}%</el-descriptions-item>
<el-descriptions-item label="预计成交日期">{{ currentOpportunity.expectedCloseDate || '-' }}</el-descriptions-item>
</el-descriptions>
</div>
</template>
<template #footer>
<div class="dialog-footer">
<el-button class="btn-ghost" @click="handleClose">
<el-icon><Close /></el-icon>取消
</el-button>
<el-button class="btn-main" type="primary" @click="handleConfirm" :disabled="!currentOpportunity">
<el-icon><Check /></el-icon>确定
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="OpportunitySelector">
import { useDictStore } from '@/store/modules/dict'
import DictTag from '@/components/DictTag/index.vue'
import ListPagination from '@/components/ListPagination/index.vue'
import { Check, Close, Refresh, Search } from '@element-plus/icons-vue'
import { ref, watch, computed } from 'vue'
import type { ElFormInstance } from 'element-plus'
import { listOpportunity } from '@/api/crm/opportunity'
import type { OpportunityQuery, OpportunityVO } from '@/api/crm/opportunity/types'
interface Props {
visible: boolean
customerId?: number | string
}
const props = defineProps<Props>()
const dictStore = useDictStore()
const dialogTitle = computed(() => {
return props.customerId ? '选择商机(已筛选当前客户)' : '选择商机'
})
const emit = defineEmits(['update:visible', 'confirm'])
const dialogVisible = ref(false)
watch(
() => props.visible,
async (val) => {
dialogVisible.value = val
if (val) {
await dictStore.loadAllDict()
getList()
}
}
)
watch(
() => props.customerId,
(val) => {
if (dialogVisible.value) {
queryParams.value.customerId = val
getList()
}
}
)
watch(dialogVisible, (val) => {
emit('update:visible', val)
})
const queryParams = ref<OpportunityQuery>({
pageNum: 1,
pageSize: 20,
opportunityName: undefined,
opportunityNo: undefined,
customerId: props.customerId,
stage: undefined,
status: 'following'
})
const opportunityList = ref<OpportunityVO[]>([])
const loading = ref(false)
const total = ref(0)
const currentOpportunity = ref<OpportunityVO | null>(null)
const queryFormRef = ref<ElFormInstance>()
const getList = async () => {
loading.value = true
try {
queryParams.value.customerId = props.customerId
const res = await listOpportunity({
...queryParams.value,
customerId: props.customerId != null ? String(props.customerId) : undefined
})
opportunityList.value = res.rows || []
total.value = res.total ?? 0
} catch (error) {
console.error('获取商机列表失败:', error)
} finally {
loading.value = false
}
}
const handleQuery = () => {
queryParams.value.pageNum = 1
getList()
}
const resetQuery = () => {
queryFormRef.value?.resetFields()
handleQuery()
}
watchDebounced(
() => ({
opportunityName: queryParams.value.opportunityName,
opportunityNo: queryParams.value.opportunityNo,
stage: queryParams.value.stage,
status: queryParams.value.status
}),
() => {
queryParams.value.pageNum = 1
getList()
},
{ debounce: 300, deep: true }
)
const handleCurrentChange = (row: OpportunityVO) => {
currentOpportunity.value = row
}
const handleConfirm = () => {
emit('confirm', currentOpportunity.value)
handleClose()
}
const handleClose = () => {
currentOpportunity.value = null
dialogVisible.value = false
}
const getWinRateColor = (winRate: number) => {
if (winRate >= 80) return '#67c23a'
if (winRate >= 60) return '#409eff'
if (winRate >= 40) return '#e6a23c'
return '#f56c6c'
}
</script>
<style scoped>
.selected-opportunity {
padding: 10px;
background-color: #f5f7fa;
border-radius: 4px;
}
.mb-3 {
margin-bottom: 16px;
}
.mt-3 {
margin-top: 16px;
}
.search-form-inline {
display: flex;
flex-wrap: wrap;
}
.form-buttons-right {
margin-left: auto !important;
}
</style>