临时更新
This commit is contained in:
@@ -0,0 +1,97 @@
|
|||||||
|
import request from "@/utils/request";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CRM 接口
|
||||||
|
*
|
||||||
|
* 说明:
|
||||||
|
* - 客户 / 供应商沿用已有 ERP 接口(数据模型 TenantCrmCustomer / TenantCrmSupplier,租户隔离);
|
||||||
|
* - 联系人沿用 /backend/crm/contact 系列接口(见 api/contact.js);
|
||||||
|
* - 客户公海为 CRM 新增能力,需后端提供 /backend/crm/pool 系列接口。
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* --------------------------------- 客户 --------------------------------- */
|
||||||
|
|
||||||
|
export function getCrmCustomerList(params) {
|
||||||
|
return request({ url: "/backend/erp/customer/list", method: "get", params });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCrmCustomerDetail(id) {
|
||||||
|
return request({ url: `/backend/erp/customer/${id}`, method: "get" });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createCrmCustomer(data) {
|
||||||
|
return request({ url: "/backend/erp/customer", method: "post", data });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateCrmCustomer(id, data) {
|
||||||
|
return request({ url: `/backend/erp/customer/${id}`, method: "put", data });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteCrmCustomer(id) {
|
||||||
|
return request({ url: `/backend/erp/customer/${id}`, method: "delete" });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toggleCrmCustomerStatus(id, status) {
|
||||||
|
return request({
|
||||||
|
url: `/backend/erp/customer/${id}/status`,
|
||||||
|
method: "post",
|
||||||
|
data: { status },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------- 供应商 -------------------------------- */
|
||||||
|
|
||||||
|
export function getCrmSupplierList(params) {
|
||||||
|
return request({ url: "/backend/erp/supplier/list", method: "get", params });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCrmSupplierDetail(id) {
|
||||||
|
return request({ url: `/backend/erp/supplier/${id}`, method: "get" });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createCrmSupplier(data) {
|
||||||
|
return request({ url: "/backend/erp/supplier", method: "post", data });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateCrmSupplier(id, data) {
|
||||||
|
return request({ url: `/backend/erp/supplier/${id}`, method: "put", data });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteCrmSupplier(id) {
|
||||||
|
return request({ url: `/backend/erp/supplier/${id}`, method: "delete" });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toggleCrmSupplierStatus(id, status) {
|
||||||
|
return request({
|
||||||
|
url: `/backend/erp/supplier/${id}/status`,
|
||||||
|
method: "post",
|
||||||
|
data: { status },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------- 客户公海 ------------------------------- */
|
||||||
|
|
||||||
|
/** 公海列表:租户内共享,所有成员可见 */
|
||||||
|
export function getPoolList(params) {
|
||||||
|
return request({ url: "/backend/crm/pool/list", method: "get", params });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 领取客户:ids 客户ID集合,领取后负责人为当前登录用户 */
|
||||||
|
export function claimPoolCustomer(data) {
|
||||||
|
return request({ url: "/backend/crm/pool/claim", method: "post", data });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 分配客户:ids 客户ID集合,owner_user_id 负责人 */
|
||||||
|
export function assignPoolCustomer(data) {
|
||||||
|
return request({ url: "/backend/crm/pool/assign", method: "post", data });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 移出公海:ids 客户ID集合 */
|
||||||
|
export function removeFromPool(data) {
|
||||||
|
return request({ url: "/backend/crm/pool/remove", method: "post", data });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 移入公海:ids 客户ID集合,reason 移入原因 */
|
||||||
|
export function moveToPool(data) {
|
||||||
|
return request({ url: "/backend/crm/pool/move", method: "post", data });
|
||||||
|
}
|
||||||
@@ -0,0 +1,293 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
:model-value="visible"
|
||||||
|
:title="isEdit ? '编辑联系人' : '新增联系人'"
|
||||||
|
width="680px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
@update:model-value="handleClose"
|
||||||
|
@closed="handleClosed"
|
||||||
|
>
|
||||||
|
<el-form
|
||||||
|
ref="formRef"
|
||||||
|
:model="form"
|
||||||
|
:rules="rules"
|
||||||
|
label-width="100px"
|
||||||
|
label-position="right"
|
||||||
|
>
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="关联类型" prop="related_type">
|
||||||
|
<el-select
|
||||||
|
v-model="form.related_type"
|
||||||
|
placeholder="请选择"
|
||||||
|
style="width: 100%"
|
||||||
|
:disabled="!!lockedRelated"
|
||||||
|
@change="handleRelatedTypeChange"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in RELATED_TYPE_OPTIONS"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="关联对象" prop="related_id">
|
||||||
|
<el-select
|
||||||
|
v-model="form.related_id"
|
||||||
|
filterable
|
||||||
|
remote
|
||||||
|
clearable
|
||||||
|
reserve-keyword
|
||||||
|
:remote-method="remoteSearchCompany"
|
||||||
|
:loading="companyLoading"
|
||||||
|
:placeholder="companyPlaceholder"
|
||||||
|
style="width: 100%"
|
||||||
|
:disabled="!!lockedRelated"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in companyOptions"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.name"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="姓名" prop="contact_name">
|
||||||
|
<el-input v-model="form.contact_name" placeholder="请输入联系人姓名" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="性别" prop="gender">
|
||||||
|
<el-select v-model="form.gender" placeholder="请选择" style="width: 100%">
|
||||||
|
<el-option
|
||||||
|
v-for="item in GENDER_OPTIONS"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="手机号" prop="mobile">
|
||||||
|
<el-input v-model="form.mobile" placeholder="请输入手机号" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="座机" prop="phone">
|
||||||
|
<el-input v-model="form.phone" placeholder="请输入座机号(选填)" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="邮箱" prop="email">
|
||||||
|
<el-input v-model="form.email" placeholder="请输入邮箱(选填)" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="部门" prop="department">
|
||||||
|
<el-input v-model="form.department" placeholder="请输入所属部门(选填)" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="职位" prop="position">
|
||||||
|
<el-input v-model="form.position" placeholder="如:采购负责人" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="主联系人" prop="is_primary">
|
||||||
|
<el-switch
|
||||||
|
v-model="form.is_primary"
|
||||||
|
:active-value="1"
|
||||||
|
:inactive-value="0"
|
||||||
|
active-text="是"
|
||||||
|
inactive-text="否"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="备注" prop="remark">
|
||||||
|
<el-input
|
||||||
|
v-model="form.remark"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
placeholder="如:关键决策人、对接注意事项(选填)"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="handleClose">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="submitting" @click="handleSave">保存</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, computed, watch, nextTick } from "vue";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
import { createContact, updateContact } from "@/api/contact";
|
||||||
|
import { getCrmCustomerList, getCrmSupplierList } from "@/api/crm";
|
||||||
|
import { RELATED_TYPE_OPTIONS, GENDER_OPTIONS } from "../../dict";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
visible: { type: Boolean, default: false },
|
||||||
|
editData: { type: Object, default: null },
|
||||||
|
// 从客户/供应商页面打开时传入,锁定关联对象
|
||||||
|
defaultRelated: { type: Object, default: null },
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["update:visible", "success"]);
|
||||||
|
|
||||||
|
const formRef = ref();
|
||||||
|
const submitting = ref(false);
|
||||||
|
const companyLoading = ref(false);
|
||||||
|
const companyOptions = ref([]);
|
||||||
|
const isEdit = ref(false);
|
||||||
|
|
||||||
|
const lockedRelated = computed(() => props.defaultRelated || null);
|
||||||
|
|
||||||
|
const defaultForm = () => ({
|
||||||
|
id: undefined,
|
||||||
|
related_type: 1,
|
||||||
|
related_id: undefined,
|
||||||
|
contact_name: "",
|
||||||
|
gender: 0,
|
||||||
|
mobile: "",
|
||||||
|
phone: "",
|
||||||
|
email: "",
|
||||||
|
department: "",
|
||||||
|
position: "",
|
||||||
|
is_primary: 0,
|
||||||
|
remark: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = reactive(defaultForm());
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
related_type: [{ required: true, message: "请选择关联类型", trigger: "change" }],
|
||||||
|
related_id: [{ required: true, message: "请选择关联对象", trigger: "change" }],
|
||||||
|
contact_name: [{ required: true, message: "请输入联系人姓名", trigger: "blur" }],
|
||||||
|
mobile: [
|
||||||
|
{ required: true, message: "请输入手机号", trigger: "blur" },
|
||||||
|
{
|
||||||
|
pattern: /^1[3-9]\d{9}$/,
|
||||||
|
message: "手机号格式不正确",
|
||||||
|
trigger: "blur",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
email: [{ type: "email", message: "邮箱格式不正确", trigger: "blur" }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const companyPlaceholder = computed(() =>
|
||||||
|
form.related_type === 2 ? "搜索供应商名称" : "搜索客户名称"
|
||||||
|
);
|
||||||
|
|
||||||
|
async function loadCompanyOptions(keyword = "") {
|
||||||
|
companyLoading.value = true;
|
||||||
|
try {
|
||||||
|
const params = { page: 1, pageSize: 30 };
|
||||||
|
if (keyword) params.keyword = keyword;
|
||||||
|
const res =
|
||||||
|
form.related_type === 2
|
||||||
|
? await getCrmSupplierList(params)
|
||||||
|
: await getCrmCustomerList(params);
|
||||||
|
const list = res?.data?.list || [];
|
||||||
|
companyOptions.value = list.map((item) => ({
|
||||||
|
id: item.id,
|
||||||
|
name: item.supplier_name || item.customer_name,
|
||||||
|
}));
|
||||||
|
} catch (e) {
|
||||||
|
companyOptions.value = [];
|
||||||
|
} finally {
|
||||||
|
companyLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function remoteSearchCompany(keyword) {
|
||||||
|
loadCompanyOptions(keyword);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRelatedTypeChange() {
|
||||||
|
form.related_id = undefined;
|
||||||
|
companyOptions.value = [];
|
||||||
|
loadCompanyOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.visible,
|
||||||
|
async (val) => {
|
||||||
|
if (!val) return;
|
||||||
|
isEdit.value = !!props.editData;
|
||||||
|
Object.assign(form, defaultForm());
|
||||||
|
if (props.editData) {
|
||||||
|
Object.assign(form, props.editData);
|
||||||
|
form.related_type = Number(props.editData.related_type) || 1;
|
||||||
|
form.is_primary = Number(props.editData.is_primary) || 0;
|
||||||
|
form.gender = Number(props.editData.gender) || 0;
|
||||||
|
await loadCompanyOptions();
|
||||||
|
// 编辑时确保关联对象在下拉中存在
|
||||||
|
const name =
|
||||||
|
props.editData.related_name ||
|
||||||
|
props.editData.customer_name ||
|
||||||
|
props.editData.supplier_name;
|
||||||
|
if (form.related_id && !companyOptions.value.some((i) => i.id === form.related_id)) {
|
||||||
|
companyOptions.value.unshift({ id: form.related_id, name: name || String(form.related_id) });
|
||||||
|
}
|
||||||
|
} else if (props.defaultRelated) {
|
||||||
|
form.related_type = Number(props.defaultRelated.related_type) || 1;
|
||||||
|
form.related_id = props.defaultRelated.related_id;
|
||||||
|
companyOptions.value = [
|
||||||
|
{
|
||||||
|
id: props.defaultRelated.related_id,
|
||||||
|
name: props.defaultRelated.related_name || String(props.defaultRelated.related_id),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
await loadCompanyOptions();
|
||||||
|
}
|
||||||
|
await nextTick();
|
||||||
|
formRef.value?.clearValidate();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleClose() {
|
||||||
|
emit("update:visible", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClosed() {
|
||||||
|
formRef.value?.resetFields();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSave() {
|
||||||
|
if (!formRef.value) return;
|
||||||
|
try {
|
||||||
|
await formRef.value.validate();
|
||||||
|
} catch (e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
submitting.value = true;
|
||||||
|
try {
|
||||||
|
const payload = { ...form };
|
||||||
|
if (isEdit.value) {
|
||||||
|
await updateContact(payload);
|
||||||
|
ElMessage.success("更新成功");
|
||||||
|
} else {
|
||||||
|
await createContact(payload);
|
||||||
|
ElMessage.success("新增成功");
|
||||||
|
}
|
||||||
|
emit("success");
|
||||||
|
handleClose();
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error(e.message || "操作失败");
|
||||||
|
} finally {
|
||||||
|
submitting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,285 @@
|
|||||||
|
<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: 240px"
|
||||||
|
@keyup.enter="handleSearch"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="关联类型">
|
||||||
|
<el-select v-model="filters.related_type" clearable placeholder="全部" style="width: 120px">
|
||||||
|
<el-option
|
||||||
|
v-for="item in RELATED_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.is_primary" clearable placeholder="全部" style="width: 110px">
|
||||||
|
<el-option label="是" :value="1" />
|
||||||
|
<el-option label="否" :value="0" />
|
||||||
|
</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>
|
||||||
|
<el-table-column type="index" label="#" width="56" align="center" />
|
||||||
|
<el-table-column label="姓名" min-width="140" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="name-cell-wrapper">
|
||||||
|
<span class="name-cell">{{ row.contact_name }}</span>
|
||||||
|
<el-tag v-if="Number(row.is_primary) === 1" type="danger" size="small" effect="plain">
|
||||||
|
主联系人
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="性别" width="70" align="center">
|
||||||
|
<template #default="{ row }">{{ genderText(row.gender) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="mobile" label="手机号" width="130" />
|
||||||
|
<el-table-column prop="phone" label="座机" width="130" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="email" label="邮箱" min-width="170" show-overflow-tooltip />
|
||||||
|
<el-table-column label="关联类型" width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag size="small" :type="Number(row.related_type) === 1 ? 'primary' : 'warning'">
|
||||||
|
{{ relatedTypeText(row.related_type) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="关联对象" min-width="180" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">{{ row.related_name || '-' }}</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="160" align="center">
|
||||||
|
<template #default="{ row }">{{ formatDateTime(row.update_time) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="220" align="center" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button link type="primary" size="small" @click="openEdit(row)">编辑</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="Number(row.is_primary) !== 1"
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
@click="handleSetPrimary(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>
|
||||||
|
|
||||||
|
<ContactEdit
|
||||||
|
v-model:visible="editVisible"
|
||||||
|
:edit-data="currentEditData"
|
||||||
|
@success="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 { listContacts, updateContact, deleteContact } from "@/api/contact";
|
||||||
|
import ContactEdit from "./components/edit.vue";
|
||||||
|
import {
|
||||||
|
RELATED_TYPE_OPTIONS,
|
||||||
|
genderText,
|
||||||
|
relatedTypeText,
|
||||||
|
formatDateTime,
|
||||||
|
} from "../dict";
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const tableData = ref([]);
|
||||||
|
const editVisible = ref(false);
|
||||||
|
const currentEditData = ref(null);
|
||||||
|
|
||||||
|
const filters = reactive({
|
||||||
|
keyword: "",
|
||||||
|
related_type: "",
|
||||||
|
is_primary: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const pagination = reactive({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
total: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchList();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function fetchList() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await listContacts({
|
||||||
|
page: pagination.page,
|
||||||
|
pageSize: pagination.pageSize,
|
||||||
|
...filters,
|
||||||
|
});
|
||||||
|
const data = res?.data || {};
|
||||||
|
const list = Array.isArray(data) ? data : data.list || [];
|
||||||
|
tableData.value = list;
|
||||||
|
pagination.total = data.total || list.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.related_type = "";
|
||||||
|
filters.is_primary = "";
|
||||||
|
handleSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCreate() {
|
||||||
|
currentEditData.value = null;
|
||||||
|
editVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEdit(row) {
|
||||||
|
currentEditData.value = { ...row };
|
||||||
|
editVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSetPrimary(row) {
|
||||||
|
try {
|
||||||
|
await updateContact({ ...row, is_primary: 1 });
|
||||||
|
ElMessage.success("已设为主联系人");
|
||||||
|
fetchList();
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error(e.message || "操作失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDelete(row) {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`确定要删除联系人「${row.contact_name}」吗?删除后不可恢复。`,
|
||||||
|
"删除确认",
|
||||||
|
{ type: "warning" }
|
||||||
|
);
|
||||||
|
await deleteContact({ id: 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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-cell-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-cell {
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
margin-top: 16px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,288 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
:model-value="visible"
|
||||||
|
:title="isEdit ? '编辑客户' : '新增客户'"
|
||||||
|
width="760px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
@update:model-value="handleClose"
|
||||||
|
@closed="handleClosed"
|
||||||
|
>
|
||||||
|
<el-tabs v-model="activeTab">
|
||||||
|
<el-tab-pane label="基本信息" name="basic">
|
||||||
|
<el-form
|
||||||
|
ref="formRef"
|
||||||
|
:model="form"
|
||||||
|
:rules="rules"
|
||||||
|
label-width="110px"
|
||||||
|
label-position="right"
|
||||||
|
>
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="客户名称" prop="customer_name">
|
||||||
|
<el-input v-model="form.customer_name" placeholder="请输入客户名称" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="客户类型" prop="customer_type">
|
||||||
|
<el-select v-model="form.customer_type" placeholder="请选择" style="width: 100%">
|
||||||
|
<el-option
|
||||||
|
v-for="item in CUSTOMER_TYPE_OPTIONS"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="客户等级" prop="customer_level">
|
||||||
|
<el-select v-model="form.customer_level" placeholder="请选择" style="width: 100%">
|
||||||
|
<el-option
|
||||||
|
v-for="item in levelOptions('customer')"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="所属行业" prop="industry">
|
||||||
|
<el-input v-model="form.industry" placeholder="如:互联网、制造业" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="合作开始" prop="register_time">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="form.register_time"
|
||||||
|
type="date"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
placeholder="选择日期"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="合作到期" prop="expire_time">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="form.expire_time"
|
||||||
|
type="date"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
placeholder="选择日期"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="客户地址" prop="address">
|
||||||
|
<el-input v-model="form.address" placeholder="请输入详细地址" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="状态" prop="status">
|
||||||
|
<el-select v-model="form.status" placeholder="请选择" style="width: 100%">
|
||||||
|
<el-option
|
||||||
|
v-for="item in STATUS_OPTIONS"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane label="联系信息" name="contact">
|
||||||
|
<el-form :model="form" label-width="110px" label-position="right">
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="联系人">
|
||||||
|
<el-input v-model="form.contact_person" placeholder="请输入联系人姓名" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="联系电话">
|
||||||
|
<el-input v-model="form.contact_phone" placeholder="请输入联系电话" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="联系邮箱">
|
||||||
|
<el-input v-model="form.contact_email" placeholder="请输入邮箱" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane label="开票信息" name="invoice">
|
||||||
|
<el-form :model="form" label-width="110px" label-position="right">
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="发票抬头">
|
||||||
|
<el-input v-model="form.invoice_title" placeholder="请输入发票抬头" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="纳税人识别号">
|
||||||
|
<el-input v-model="form.tax_number" placeholder="请输入纳税人识别号" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="注册地址">
|
||||||
|
<el-input v-model="form.registered_address" placeholder="请输入注册地址" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="注册电话">
|
||||||
|
<el-input v-model="form.registered_phone" placeholder="请输入注册电话" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="开户行名称">
|
||||||
|
<el-input v-model="form.bank_name" placeholder="请输入开户行名称" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="开户行账号">
|
||||||
|
<el-input v-model="form.bank_account" placeholder="请输入银行账号" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane label="备注" name="remark">
|
||||||
|
<el-form :model="form" label-width="110px" label-position="right">
|
||||||
|
<el-form-item label="备注">
|
||||||
|
<el-input
|
||||||
|
v-model="form.remark"
|
||||||
|
type="textarea"
|
||||||
|
:rows="8"
|
||||||
|
placeholder="请输入备注信息(选填)"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="handleClose">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="submitting" @click="handleSave">保存</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, watch, nextTick } from "vue";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
import { createCrmCustomer, updateCrmCustomer } from "@/api/crm";
|
||||||
|
import {
|
||||||
|
CUSTOMER_TYPE_OPTIONS,
|
||||||
|
STATUS_OPTIONS,
|
||||||
|
levelOptions,
|
||||||
|
} from "../../dict";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
visible: { type: Boolean, default: false },
|
||||||
|
editData: { type: Object, default: null },
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["update:visible", "success"]);
|
||||||
|
|
||||||
|
const formRef = ref();
|
||||||
|
const activeTab = ref("basic");
|
||||||
|
const submitting = ref(false);
|
||||||
|
const isEdit = ref(false);
|
||||||
|
const internalId = ref(null);
|
||||||
|
|
||||||
|
const defaultForm = () => ({
|
||||||
|
customer_name: "",
|
||||||
|
customer_type: "1",
|
||||||
|
customer_level: "3",
|
||||||
|
industry: "",
|
||||||
|
contact_person: "",
|
||||||
|
contact_phone: "",
|
||||||
|
contact_email: "",
|
||||||
|
address: "",
|
||||||
|
register_time: "",
|
||||||
|
expire_time: "",
|
||||||
|
status: "1",
|
||||||
|
remark: "",
|
||||||
|
invoice_title: "",
|
||||||
|
tax_number: "",
|
||||||
|
registered_address: "",
|
||||||
|
registered_phone: "",
|
||||||
|
bank_name: "",
|
||||||
|
bank_account: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = reactive(defaultForm());
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
customer_name: [{ required: true, message: "请输入客户名称", trigger: "blur" }],
|
||||||
|
customer_type: [{ required: true, message: "请选择客户类型", trigger: "change" }],
|
||||||
|
contact_phone: [
|
||||||
|
{
|
||||||
|
pattern: /^(1[3-9]\d{9}|0\d{2,3}-?\d{7,8}|\d{7,8})$/,
|
||||||
|
message: "联系电话格式不正确",
|
||||||
|
trigger: "blur",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
contact_email: [{ type: "email", message: "邮箱格式不正确", trigger: "blur" }],
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.visible,
|
||||||
|
async (val) => {
|
||||||
|
if (!val) return;
|
||||||
|
activeTab.value = "basic";
|
||||||
|
isEdit.value = !!props.editData;
|
||||||
|
internalId.value = props.editData?.id ?? null;
|
||||||
|
if (props.editData) {
|
||||||
|
Object.assign(form, defaultForm(), props.editData);
|
||||||
|
} else {
|
||||||
|
Object.assign(form, defaultForm());
|
||||||
|
}
|
||||||
|
await nextTick();
|
||||||
|
formRef.value?.clearValidate();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleClose() {
|
||||||
|
emit("update:visible", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClosed() {
|
||||||
|
formRef.value?.resetFields();
|
||||||
|
activeTab.value = "basic";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSave() {
|
||||||
|
if (!formRef.value) return;
|
||||||
|
try {
|
||||||
|
await formRef.value.validate();
|
||||||
|
} catch (e) {
|
||||||
|
activeTab.value = "basic";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
submitting.value = true;
|
||||||
|
try {
|
||||||
|
const payload = { ...form, is_draft: 0 };
|
||||||
|
if (internalId.value) {
|
||||||
|
await updateCrmCustomer(internalId.value, payload);
|
||||||
|
ElMessage.success("更新成功");
|
||||||
|
} else {
|
||||||
|
await createCrmCustomer(payload);
|
||||||
|
ElMessage.success("新增成功");
|
||||||
|
}
|
||||||
|
emit("success");
|
||||||
|
handleClose();
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error(e.message || "操作失败");
|
||||||
|
} finally {
|
||||||
|
submitting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,604 @@
|
|||||||
|
<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="warning"
|
||||||
|
plain
|
||||||
|
:disabled="!multipleSelection.length"
|
||||||
|
@click="openMovePool(multipleSelection)"
|
||||||
|
>
|
||||||
|
移入公海
|
||||||
|
</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: 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 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="100">
|
||||||
|
<template #default="{ row }">{{ row.owner_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="280" 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="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="560px"
|
||||||
|
>
|
||||||
|
<div v-if="detailData" class="detail-content">
|
||||||
|
<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_name || '未分配' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="合作开始">{{ formatDate(detailData.register_time) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="合作到期">{{ formatDate(detailData.expire_time) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="客户地址" :span="2">{{ detailData.address || '-' }}</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="mobile" label="手机号" width="130" />
|
||||||
|
<el-table-column prop="email" label="邮箱" min-width="180" show-overflow-tooltip />
|
||||||
|
<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="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"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 移入公海 -->
|
||||||
|
<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 } from "@element-plus/icons-vue";
|
||||||
|
import {
|
||||||
|
getCrmCustomerList,
|
||||||
|
deleteCrmCustomer,
|
||||||
|
moveToPool,
|
||||||
|
} from "@/api/crm";
|
||||||
|
import { listContacts, deleteContact } from "@/api/contact";
|
||||||
|
import CustomerEdit from "./components/edit.vue";
|
||||||
|
import ContactEdit from "../contact/components/edit.vue";
|
||||||
|
import {
|
||||||
|
CUSTOMER_TYPE_OPTIONS,
|
||||||
|
STATUS_OPTIONS,
|
||||||
|
POOL_REASON_OPTIONS,
|
||||||
|
levelOptions,
|
||||||
|
typeText,
|
||||||
|
typeTag,
|
||||||
|
levelText,
|
||||||
|
levelTag,
|
||||||
|
statusText,
|
||||||
|
statusTag,
|
||||||
|
formatDate,
|
||||||
|
formatDateTime,
|
||||||
|
} 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 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,137 @@
|
|||||||
|
/**
|
||||||
|
* CRM 模块通用字典与格式化方法
|
||||||
|
* kind: customer(客户) | supplier(供应商)
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const CUSTOMER_TYPE_OPTIONS = [
|
||||||
|
{ label: "企业", value: "1" },
|
||||||
|
{ label: "政府机构", value: "2" },
|
||||||
|
{ label: "国企", value: "3" },
|
||||||
|
{ label: "教育机构", value: "4" },
|
||||||
|
{ label: "个人", value: "5" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const SUPPLIER_TYPE_OPTIONS = [
|
||||||
|
{ label: "原材料供应商", value: "1" },
|
||||||
|
{ label: "设备供应商", value: "2" },
|
||||||
|
{ label: "服务供应商", value: "3" },
|
||||||
|
{ label: "其他", value: "4" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const STATUS_OPTIONS = [
|
||||||
|
{ label: "正常", value: "1" },
|
||||||
|
{ label: "禁用", value: "0" },
|
||||||
|
{ label: "冻结", value: "2" },
|
||||||
|
{ label: "已注销", value: "3" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const GENDER_OPTIONS = [
|
||||||
|
{ label: "未知", value: 0 },
|
||||||
|
{ label: "男", value: 1 },
|
||||||
|
{ label: "女", value: 2 },
|
||||||
|
];
|
||||||
|
|
||||||
|
/** 联系人关联类型:1=客户,2=供应商 */
|
||||||
|
export const RELATED_TYPE_OPTIONS = [
|
||||||
|
{ label: "客户", value: 1 },
|
||||||
|
{ label: "供应商", value: 2 },
|
||||||
|
];
|
||||||
|
|
||||||
|
/** 客户移入公海原因 */
|
||||||
|
export const POOL_REASON_OPTIONS = [
|
||||||
|
"长期未跟进",
|
||||||
|
"合作终止",
|
||||||
|
"重复建档",
|
||||||
|
"负责人离职",
|
||||||
|
"手动释放",
|
||||||
|
"其他",
|
||||||
|
];
|
||||||
|
|
||||||
|
const TYPE_MAP = {
|
||||||
|
customer: {
|
||||||
|
1: "企业",
|
||||||
|
2: "政府机构",
|
||||||
|
3: "国企",
|
||||||
|
4: "教育机构",
|
||||||
|
5: "个人",
|
||||||
|
},
|
||||||
|
supplier: {
|
||||||
|
1: "原材料供应商",
|
||||||
|
2: "设备供应商",
|
||||||
|
3: "服务供应商",
|
||||||
|
4: "其他",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const LEVEL_MAP = {
|
||||||
|
customer: {
|
||||||
|
1: "核心客户",
|
||||||
|
2: "重要客户",
|
||||||
|
3: "普通客户",
|
||||||
|
4: "潜在客户",
|
||||||
|
},
|
||||||
|
supplier: {
|
||||||
|
1: "核心供应商",
|
||||||
|
2: "重要供应商",
|
||||||
|
3: "普通供应商",
|
||||||
|
4: "潜在供应商",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const STATUS_MAP = { 0: "禁用", 1: "正常", 2: "冻结", 3: "已注销" };
|
||||||
|
const STATUS_TAG_MAP = { 0: "info", 1: "success", 2: "warning", 3: "danger" };
|
||||||
|
const LEVEL_TAG_MAP = { 1: "danger", 2: "warning", 3: "info", 4: "" };
|
||||||
|
const GENDER_MAP = { 0: "未知", 1: "男", 2: "女" };
|
||||||
|
|
||||||
|
const normalize = (val) => (val === undefined || val === null ? "" : String(val));
|
||||||
|
|
||||||
|
export const typeOptions = (kind = "customer") =>
|
||||||
|
(kind === "supplier" ? SUPPLIER_TYPE_OPTIONS : CUSTOMER_TYPE_OPTIONS);
|
||||||
|
|
||||||
|
export const typeText = (kind, val) =>
|
||||||
|
TYPE_MAP[kind]?.[normalize(val)] || normalize(val) || "-";
|
||||||
|
|
||||||
|
export const typeTag = (val) => {
|
||||||
|
const map = { 1: "primary", 2: "warning", 3: "success", 4: "info", 5: "info" };
|
||||||
|
return map[normalize(val)] || "info";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const levelOptions = (kind = "customer") =>
|
||||||
|
Object.entries(LEVEL_MAP[kind] || LEVEL_MAP.customer).map(([value, label]) => ({
|
||||||
|
value,
|
||||||
|
label,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const levelText = (kind, val) =>
|
||||||
|
LEVEL_MAP[kind]?.[normalize(val)] || normalize(val) || "-";
|
||||||
|
|
||||||
|
export const levelTag = (val) => LEVEL_TAG_MAP[normalize(val)] ?? "";
|
||||||
|
|
||||||
|
export const statusText = (val) => STATUS_MAP[normalize(val)] || normalize(val) || "-";
|
||||||
|
|
||||||
|
export const statusTag = (val) => STATUS_TAG_MAP[normalize(val)] || "info";
|
||||||
|
|
||||||
|
export const genderText = (val) => GENDER_MAP[normalize(val)] || "未知";
|
||||||
|
|
||||||
|
export const relatedTypeText = (val) =>
|
||||||
|
normalize(val) === "1" ? "客户" : normalize(val) === "2" ? "供应商" : "-";
|
||||||
|
|
||||||
|
export function formatDate(val) {
|
||||||
|
if (!val) return "-";
|
||||||
|
const d = new Date(val);
|
||||||
|
if (isNaN(d.getTime())) return String(val);
|
||||||
|
const m = String(d.getMonth() + 1).padStart(2, "0");
|
||||||
|
const day = String(d.getDate()).padStart(2, "0");
|
||||||
|
return `${d.getFullYear()}-${m}-${day}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDateTime(val) {
|
||||||
|
if (!val) return "-";
|
||||||
|
const d = new Date(val);
|
||||||
|
if (isNaN(d.getTime())) return String(val);
|
||||||
|
const m = String(d.getMonth() + 1).padStart(2, "0");
|
||||||
|
const day = String(d.getDate()).padStart(2, "0");
|
||||||
|
const h = String(d.getHours()).padStart(2, "0");
|
||||||
|
const min = String(d.getMinutes()).padStart(2, "0");
|
||||||
|
return `${d.getFullYear()}-${m}-${day} ${h}:${min}`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<router-view />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup></script>
|
||||||
|
|
||||||
|
<style lang="less" scoped></style>
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,284 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
:model-value="visible"
|
||||||
|
:title="isEdit ? '编辑供应商' : '新增供应商'"
|
||||||
|
width="760px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
@update:model-value="handleClose"
|
||||||
|
@closed="handleClosed"
|
||||||
|
>
|
||||||
|
<el-tabs v-model="activeTab">
|
||||||
|
<el-tab-pane label="基本信息" name="basic">
|
||||||
|
<el-form
|
||||||
|
ref="formRef"
|
||||||
|
:model="form"
|
||||||
|
:rules="rules"
|
||||||
|
label-width="110px"
|
||||||
|
label-position="right"
|
||||||
|
>
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="供应商名称" prop="supplier_name">
|
||||||
|
<el-input v-model="form.supplier_name" placeholder="请输入供应商名称" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="供应商类型" prop="supplier_type">
|
||||||
|
<el-select v-model="form.supplier_type" placeholder="请选择" style="width: 100%">
|
||||||
|
<el-option
|
||||||
|
v-for="item in SUPPLIER_TYPE_OPTIONS"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="供应商等级" prop="supplier_level">
|
||||||
|
<el-select v-model="form.supplier_level" placeholder="请选择" style="width: 100%">
|
||||||
|
<el-option
|
||||||
|
v-for="item in levelOptions('supplier')"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="所属行业" prop="industry">
|
||||||
|
<el-input v-model="form.industry" placeholder="如:机械设备、电子元器件" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="合作开始" prop="register_time">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="form.register_time"
|
||||||
|
type="date"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
placeholder="选择日期"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="合作到期" prop="expire_time">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="form.expire_time"
|
||||||
|
type="date"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
placeholder="选择日期"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="供应商地址" prop="address">
|
||||||
|
<el-input v-model="form.address" placeholder="请输入详细地址" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="状态" prop="status">
|
||||||
|
<el-select v-model="form.status" placeholder="请选择" style="width: 100%">
|
||||||
|
<el-option
|
||||||
|
v-for="item in STATUS_OPTIONS"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane label="联系信息" name="contact">
|
||||||
|
<el-form :model="form" label-width="110px" label-position="right">
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="联系人">
|
||||||
|
<el-input v-model="form.contact_person" placeholder="请输入联系人姓名" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="联系电话">
|
||||||
|
<el-input v-model="form.contact_phone" placeholder="请输入联系电话" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="联系邮箱">
|
||||||
|
<el-input v-model="form.contact_email" placeholder="请输入邮箱" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane label="开票信息" name="invoice">
|
||||||
|
<el-form :model="form" label-width="110px" label-position="right">
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="发票抬头">
|
||||||
|
<el-input v-model="form.invoice_title" placeholder="请输入发票抬头" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="纳税人识别号">
|
||||||
|
<el-input v-model="form.tax_number" placeholder="请输入纳税人识别号" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="注册地址">
|
||||||
|
<el-input v-model="form.registered_address" placeholder="请输入注册地址" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="注册电话">
|
||||||
|
<el-input v-model="form.registered_phone" placeholder="请输入注册电话" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="开户行名称">
|
||||||
|
<el-input v-model="form.bank_name" placeholder="请输入开户行名称" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="开户行账号">
|
||||||
|
<el-input v-model="form.bank_account" placeholder="请输入银行账号" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane label="备注" name="remark">
|
||||||
|
<el-form :model="form" label-width="110px" label-position="right">
|
||||||
|
<el-form-item label="备注">
|
||||||
|
<el-input
|
||||||
|
v-model="form.remark"
|
||||||
|
type="textarea"
|
||||||
|
:rows="8"
|
||||||
|
placeholder="请输入备注信息(选填)"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="handleClose">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="submitting" @click="handleSave">保存</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, watch, nextTick } from "vue";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
import { createCrmSupplier, updateCrmSupplier } from "@/api/crm";
|
||||||
|
import { SUPPLIER_TYPE_OPTIONS, STATUS_OPTIONS, levelOptions } from "../../dict";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
visible: { type: Boolean, default: false },
|
||||||
|
editData: { type: Object, default: null },
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["update:visible", "success"]);
|
||||||
|
|
||||||
|
const formRef = ref();
|
||||||
|
const activeTab = ref("basic");
|
||||||
|
const submitting = ref(false);
|
||||||
|
const isEdit = ref(false);
|
||||||
|
const internalId = ref(null);
|
||||||
|
|
||||||
|
const defaultForm = () => ({
|
||||||
|
supplier_name: "",
|
||||||
|
supplier_type: "1",
|
||||||
|
supplier_level: "3",
|
||||||
|
industry: "",
|
||||||
|
contact_person: "",
|
||||||
|
contact_phone: "",
|
||||||
|
contact_email: "",
|
||||||
|
address: "",
|
||||||
|
register_time: "",
|
||||||
|
expire_time: "",
|
||||||
|
status: "1",
|
||||||
|
remark: "",
|
||||||
|
invoice_title: "",
|
||||||
|
tax_number: "",
|
||||||
|
registered_address: "",
|
||||||
|
registered_phone: "",
|
||||||
|
bank_name: "",
|
||||||
|
bank_account: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = reactive(defaultForm());
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
supplier_name: [{ required: true, message: "请输入供应商名称", trigger: "blur" }],
|
||||||
|
supplier_type: [{ required: true, message: "请选择供应商类型", trigger: "change" }],
|
||||||
|
contact_phone: [
|
||||||
|
{
|
||||||
|
pattern: /^(1[3-9]\d{9}|0\d{2,3}-?\d{7,8}|\d{7,8})$/,
|
||||||
|
message: "联系电话格式不正确",
|
||||||
|
trigger: "blur",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
contact_email: [{ type: "email", message: "邮箱格式不正确", trigger: "blur" }],
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.visible,
|
||||||
|
async (val) => {
|
||||||
|
if (!val) return;
|
||||||
|
activeTab.value = "basic";
|
||||||
|
isEdit.value = !!props.editData;
|
||||||
|
internalId.value = props.editData?.id ?? null;
|
||||||
|
if (props.editData) {
|
||||||
|
Object.assign(form, defaultForm(), props.editData);
|
||||||
|
} else {
|
||||||
|
Object.assign(form, defaultForm());
|
||||||
|
}
|
||||||
|
await nextTick();
|
||||||
|
formRef.value?.clearValidate();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleClose() {
|
||||||
|
emit("update:visible", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClosed() {
|
||||||
|
formRef.value?.resetFields();
|
||||||
|
activeTab.value = "basic";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSave() {
|
||||||
|
if (!formRef.value) return;
|
||||||
|
try {
|
||||||
|
await formRef.value.validate();
|
||||||
|
} catch (e) {
|
||||||
|
activeTab.value = "basic";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
submitting.value = true;
|
||||||
|
try {
|
||||||
|
const payload = { ...form, is_draft: 0 };
|
||||||
|
if (internalId.value) {
|
||||||
|
await updateCrmSupplier(internalId.value, payload);
|
||||||
|
ElMessage.success("更新成功");
|
||||||
|
} else {
|
||||||
|
await createCrmSupplier(payload);
|
||||||
|
ElMessage.success("新增成功");
|
||||||
|
}
|
||||||
|
emit("success");
|
||||||
|
handleClose();
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error(e.message || "操作失败");
|
||||||
|
} finally {
|
||||||
|
submitting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,533 @@
|
|||||||
|
<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: 240px"
|
||||||
|
@keyup.enter="handleSearch"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="供应商类型">
|
||||||
|
<el-select v-model="filters.supplier_type" clearable placeholder="全部" style="width: 150px">
|
||||||
|
<el-option
|
||||||
|
v-for="item in SUPPLIER_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.supplier_level" clearable placeholder="全部" style="width: 140px">
|
||||||
|
<el-option
|
||||||
|
v-for="item in levelOptions('supplier')"
|
||||||
|
: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 class="table-container" v-loading="loading">
|
||||||
|
<el-table :data="tableData" stripe border row-key="id">
|
||||||
|
<el-table-column label="供应商名称" min-width="200" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span class="name-link" @click="openDetail(row)">{{ row.supplier_name }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="类型" width="140" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="typeTag(row.supplier_type)" size="small">
|
||||||
|
{{ typeText('supplier', row.supplier_type) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="等级" width="120" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="levelTag(row.supplier_level)" size="small" effect="plain">
|
||||||
|
{{ levelText('supplier', row.supplier_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="200" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatDate(row.register_time) }} ~ {{ formatDate(row.expire_time) }}
|
||||||
|
</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="260" 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="primary" size="small" @click="handleToggleStatus(row)">
|
||||||
|
{{ row.status === '1' ? '禁用' : '启用' }}
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<SupplierEdit
|
||||||
|
v-model:visible="editVisible"
|
||||||
|
:edit-data="currentEditData"
|
||||||
|
@success="fetchList"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 详情抽屉 -->
|
||||||
|
<el-drawer
|
||||||
|
v-model="detailVisible"
|
||||||
|
:title="detailData?.supplier_name || '供应商详情'"
|
||||||
|
direction="rtl"
|
||||||
|
size="560px"
|
||||||
|
>
|
||||||
|
<div v-if="detailData" class="detail-content">
|
||||||
|
<el-descriptions title="基本信息" :column="2" border size="small">
|
||||||
|
<el-descriptions-item label="供应商名称" :span="2">
|
||||||
|
{{ detailData.supplier_name }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="供应商类型">
|
||||||
|
{{ typeText('supplier', detailData.supplier_type) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="供应商等级">
|
||||||
|
{{ levelText('supplier', detailData.supplier_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="合作开始">{{ formatDate(detailData.register_time) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="合作到期">{{ formatDate(detailData.expire_time) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="供应商地址" :span="2">
|
||||||
|
{{ detailData.address || '-' }}
|
||||||
|
</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?.supplier_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="mobile" label="手机号" width="130" />
|
||||||
|
<el-table-column prop="email" label="邮箱" min-width="180" show-overflow-tooltip />
|
||||||
|
<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="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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, computed, onMounted } from "vue";
|
||||||
|
import { ElMessage, ElMessageBox } from "element-plus";
|
||||||
|
import { Plus, Search, Refresh } from "@element-plus/icons-vue";
|
||||||
|
import {
|
||||||
|
getCrmSupplierList,
|
||||||
|
deleteCrmSupplier,
|
||||||
|
toggleCrmSupplierStatus,
|
||||||
|
} from "@/api/crm";
|
||||||
|
import { listContacts, deleteContact } from "@/api/contact";
|
||||||
|
import SupplierEdit from "./components/edit.vue";
|
||||||
|
import ContactEdit from "../contact/components/edit.vue";
|
||||||
|
import {
|
||||||
|
SUPPLIER_TYPE_OPTIONS,
|
||||||
|
STATUS_OPTIONS,
|
||||||
|
levelOptions,
|
||||||
|
typeText,
|
||||||
|
typeTag,
|
||||||
|
levelText,
|
||||||
|
levelTag,
|
||||||
|
statusText,
|
||||||
|
statusTag,
|
||||||
|
formatDate,
|
||||||
|
formatDateTime,
|
||||||
|
} from "../dict";
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const tableData = 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 filters = reactive({
|
||||||
|
keyword: "",
|
||||||
|
supplier_type: "",
|
||||||
|
supplier_level: "",
|
||||||
|
status: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const pagination = reactive({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
total: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const contactRelated = computed(() =>
|
||||||
|
currentRow.value
|
||||||
|
? {
|
||||||
|
related_type: 2,
|
||||||
|
related_id: currentRow.value.id,
|
||||||
|
related_name: currentRow.value.supplier_name,
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchList();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function fetchList() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await getCrmSupplierList({
|
||||||
|
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.supplier_type = "";
|
||||||
|
filters.supplier_level = "";
|
||||||
|
filters.status = "";
|
||||||
|
handleSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 handleToggleStatus(row) {
|
||||||
|
const newStatus = row.status === "1" ? "0" : "1";
|
||||||
|
const action = newStatus === "1" ? "启用" : "禁用";
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`确定要${action}供应商「${row.supplier_name}」吗?`, "提示", {
|
||||||
|
type: "warning",
|
||||||
|
});
|
||||||
|
await toggleCrmSupplierStatus(row.id, newStatus);
|
||||||
|
ElMessage.success(`${action}成功`);
|
||||||
|
fetchList();
|
||||||
|
} catch (e) {
|
||||||
|
if (e !== "cancel") {
|
||||||
|
ElMessage.error(e.message || "操作失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDelete(row) {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`确定要删除供应商「${row.supplier_name}」吗?删除后不可恢复。`,
|
||||||
|
"删除确认",
|
||||||
|
{ type: "warning" }
|
||||||
|
);
|
||||||
|
await deleteCrmSupplier(row.id);
|
||||||
|
ElMessage.success("删除成功");
|
||||||
|
fetchList();
|
||||||
|
} catch (e) {
|
||||||
|
if (e !== "cancel") {
|
||||||
|
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: 2, 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 || "删除失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user