799 lines
26 KiB
Vue
799 lines
26 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>
|
||
<el-button type="success" :icon="Cpu" @click="smartAddVisible = true">智能添加</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: 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-select v-model="filters.status" clearable placeholder="全部" style="width: 110px">
|
||
<el-option
|
||
v-for="item in STATUS_OPTIONS"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.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 v-if="multipleSelection.length" class="batch-bar">
|
||
<span class="batch-tip">已选 {{ multipleSelection.length }} 项</span>
|
||
<el-button size="small" type="primary" plain :loading="batchLoading" @click="handleBatchToggleStatus('1')">
|
||
批量启用
|
||
</el-button>
|
||
<el-button size="small" type="info" plain :loading="batchLoading" @click="handleBatchToggleStatus('0')">
|
||
批量禁用
|
||
</el-button>
|
||
<el-button size="small" type="success" plain :loading="batchLoading" @click="handleBatchConvertSupplier">
|
||
批量转供应商
|
||
</el-button>
|
||
<el-button size="small" type="warning" plain @click="openMovePool(multipleSelection)">
|
||
批量移入公海
|
||
</el-button>
|
||
<el-button size="small" type="danger" plain :loading="batchLoading" @click="handleBatchDelete">
|
||
批量删除
|
||
</el-button>
|
||
</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>
|
||
<!-- 同一家单位可能既是客户又是供应商(已建供应商档案) -->
|
||
<el-tag v-if="row.also_supplier" size="small" type="warning" effect="plain" style="margin-left: 6px">
|
||
也是供应商
|
||
</el-tag>
|
||
</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="100">
|
||
<template #default="{ row }">{{ row.owner_user_name || '未分配' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="状态" width="90" align="center">
|
||
<template #default="{ row }">
|
||
<el-tag :type="statusTag(row.status)" size="small">{{ statusText(row.status) }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="更新时间" width="160" align="center">
|
||
<template #default="{ row }">{{ formatDateTime(row.update_time) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="350" align="center" fixed="right">
|
||
<template #default="{ row }">
|
||
<el-button link type="primary" size="small" @click="openEdit(row)">编辑</el-button>
|
||
<el-button link type="primary" size="small" @click="openContact(row)">联系人</el-button>
|
||
<el-button link type="success" size="small" @click="handleConvertSupplier(row)">转为供应商</el-button>
|
||
<el-button link type="warning" size="small" @click="openMovePool([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>
|
||
|
||
<CustomerEdit
|
||
v-model:visible="editVisible"
|
||
:edit-data="currentEditData"
|
||
@success="fetchList"
|
||
/>
|
||
|
||
<!-- 详情抽屉 -->
|
||
<el-drawer
|
||
v-model="detailVisible"
|
||
:title="detailData?.customer_name || '客户详情'"
|
||
direction="rtl"
|
||
size="700px"
|
||
>
|
||
<div v-if="detailData" class="detail-content">
|
||
<div class="detail-actions">
|
||
<el-button type="success" size="small" @click="handleConvertSupplier(detailData)">转为供应商</el-button>
|
||
<span class="detail-actions-tip">同一家单位可能既是客户又是供应商,转化后原客户记录保留</span>
|
||
</div>
|
||
<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="状态">
|
||
<el-tag :type="statusTag(detailData.status)" size="small">
|
||
{{ statusText(detailData.status) }}
|
||
</el-tag>
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="负责人">{{ detailData.owner_user_name || '未分配' }}</el-descriptions-item>
|
||
<el-descriptions-item label="注册资本">{{ detailData.registered_capital ? detailData.registered_capital + ' 万元' : '-' }}</el-descriptions-item>
|
||
<el-descriptions-item label="实缴资本">{{ detailData.paid_capital ? detailData.paid_capital + ' 万元' : '-' }}</el-descriptions-item>
|
||
<el-descriptions-item label="成立日期">{{ formatDate(detailData.establish_date) }}</el-descriptions-item>
|
||
<el-descriptions-item label="行政区划">{{ detailData.administrative_division || '-' }}</el-descriptions-item>
|
||
<el-descriptions-item label="企业类型">{{ detailData.enterprise_type || '-' }}</el-descriptions-item>
|
||
<el-descriptions-item label="纳税人资质">{{ detailData.taxpayer_qualification || '-' }}</el-descriptions-item>
|
||
<el-descriptions-item label="客户地址" :span="2">{{ detailData.address || '-' }}</el-descriptions-item>
|
||
<el-descriptions-item label="经营范围" :span="2">{{ detailData.business_scope || '-' }}</el-descriptions-item>
|
||
<el-descriptions-item label="创建时间">{{ formatDateTime(detailData.create_time) }}</el-descriptions-item>
|
||
<el-descriptions-item label="更新时间">{{ formatDateTime(detailData.update_time) }}</el-descriptions-item>
|
||
</el-descriptions>
|
||
|
||
<el-descriptions
|
||
title="联系信息"
|
||
:column="2"
|
||
border
|
||
size="small"
|
||
style="margin-top: 20px"
|
||
>
|
||
<el-descriptions-item label="联系人">{{ detailData.contact_person || '-' }}</el-descriptions-item>
|
||
<el-descriptions-item label="联系电话">{{ detailData.contact_phone || '-' }}</el-descriptions-item>
|
||
<el-descriptions-item label="联系邮箱" :span="2">
|
||
{{ detailData.contact_email || '-' }}
|
||
</el-descriptions-item>
|
||
</el-descriptions>
|
||
|
||
<el-descriptions
|
||
title="开票信息"
|
||
:column="2"
|
||
border
|
||
size="small"
|
||
style="margin-top: 20px"
|
||
>
|
||
<el-descriptions-item label="发票抬头" :span="2">
|
||
{{ detailData.invoice_title || '-' }}
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="纳税人识别号">{{ detailData.tax_number || '-' }}</el-descriptions-item>
|
||
<el-descriptions-item label="注册电话">{{ detailData.registered_phone || '-' }}</el-descriptions-item>
|
||
<el-descriptions-item label="注册地址" :span="2">
|
||
{{ detailData.registered_address || '-' }}
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="开户行">{{ detailData.bank_name || '-' }}</el-descriptions-item>
|
||
<el-descriptions-item label="银行账号">{{ detailData.bank_account || '-' }}</el-descriptions-item>
|
||
</el-descriptions>
|
||
|
||
<el-descriptions
|
||
title="备注"
|
||
:column="1"
|
||
border
|
||
size="small"
|
||
style="margin-top: 20px"
|
||
>
|
||
<el-descriptions-item label="备注">{{ detailData.remark || '-' }}</el-descriptions-item>
|
||
</el-descriptions>
|
||
</div>
|
||
</el-drawer>
|
||
|
||
<!-- 联系人抽屉 -->
|
||
<el-drawer
|
||
v-model="contactVisible"
|
||
:title="`${currentRow?.customer_name || '客户'} - 联系人`"
|
||
direction="rtl"
|
||
size="820px"
|
||
>
|
||
<div class="contact-drawer">
|
||
<div class="drawer-toolbar">
|
||
<el-button type="primary" :icon="Plus" size="small" @click="openContactCreate">
|
||
新增联系人
|
||
</el-button>
|
||
<span class="count-tip">共 {{ contactList.length }} 位联系人</span>
|
||
</div>
|
||
<el-table :data="contactList" v-loading="contactLoading" stripe border size="small">
|
||
<el-table-column label="姓名" width="140">
|
||
<template #default="{ row }">
|
||
<span>{{ row.contact_name }}</span>
|
||
<el-tag
|
||
v-if="Number(row.is_primary) === 1"
|
||
type="danger"
|
||
size="small"
|
||
effect="plain"
|
||
style="margin-left: 6px"
|
||
>
|
||
主联系人
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="department" label="部门" width="120" show-overflow-tooltip />
|
||
<el-table-column prop="position" label="职位" width="130" show-overflow-tooltip />
|
||
<el-table-column label="手机号" width="170" show-overflow-tooltip>
|
||
<template #default="{ row }">{{ formatMobiles(row.mobiles) || row.mobile || "-" }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="email" label="邮箱" min-width="180" show-overflow-tooltip />
|
||
<el-table-column prop="wechat" label="微信" width="120" show-overflow-tooltip />
|
||
<el-table-column prop="qq" label="QQ" width="110" show-overflow-tooltip />
|
||
<el-table-column label="操作" width="120" align="center" fixed="right">
|
||
<template #default="{ row }">
|
||
<el-button link type="primary" size="small" @click="openContactEdit(row)">编辑</el-button>
|
||
<el-button link type="danger" size="small" @click="handleContactDelete(row)">
|
||
删除
|
||
</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
<template #empty>
|
||
<el-empty description="暂无联系人" :image-size="60" />
|
||
</template>
|
||
</el-table>
|
||
</div>
|
||
</el-drawer>
|
||
|
||
<ContactEdit
|
||
v-model:visible="contactEditVisible"
|
||
:edit-data="contactEditData"
|
||
:default-related="contactRelated"
|
||
@success="loadContacts"
|
||
/>
|
||
|
||
<SmartAddCompany
|
||
v-model:visible="smartAddVisible"
|
||
type="customer"
|
||
@success="fetchList"
|
||
/>
|
||
|
||
<!-- 移入公海 -->
|
||
<el-dialog v-model="poolVisible" title="移入客户公海" width="460px" :close-on-click-modal="false">
|
||
<el-alert
|
||
type="info"
|
||
:closable="false"
|
||
show-icon
|
||
title="移入后客户将进入公海,租户内所有成员可见,可被他人领取或分配。"
|
||
style="margin-bottom: 16px"
|
||
/>
|
||
<el-form label-width="90px">
|
||
<el-form-item label="移入客户">
|
||
<span>{{ poolTargetNames }}</span>
|
||
</el-form-item>
|
||
<el-form-item label="移入原因">
|
||
<el-select v-model="poolReason" placeholder="请选择原因" style="width: 100%">
|
||
<el-option v-for="item in POOL_REASON_OPTIONS" :key="item" :label="item" :value="item" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="poolVisible = false">取消</el-button>
|
||
<el-button type="primary" :loading="poolSubmitting" @click="submitMovePool">确定</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, reactive, computed, onMounted } from "vue";
|
||
import { ElMessage, ElMessageBox } from "element-plus";
|
||
import { Plus, Search, Refresh, Cpu } from "@element-plus/icons-vue";
|
||
import {
|
||
getCrmCustomerList,
|
||
deleteCrmCustomer,
|
||
toggleCrmCustomerStatus,
|
||
moveToPool,
|
||
convertCustomerToSupplier,
|
||
} from "@/api/crm";
|
||
import { listContacts, deleteContact } from "@/api/contact";
|
||
import CustomerEdit from "./components/edit.vue";
|
||
import ContactEdit from "../contact/components/edit.vue";
|
||
import SmartAddCompany from "./components/smartAddCompany.vue";
|
||
import {
|
||
CUSTOMER_TYPE_OPTIONS,
|
||
STATUS_OPTIONS,
|
||
POOL_REASON_OPTIONS,
|
||
levelOptions,
|
||
typeText,
|
||
typeTag,
|
||
levelText,
|
||
levelTag,
|
||
statusText,
|
||
statusTag,
|
||
formatDate,
|
||
formatDateTime,
|
||
formatMobiles,
|
||
} from "../dict";
|
||
|
||
const loading = ref(false);
|
||
const tableData = ref([]);
|
||
const multipleSelection = ref([]);
|
||
const editVisible = ref(false);
|
||
const currentEditData = ref(null);
|
||
const detailVisible = ref(false);
|
||
const detailData = ref(null);
|
||
|
||
// 联系人抽屉
|
||
const contactVisible = ref(false);
|
||
const contactLoading = ref(false);
|
||
const contactList = ref([]);
|
||
const currentRow = ref(null);
|
||
const contactEditVisible = ref(false);
|
||
const contactEditData = ref(null);
|
||
const smartAddVisible = ref(false);
|
||
|
||
// 移入公海
|
||
const poolVisible = ref(false);
|
||
const poolReason = ref("");
|
||
const poolTargets = ref([]);
|
||
const poolSubmitting = ref(false);
|
||
|
||
const filters = reactive({
|
||
keyword: "",
|
||
customer_type: "",
|
||
customer_level: "",
|
||
status: "",
|
||
});
|
||
|
||
const pagination = reactive({
|
||
page: 1,
|
||
pageSize: 20,
|
||
total: 0,
|
||
});
|
||
|
||
const contactRelated = computed(() =>
|
||
currentRow.value
|
||
? {
|
||
related_type: 1,
|
||
related_id: currentRow.value.id,
|
||
related_name: currentRow.value.customer_name,
|
||
}
|
||
: null
|
||
);
|
||
|
||
const poolTargetNames = computed(() =>
|
||
poolTargets.value.map((item) => item.customer_name).join("、")
|
||
);
|
||
|
||
onMounted(() => {
|
||
fetchList();
|
||
});
|
||
|
||
async function fetchList() {
|
||
loading.value = true;
|
||
try {
|
||
const res = await getCrmCustomerList({
|
||
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.customer_type = "";
|
||
filters.customer_level = "";
|
||
filters.status = "";
|
||
handleSearch();
|
||
}
|
||
|
||
function handleSelectionChange(val) {
|
||
multipleSelection.value = val;
|
||
}
|
||
|
||
/* ------------------------------ 批量操作 ------------------------------ */
|
||
|
||
const batchLoading = ref(false);
|
||
|
||
/**
|
||
* 批量执行:逐个复用单条接口,单行失败不影响其他行;
|
||
* 结束后按成功 / 失败数量给出汇总提示,并刷新列表、清空选中。
|
||
*/
|
||
async function runBatch(rows, action, actionText) {
|
||
if (!rows.length) return;
|
||
batchLoading.value = true;
|
||
try {
|
||
const results = await Promise.allSettled(rows.map((row) => action(row)));
|
||
const failed = results.filter((item) => item.status === "rejected");
|
||
const okCount = rows.length - failed.length;
|
||
if (failed.length === 0) {
|
||
ElMessage.success(`${actionText}成功(${okCount} 条)`);
|
||
} else if (okCount === 0) {
|
||
ElMessage.error(`${actionText}失败:${failed[0].reason?.message || "请稍后重试"}`);
|
||
} else {
|
||
ElMessage.warning(`${actionText}完成:成功 ${okCount} 条,失败 ${failed.length} 条`);
|
||
}
|
||
multipleSelection.value = [];
|
||
fetchList();
|
||
} finally {
|
||
batchLoading.value = false;
|
||
}
|
||
}
|
||
|
||
/** 批量启用 / 禁用 */
|
||
async function handleBatchToggleStatus(status) {
|
||
const rows = multipleSelection.value;
|
||
if (!rows.length) return;
|
||
const action = status === "1" ? "启用" : "禁用";
|
||
try {
|
||
await ElMessageBox.confirm(`确定要${action}选中的 ${rows.length} 个客户吗?`, `批量${action}`, {
|
||
type: "warning",
|
||
});
|
||
} catch (e) {
|
||
return; // 取消
|
||
}
|
||
await runBatch(rows, (row) => toggleCrmCustomerStatus(row.id, status), `批量${action}`);
|
||
}
|
||
|
||
/** 批量删除(软删,不可恢复) */
|
||
async function handleBatchDelete() {
|
||
const rows = multipleSelection.value;
|
||
if (!rows.length) return;
|
||
try {
|
||
await ElMessageBox.confirm(
|
||
`确定要删除选中的 ${rows.length} 个客户吗?删除后不可恢复。`,
|
||
"批量删除",
|
||
{ type: "warning" }
|
||
);
|
||
} catch (e) {
|
||
return; // 取消
|
||
}
|
||
await runBatch(rows, (row) => deleteCrmCustomer(row.id), "批量删除");
|
||
}
|
||
|
||
/** 批量转供应商:同名企业后端只补同步联系人,原客户记录保留 */
|
||
async function handleBatchConvertSupplier() {
|
||
const rows = multipleSelection.value;
|
||
if (!rows.length) return;
|
||
try {
|
||
await ElMessageBox.confirm(
|
||
`将选中的 ${rows.length} 个客户的企业资料与联系人复制到供应商库?原客户记录会保留。`,
|
||
"批量转供应商",
|
||
{ type: "warning" }
|
||
);
|
||
} catch (e) {
|
||
return; // 取消
|
||
}
|
||
await runBatch(rows, (row) => convertCustomerToSupplier(row.id), "批量转供应商");
|
||
}
|
||
|
||
function openCreate() {
|
||
currentEditData.value = null;
|
||
editVisible.value = true;
|
||
}
|
||
|
||
function openEdit(row) {
|
||
currentEditData.value = { ...row };
|
||
editVisible.value = true;
|
||
}
|
||
|
||
function openDetail(row) {
|
||
detailData.value = { ...row };
|
||
detailVisible.value = true;
|
||
}
|
||
|
||
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 || "删除失败");
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 客户转供应商:把客户的企业资料与联系人复制一套到供应商库。
|
||
* 同一家单位可能既是客户又是供应商,原客户记录保留;供应商库已有同名企业时只补同步联系人,不重复建档。
|
||
*/
|
||
async function handleConvertSupplier(row) {
|
||
try {
|
||
await ElMessageBox.confirm(
|
||
`将客户「${row.customer_name}」的企业资料与联系人复制一套到供应商库?原客户记录会保留,可在供应商管理中继续完善。`,
|
||
"转为供应商",
|
||
{ type: "warning", confirmButtonText: "确认转化" }
|
||
);
|
||
} catch (e) {
|
||
return; // 取消
|
||
}
|
||
try {
|
||
const res = await convertCustomerToSupplier(row.id);
|
||
const data = res?.data || {};
|
||
const c = data.contacts || {};
|
||
ElMessage.success(
|
||
data.existed
|
||
? `供应商库已有同名企业,已同步联系人(新增 ${c.inserted || 0}、补充 ${c.merged || 0}),未重复建档`
|
||
: `已转为供应商,并同步联系人(新增 ${c.inserted || 0})`
|
||
);
|
||
fetchList();
|
||
} catch (e) {
|
||
ElMessage.error(e.message || "转化失败");
|
||
}
|
||
}
|
||
|
||
/* ------------------------------ 联系人 ------------------------------ */
|
||
async function openContact(row) {
|
||
currentRow.value = { ...row };
|
||
contactVisible.value = true;
|
||
loadContacts();
|
||
}
|
||
|
||
async function loadContacts() {
|
||
if (!currentRow.value) return;
|
||
contactLoading.value = true;
|
||
try {
|
||
const res = await listContacts({ related_type: 1, related_id: currentRow.value.id });
|
||
const data = res?.data || {};
|
||
contactList.value = Array.isArray(data) ? data : data.list || [];
|
||
} catch (e) {
|
||
contactList.value = [];
|
||
ElMessage.error(e.message || "联系人查询失败");
|
||
} finally {
|
||
contactLoading.value = false;
|
||
}
|
||
}
|
||
|
||
function openContactCreate() {
|
||
contactEditData.value = null;
|
||
contactEditVisible.value = true;
|
||
}
|
||
|
||
function openContactEdit(row) {
|
||
contactEditData.value = { ...row };
|
||
contactEditVisible.value = true;
|
||
}
|
||
|
||
async function handleContactDelete(row) {
|
||
try {
|
||
await ElMessageBox.confirm(`确定删除联系人「${row.contact_name}」吗?`, "删除确认", {
|
||
type: "warning",
|
||
});
|
||
await deleteContact({ id: row.id });
|
||
ElMessage.success("删除成功");
|
||
loadContacts();
|
||
} catch (e) {
|
||
if (e !== "cancel") {
|
||
ElMessage.error(e.message || "删除失败");
|
||
}
|
||
}
|
||
}
|
||
|
||
/* ------------------------------ 移入公海 ------------------------------ */
|
||
function openMovePool(rows) {
|
||
if (!rows.length) return;
|
||
poolTargets.value = rows;
|
||
poolReason.value = "";
|
||
poolVisible.value = true;
|
||
}
|
||
|
||
async function submitMovePool() {
|
||
if (!poolReason.value) {
|
||
ElMessage.warning("请选择移入原因");
|
||
return;
|
||
}
|
||
poolSubmitting.value = true;
|
||
try {
|
||
await moveToPool({
|
||
ids: poolTargets.value.map((item) => item.id),
|
||
reason: poolReason.value,
|
||
});
|
||
ElMessage.success("已移入公海");
|
||
poolVisible.value = false;
|
||
multipleSelection.value = [];
|
||
fetchList();
|
||
} catch (e) {
|
||
ElMessage.error(e.message || "操作失败");
|
||
} finally {
|
||
poolSubmitting.value = false;
|
||
}
|
||
}
|
||
</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;
|
||
}
|
||
}
|
||
|
||
.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);
|
||
}
|
||
|
||
/* 批量操作栏:勾选行后出现 */
|
||
.batch-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
margin-bottom: 12px;
|
||
padding: 10px 16px;
|
||
border-radius: 6px;
|
||
border: 1px solid var(--el-color-primary-light-7);
|
||
background: var(--el-color-primary-light-9);
|
||
|
||
.batch-tip {
|
||
margin-right: 8px;
|
||
font-size: 13px;
|
||
color: var(--el-text-color-regular);
|
||
}
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.contact-drawer {
|
||
.drawer-toolbar {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
margin-bottom: 12px;
|
||
|
||
.count-tip {
|
||
font-size: 13px;
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
}
|
||
}
|
||
|
||
.detail-content {
|
||
padding: 4px 8px;
|
||
|
||
.detail-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.detail-actions-tip {
|
||
font-size: 12px;
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
|
||
:deep(.el-descriptions__label) {
|
||
width: 130px;
|
||
min-width: 130px;
|
||
color: var(--el-text-color-regular);
|
||
font-weight: 500;
|
||
vertical-align: top;
|
||
}
|
||
|
||
:deep(.el-descriptions__content) {
|
||
word-break: break-all;
|
||
}
|
||
}
|
||
</style>
|