Files
yunzerwebsiteallinone/backend/src/views/apps/crm/supplier/index.vue
T
2026-09-10 20:18:11 +08:00

534 lines
17 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="crm-page">
<div class="page-header">
<div>
<h2>供应商管理</h2>
<p>维护供应商档案、合作期限与对接人,支撑采购与对账业务</p>
</div>
<div class="header-actions">
<el-button :icon="Refresh" @click="fetchList">刷新</el-button>
<el-button type="primary" :icon="Plus" @click="openCreate">新增供应商</el-button>
</div>
</div>
<div class="filter-bar">
<el-form :inline="true" :model="filters" @submit.prevent>
<el-form-item>
<el-input
v-model="filters.keyword"
clearable
placeholder="搜索供应商名称 / 联系人 / 电话"
:prefix-icon="Search"
style="width: 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>