252 lines
8.5 KiB
Vue
252 lines
8.5 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.source" clearable placeholder="全部" style="width: 130px">
|
||
<el-option v-for="i in CLUE_SOURCE_OPTIONS" :key="i.value" :label="i.label" :value="i.value" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="客户级别">
|
||
<el-select v-model="filters.clue_level" clearable placeholder="全部" style="width: 120px">
|
||
<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: 120px">
|
||
<el-option v-for="i in CLUE_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="90" align="center" fixed>
|
||
<template #default="{ row }">
|
||
<el-tag :type="clueStatusTag(row.status)" size="small">{{ clueStatusText(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.clue_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 }">{{ clueSourceText(row.source) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="客户级别" width="100" align="center">
|
||
<template #default="{ row }">
|
||
<el-tag :type="pipelineLevelTag(row.clue_level)" size="small">
|
||
{{ pipelineLevelText(row.clue_level) }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="industry" label="客户行业" width="120" show-overflow-tooltip />
|
||
<el-table-column label="负责人" width="100">
|
||
<template #default="{ row }">{{ row.owner_user_name || "-" }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="对接人" width="100">
|
||
<template #default="{ row }">{{ row.contact_person || "-" }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="对接人手机" width="130">
|
||
<template #default="{ row }">{{ row.contact_phone || "-" }}</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="160" align="center">
|
||
<template #default="{ row }">{{ formatDateTime(row.create_time) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="100" align="center" fixed="right">
|
||
<template #default="{ row }">
|
||
<!-- 编辑 / 删除已收口到「线索详情」内,列表只保留转化 -->
|
||
<el-button
|
||
link
|
||
type="warning"
|
||
size="small"
|
||
:disabled="row.locked === 1 || row.status === 2"
|
||
@click="openConvert(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>
|
||
|
||
<ClueEdit v-model:visible="editVisible" :edit-data="currentEditData" @success="fetchList" />
|
||
<ClueConvert v-model:visible="convertVisible" :clue="currentEditData" @success="handleConvertSuccess" />
|
||
<ClueDetail
|
||
v-model:visible="detailVisible"
|
||
:clue="currentEditData"
|
||
@refresh="fetchList"
|
||
@edit="handleDetailEdit"
|
||
@delete="handleDetailDelete"
|
||
/>
|
||
</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 { getClueList, deleteClue } from "@/api/crmPipeline";
|
||
import ClueEdit from "./components/edit.vue";
|
||
import ClueConvert from "./components/convert.vue";
|
||
import ClueDetail from "./components/detail.vue";
|
||
import {
|
||
CLUE_SOURCE_OPTIONS,
|
||
CLUE_STATUS_OPTIONS,
|
||
PIPELINE_LEVEL_OPTIONS,
|
||
clueSourceText,
|
||
pipelineLevelText,
|
||
pipelineLevelTag,
|
||
clueStatusText,
|
||
clueStatusTag,
|
||
formatDateTime,
|
||
} from "../dict";
|
||
|
||
const loading = ref(false);
|
||
const tableData = ref([]);
|
||
const editVisible = ref(false);
|
||
const convertVisible = ref(false);
|
||
const detailVisible = ref(false);
|
||
const currentEditData = ref(null);
|
||
|
||
const filters = reactive({
|
||
keyword: "",
|
||
source: "",
|
||
clue_level: "",
|
||
status: "",
|
||
});
|
||
|
||
const pagination = reactive({ page: 1, pageSize: 20, total: 0 });
|
||
|
||
onMounted(() => {
|
||
fetchList();
|
||
});
|
||
|
||
async function fetchList() {
|
||
loading.value = true;
|
||
try {
|
||
const res = await getClueList({
|
||
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.source = "";
|
||
filters.clue_level = "";
|
||
filters.status = "";
|
||
handleSearch();
|
||
}
|
||
|
||
function openCreate() {
|
||
currentEditData.value = null;
|
||
editVisible.value = true;
|
||
}
|
||
|
||
function openConvert(row) {
|
||
currentEditData.value = { ...row };
|
||
convertVisible.value = true;
|
||
}
|
||
|
||
function openDetail(row) {
|
||
currentEditData.value = { ...row };
|
||
detailVisible.value = true;
|
||
}
|
||
|
||
function handleConvertSuccess() {
|
||
fetchList();
|
||
ElMessage.success("线索已转化,可在商机管理中继续跟进");
|
||
}
|
||
|
||
/** 详情内编辑:收起详情后打开编辑弹窗(详情数据即当前行) */
|
||
function handleDetailEdit() {
|
||
detailVisible.value = false;
|
||
editVisible.value = true;
|
||
}
|
||
|
||
/** 详情内删除:复用删除逻辑,删除成功后收起详情 */
|
||
async function handleDetailDelete() {
|
||
const row = currentEditData.value;
|
||
if (!row) return;
|
||
if (await handleDelete(row)) detailVisible.value = false;
|
||
}
|
||
|
||
/** 删除线索:返回是否删除成功,供详情内调用判断是否需要收起抽屉 */
|
||
async function handleDelete(row) {
|
||
try {
|
||
await ElMessageBox.confirm(`确定删除线索「${row.clue_name}」吗?删除后不可恢复。`, "删除确认", {
|
||
type: "warning",
|
||
});
|
||
await deleteClue(row.id);
|
||
ElMessage.success("删除成功");
|
||
fetchList();
|
||
return true;
|
||
} catch (e) {
|
||
if (e !== "cancel") ElMessage.error(e.message || "删除失败");
|
||
return false;
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="less" scoped src="../styles/crm-page.less"></style>
|