296 lines
9.8 KiB
Vue
296 lines
9.8 KiB
Vue
<template>
|
||
<div class="crm-page">
|
||
<div class="page-header">
|
||
<div>
|
||
<h2>商机管理</h2>
|
||
<p>推进商机阶段,达成后可转化为项目</p>
|
||
</div>
|
||
<div class="header-actions">
|
||
<el-button :icon="Refresh" @click="fetchList">刷新</el-button>
|
||
<el-button type="primary" :icon="Plus" @click="openCreate">新增商机</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="filter-bar">
|
||
<el-form :inline="true" :model="filters" @submit.prevent>
|
||
<el-form-item>
|
||
<el-input
|
||
v-model="filters.keyword"
|
||
clearable
|
||
placeholder="搜索商机名称 / 客户 / 对接人"
|
||
:prefix-icon="Search"
|
||
style="width: 260px"
|
||
@keyup.enter="handleSearch"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="商机阶段">
|
||
<el-select v-model="filters.stage" clearable placeholder="全部" style="width: 130px">
|
||
<el-option v-for="i in BUSINESS_STAGE_OPTIONS" :key="i.value" :label="i.label" :value="i.value" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="级别">
|
||
<el-select v-model="filters.level" clearable placeholder="全部" style="width: 110px">
|
||
<el-option v-for="i in PIPELINE_LEVEL_OPTIONS" :key="i.value" :label="i.label" :value="i.value" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="状态">
|
||
<el-select v-model="filters.status" clearable placeholder="全部" style="width: 130px">
|
||
<el-option v-for="i in BUSINESS_STATUS_OPTIONS" :key="i.value" :label="i.label" :value="i.value" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button type="primary" :icon="Search" @click="handleSearch">查询</el-button>
|
||
<el-button :icon="Refresh" @click="resetFilters">重置</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
</div>
|
||
|
||
<div class="table-container" v-loading="loading">
|
||
<el-table :data="tableData" stripe border row-key="id">
|
||
<el-table-column label="状态" width="110" align="center" fixed>
|
||
<template #default="{ row }">
|
||
<el-tag :type="businessStatusTag(row.status)" size="small">{{ businessStatusText(row.status) }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="商机名称" min-width="170" show-overflow-tooltip fixed>
|
||
<template #default="{ row }">
|
||
<span class="name-link" @click="openDetail(row)">{{ row.business_name }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="customer_name" label="客户名称" min-width="160" show-overflow-tooltip />
|
||
<el-table-column label="商机阶段" width="110" align="center">
|
||
<template #default="{ row }">
|
||
<el-tag :type="businessStageTag(row.stage)" size="small">{{ businessStageText(row.stage) }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="级别" width="90" align="center">
|
||
<template #default="{ row }">
|
||
<el-tag :type="pipelineLevelTag(row.level)" size="small" effect="plain">
|
||
{{ pipelineLevelText(row.level) }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="预计金额" width="130" align="right">
|
||
<template #default="{ row }">{{ formatMoney(row.amount) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="预计成交日期" width="130" align="center">
|
||
<template #default="{ row }">{{ formatDateOnly(row.expect_deal_date) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="负责人" width="100">
|
||
<template #default="{ row }">{{ row.owner_user_name || "-" }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="下次联系时间" width="160" align="center">
|
||
<template #default="{ row }">{{ formatDateTime(row.next_contact_time) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="280" align="center" fixed="right">
|
||
<template #default="{ row }">
|
||
<el-button link type="primary" size="small" :disabled="row.locked === 1" @click="openEdit(row)">
|
||
编辑
|
||
</el-button>
|
||
<el-button
|
||
link
|
||
type="warning"
|
||
size="small"
|
||
:disabled="row.locked === 1 || row.status === 2 || row.status === 3"
|
||
@click="openConvert(row)"
|
||
>
|
||
转项目
|
||
</el-button>
|
||
<el-button
|
||
v-if="row.status === 3"
|
||
link
|
||
type="success"
|
||
size="small"
|
||
:disabled="row.locked === 1"
|
||
@click="handleRestore(row)"
|
||
>
|
||
恢复
|
||
</el-button>
|
||
<el-button
|
||
v-else
|
||
link
|
||
type="info"
|
||
size="small"
|
||
:disabled="row.locked === 1 || row.status === 2"
|
||
@click="handleDiscard(row)"
|
||
>
|
||
废弃
|
||
</el-button>
|
||
<el-button
|
||
link
|
||
type="danger"
|
||
size="small"
|
||
:disabled="row.locked === 1 || row.status === 2"
|
||
@click="handleDelete(row)"
|
||
>
|
||
删除
|
||
</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
<template #empty><el-empty description="暂无商机" :image-size="80" /></template>
|
||
</el-table>
|
||
|
||
<div class="pagination">
|
||
<el-pagination
|
||
v-model:current-page="pagination.page"
|
||
v-model:page-size="pagination.pageSize"
|
||
:total="pagination.total"
|
||
:page-sizes="[10, 20, 50, 100]"
|
||
layout="total, sizes, prev, pager, next, jumper"
|
||
@size-change="handleSearch"
|
||
@current-change="fetchList"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<BusinessEdit v-model:visible="editVisible" :edit-data="currentRow" @success="fetchList" />
|
||
<BusinessConvert v-model:visible="convertVisible" :business="currentRow" @success="handleConvertSuccess" />
|
||
|
||
<BusinessDetail
|
||
v-model:visible="detailVisible"
|
||
:business="currentRow"
|
||
@refresh="fetchList"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, reactive, onMounted } from "vue";
|
||
import { ElMessage, ElMessageBox } from "element-plus";
|
||
import { Plus, Search, Refresh } from "@element-plus/icons-vue";
|
||
import { getBusinessList, deleteBusiness, changeBusinessStatus } from "@/api/crmPipeline";
|
||
import BusinessEdit from "./components/edit.vue";
|
||
import BusinessConvert from "./components/convert.vue";
|
||
import BusinessDetail from "./components/detail.vue";
|
||
import {
|
||
BUSINESS_STAGE_OPTIONS,
|
||
BUSINESS_STATUS_OPTIONS,
|
||
PIPELINE_LEVEL_OPTIONS,
|
||
businessStageText,
|
||
businessStageTag,
|
||
businessStatusText,
|
||
businessStatusTag,
|
||
pipelineLevelText,
|
||
pipelineLevelTag,
|
||
formatMoney,
|
||
formatDateOnly,
|
||
formatDateTime,
|
||
} from "../dict";
|
||
|
||
const loading = ref(false);
|
||
const tableData = ref([]);
|
||
const editVisible = ref(false);
|
||
const convertVisible = ref(false);
|
||
const detailVisible = ref(false);
|
||
const currentRow = ref(null);
|
||
|
||
const filters = reactive({ keyword: "", stage: "", level: "", status: "" });
|
||
const pagination = reactive({ page: 1, pageSize: 20, total: 0 });
|
||
|
||
onMounted(() => {
|
||
fetchList();
|
||
});
|
||
|
||
async function fetchList() {
|
||
loading.value = true;
|
||
try {
|
||
const res = await getBusinessList({
|
||
page: pagination.page,
|
||
pageSize: pagination.pageSize,
|
||
...filters,
|
||
});
|
||
tableData.value = res?.data?.list || [];
|
||
pagination.total = res?.data?.total || 0;
|
||
} catch (e) {
|
||
tableData.value = [];
|
||
pagination.total = 0;
|
||
ElMessage.error(e.message || "查询失败");
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
|
||
function handleSearch() {
|
||
pagination.page = 1;
|
||
fetchList();
|
||
}
|
||
|
||
function resetFilters() {
|
||
filters.keyword = "";
|
||
filters.stage = "";
|
||
filters.level = "";
|
||
filters.status = "";
|
||
handleSearch();
|
||
}
|
||
|
||
function openCreate() {
|
||
currentRow.value = null;
|
||
editVisible.value = true;
|
||
}
|
||
|
||
function openEdit(row) {
|
||
currentRow.value = { ...row };
|
||
editVisible.value = true;
|
||
}
|
||
|
||
function openConvert(row) {
|
||
currentRow.value = { ...row };
|
||
convertVisible.value = true;
|
||
}
|
||
|
||
function openDetail(row) {
|
||
currentRow.value = { ...row };
|
||
detailVisible.value = true;
|
||
}
|
||
|
||
function handleConvertSuccess() {
|
||
fetchList();
|
||
ElMessage.success("商机已转化为项目,可在项目管理中继续跟进");
|
||
}
|
||
|
||
/** 废弃商机:保留全部过程数据,仅把状态置为已废弃,可随时恢复 */
|
||
async function handleDiscard(row) {
|
||
try {
|
||
await ElMessageBox.confirm(
|
||
`确定废弃商机「${row.business_name}」吗?废弃后可随时恢复为「跟进中」。`,
|
||
"废弃确认",
|
||
{ type: "warning" }
|
||
);
|
||
await changeBusinessStatus(row.id, 3);
|
||
ElMessage.success("商机已废弃");
|
||
fetchList();
|
||
} catch (e) {
|
||
if (e !== "cancel") ElMessage.error(e.message || "废弃失败");
|
||
}
|
||
}
|
||
|
||
/** 恢复商机:把已废弃的商机恢复为跟进中 */
|
||
async function handleRestore(row) {
|
||
try {
|
||
await ElMessageBox.confirm(`确定将商机「${row.business_name}」恢复为「跟进中」吗?`, "恢复确认", {
|
||
type: "info",
|
||
});
|
||
await changeBusinessStatus(row.id, 1);
|
||
ElMessage.success("商机已恢复为跟进中");
|
||
fetchList();
|
||
} catch (e) {
|
||
if (e !== "cancel") ElMessage.error(e.message || "恢复失败");
|
||
}
|
||
}
|
||
|
||
async function handleDelete(row) {
|
||
try {
|
||
await ElMessageBox.confirm(`确定删除商机「${row.business_name}」吗?删除后不可恢复。`, "删除确认", {
|
||
type: "warning",
|
||
});
|
||
await deleteBusiness(row.id);
|
||
ElMessage.success("删除成功");
|
||
fetchList();
|
||
} catch (e) {
|
||
if (e !== "cancel") ElMessage.error(e.message || "删除失败");
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="less" scoped src="../styles/crm-page.less"></style>
|