增加商机线索管理修复若干bug

This commit is contained in:
2026-09-11 11:11:43 +08:00
parent b9cdabe045
commit 9c07af2b59
39 changed files with 6219 additions and 42 deletions
+238
View File
@@ -0,0 +1,238 @@
<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="线索名称" 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="90" align="center">
<template #default="{ row }">
<el-tag :type="clueStatusTag(row.status)" size="small">{{ clueStatusText(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="220" 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"
@click="openConvert(row)"
>
转化
</el-button>
<el-button link type="danger" size="small" @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>
<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"
/>
</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 openEdit(row) {
currentEditData.value = { ...row };
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("线索已转化,可在商机管理中继续跟进");
}
async function handleDelete(row) {
try {
await ElMessageBox.confirm(`确定删除线索「${row.clue_name}」吗?删除后不可恢复。`, "删除确认", {
type: "warning",
});
await deleteClue(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>