临时更新
This commit is contained in:
@@ -0,0 +1,511 @@
|
||||
<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"
|
||||
plain
|
||||
:disabled="!multipleSelection.length"
|
||||
@click="handleBatchClaim"
|
||||
>
|
||||
批量领取
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
:disabled="!multipleSelection.length"
|
||||
@click="openAssign(multipleSelection)"
|
||||
>
|
||||
批量分配
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-alert
|
||||
type="info"
|
||||
show-icon
|
||||
:closable="false"
|
||||
class="pool-tip"
|
||||
title="数据范围:当前租户全部成员共享"
|
||||
description="公海中的客户不归属任何个人,领取后成为您的客户;分配给他人后由对方跟进。"
|
||||
/>
|
||||
|
||||
<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: 240px"
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="客户类型">
|
||||
<el-select v-model="filters.customer_type" clearable placeholder="全部" style="width: 130px">
|
||||
<el-option
|
||||
v-for="item in CUSTOMER_TYPE_OPTIONS"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="客户等级">
|
||||
<el-select v-model="filters.customer_level" clearable placeholder="全部" style="width: 130px">
|
||||
<el-option
|
||||
v-for="item in levelOptions('customer')"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="进入公海时间">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
value-format="YYYY-MM-DD"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
style="width: 240px"
|
||||
/>
|
||||
</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"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="50" align="center" />
|
||||
<el-table-column label="客户名称" min-width="200" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span class="name-link" @click="openDetail(row)">{{ row.customer_name }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="typeTag(row.customer_type)" size="small">
|
||||
{{ typeText('customer', row.customer_type) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="等级" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="levelTag(row.customer_level)" size="small" effect="plain">
|
||||
{{ levelText('customer', row.customer_level) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="industry" label="行业" width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="contact_person" label="联系人" width="100" />
|
||||
<el-table-column prop="contact_phone" label="联系电话" width="130" />
|
||||
<el-table-column label="原负责人" width="110">
|
||||
<template #default="{ row }">{{ row.last_owner_name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="进入公海时间" width="160" align="center">
|
||||
<template #default="{ row }">{{ formatDateTime(row.pool_in_time) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="原因" width="130" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.pool_reason || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="260" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="handleClaim([row])">领取</el-button>
|
||||
<el-button link type="primary" size="small" @click="openAssign([row])">分配</el-button>
|
||||
<el-button link type="warning" size="small" @click="handleRemove([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>
|
||||
|
||||
<!-- 分配负责人 -->
|
||||
<el-dialog v-model="assignVisible" title="分配负责人" width="460px" :close-on-click-modal="false">
|
||||
<el-form label-width="90px">
|
||||
<el-form-item label="待分配客户">
|
||||
<span>{{ assignTargetNames }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="负责人">
|
||||
<el-select
|
||||
v-model="assignUserId"
|
||||
filterable
|
||||
placeholder="请选择负责人"
|
||||
style="width: 100%"
|
||||
:loading="userLoading"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in userOptions"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="assignVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="assignSubmitting" @click="submitAssign">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 详情抽屉 -->
|
||||
<el-drawer
|
||||
v-model="detailVisible"
|
||||
:title="detailData?.customer_name || '客户详情'"
|
||||
direction="rtl"
|
||||
size="520px"
|
||||
>
|
||||
<div v-if="detailData">
|
||||
<el-descriptions title="基本信息" :column="2" border size="small">
|
||||
<el-descriptions-item label="客户名称" :span="2">
|
||||
{{ detailData.customer_name }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="客户类型">
|
||||
{{ typeText('customer', detailData.customer_type) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="客户等级">
|
||||
{{ levelText('customer', detailData.customer_level) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="所属行业">{{ detailData.industry || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="联系人">{{ detailData.contact_person || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="联系电话">{{ detailData.contact_phone || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="联系邮箱">{{ detailData.contact_email || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="客户地址" :span="2">
|
||||
{{ detailData.address || '-' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-descriptions
|
||||
title="公海信息"
|
||||
:column="2"
|
||||
border
|
||||
size="small"
|
||||
style="margin-top: 20px"
|
||||
>
|
||||
<el-descriptions-item label="原负责人">{{ detailData.last_owner_name || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="进入原因">{{ detailData.pool_reason || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="进入时间" :span="2">
|
||||
{{ formatDateTime(detailData.pool_in_time) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" :span="2">{{ detailData.remark || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { Search, Refresh } from "@element-plus/icons-vue";
|
||||
import {
|
||||
getPoolList,
|
||||
claimPoolCustomer,
|
||||
assignPoolCustomer,
|
||||
removeFromPool,
|
||||
deleteCrmCustomer,
|
||||
} from "@/api/crm";
|
||||
import { getAllUsers } from "@/api/user";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import {
|
||||
CUSTOMER_TYPE_OPTIONS,
|
||||
levelOptions,
|
||||
typeText,
|
||||
typeTag,
|
||||
levelText,
|
||||
levelTag,
|
||||
formatDateTime,
|
||||
} from "../dict";
|
||||
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const loading = ref(false);
|
||||
const tableData = ref([]);
|
||||
const multipleSelection = ref([]);
|
||||
const dateRange = ref([]);
|
||||
const detailVisible = ref(false);
|
||||
const detailData = ref(null);
|
||||
|
||||
const filters = reactive({
|
||||
keyword: "",
|
||||
customer_type: "",
|
||||
customer_level: "",
|
||||
});
|
||||
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
// 分配
|
||||
const assignVisible = ref(false);
|
||||
const assignTargets = ref([]);
|
||||
const assignUserId = ref("");
|
||||
const assignSubmitting = ref(false);
|
||||
const userLoading = ref(false);
|
||||
const userOptions = ref([]);
|
||||
|
||||
const assignTargetNames = computed(() =>
|
||||
assignTargets.value.map((item) => item.customer_name).join("、")
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
fetchList();
|
||||
});
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const params = {
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize,
|
||||
...filters,
|
||||
};
|
||||
if (dateRange.value && dateRange.value.length === 2) {
|
||||
params.start_time = dateRange.value[0];
|
||||
params.end_time = dateRange.value[1];
|
||||
}
|
||||
const res = await getPoolList(params);
|
||||
const data = res?.data || {};
|
||||
tableData.value = Array.isArray(data) ? data : data.list || [];
|
||||
pagination.total = data.total || tableData.value.length;
|
||||
} 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.customer_type = "";
|
||||
filters.customer_level = "";
|
||||
dateRange.value = [];
|
||||
handleSearch();
|
||||
}
|
||||
|
||||
function handleSelectionChange(val) {
|
||||
multipleSelection.value = val;
|
||||
}
|
||||
|
||||
function openDetail(row) {
|
||||
detailData.value = { ...row };
|
||||
detailVisible.value = true;
|
||||
}
|
||||
|
||||
/* ------------------------------ 领取 ------------------------------ */
|
||||
async function handleClaim(rows) {
|
||||
if (!rows.length) return;
|
||||
const names = rows.map((item) => item.customer_name).join("、");
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定领取客户「${names}」吗?领取后由 ${authStore.user?.name || '当前用户'} 跟进。`,
|
||||
"领取确认",
|
||||
{ type: "warning" }
|
||||
);
|
||||
await claimPoolCustomer({ ids: rows.map((item) => item.id) });
|
||||
ElMessage.success("领取成功");
|
||||
multipleSelection.value = [];
|
||||
fetchList();
|
||||
} catch (e) {
|
||||
if (e !== "cancel") {
|
||||
ElMessage.error(e.message || "领取失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleBatchClaim() {
|
||||
handleClaim(multipleSelection.value);
|
||||
}
|
||||
|
||||
/* ------------------------------ 分配 ------------------------------ */
|
||||
async function openAssign(rows) {
|
||||
if (!rows.length) return;
|
||||
assignTargets.value = rows;
|
||||
assignUserId.value = "";
|
||||
assignVisible.value = true;
|
||||
await loadUsers();
|
||||
}
|
||||
|
||||
async function loadUsers() {
|
||||
if (userOptions.value.length) return;
|
||||
userLoading.value = true;
|
||||
try {
|
||||
const res = await getAllUsers();
|
||||
const data = res?.data || {};
|
||||
const list = Array.isArray(data) ? data : data.list || [];
|
||||
userOptions.value = list.map((item) => ({
|
||||
id: item.id,
|
||||
name: item.name || item.account || `用户${item.id}`,
|
||||
}));
|
||||
} catch (e) {
|
||||
userOptions.value = [];
|
||||
} finally {
|
||||
userLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function submitAssign() {
|
||||
if (!assignUserId.value) {
|
||||
ElMessage.warning("请选择负责人");
|
||||
return;
|
||||
}
|
||||
assignSubmitting.value = true;
|
||||
try {
|
||||
await assignPoolCustomer({
|
||||
ids: assignTargets.value.map((item) => item.id),
|
||||
owner_user_id: assignUserId.value,
|
||||
});
|
||||
ElMessage.success("分配成功");
|
||||
assignVisible.value = false;
|
||||
multipleSelection.value = [];
|
||||
fetchList();
|
||||
} catch (e) {
|
||||
ElMessage.error(e.message || "分配失败");
|
||||
} finally {
|
||||
assignSubmitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------------------- 移出 / 删除 ---------------------------- */
|
||||
async function handleRemove(rows) {
|
||||
if (!rows.length) return;
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定将选中的 ${rows.length} 个客户移出公海吗?`,
|
||||
"移出公海",
|
||||
{ type: "warning" }
|
||||
);
|
||||
await removeFromPool({ ids: rows.map((item) => item.id) });
|
||||
ElMessage.success("已移出公海");
|
||||
multipleSelection.value = [];
|
||||
fetchList();
|
||||
} catch (e) {
|
||||
if (e !== "cancel") {
|
||||
ElMessage.error(e.message || "操作失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除客户「${row.customer_name}」吗?删除后不可恢复。`,
|
||||
"删除确认",
|
||||
{ type: "warning" }
|
||||
);
|
||||
await deleteCrmCustomer(row.id);
|
||||
ElMessage.success("删除成功");
|
||||
fetchList();
|
||||
} catch (e) {
|
||||
if (e !== "cancel") {
|
||||
ElMessage.error(e.message || "删除失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.crm-page {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 16px;
|
||||
|
||||
h2 {
|
||||
margin: 0 0 6px 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.pool-tip {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
background: var(--el-bg-color);
|
||||
padding: 16px 16px 0;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 12px;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.table-container {
|
||||
background: var(--el-bg-color);
|
||||
padding: 16px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.name-link {
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
color: var(--el-color-primary);
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user