Files
yunzerwebsiteallinone/backend/src/views/apps/crm/clue/index.vue
T
2026-09-29 18:25:09 +08:00

252 lines
8.5 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>