Files
yunzerwebsiteallinone/backend/src/views/apps/erp/supplier/index.vue
T
2026-09-14 16:15:44 +08:00

530 lines
19 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="supplier-page">
<div class="page-header">
<div>
<h2>供应商管理</h2>
<p>管理企业供应商信息,支持多维度筛选与全生命周期追踪</p>
</div>
<div class="header-actions">
<el-button type="primary" :icon="Plus" @click="openCreate">新增供应商</el-button>
<el-button type="success" :icon="Cpu" @click="smartAddVisible = true">智能添加</el-button>
</div>
</div>
<div class="filter-bar">
<el-form :inline="true" :model="filters" @submit.prevent>
<el-form-item>
<el-input
v-model="filters.keyword"
clearable
placeholder="搜索供应商名称"
:prefix-icon="Search"
style="width: 220px"
@keyup.enter="handleSearch"
/>
</el-form-item>
<el-form-item label="供应商类型">
<el-select v-model="filters.supplier_type" clearable placeholder="全部" style="width: 140px">
<el-option label="原材料供应商" value="1" />
<el-option label="设备供应商" value="2" />
<el-option label="服务供应商" value="3" />
<el-option label="其他" value="4" />
</el-select>
</el-form-item>
<el-form-item label="供应商等级">
<el-select v-model="filters.supplier_level" clearable placeholder="全部" style="width: 130px">
<el-option label="核心供应商" value="1" />
<el-option label="重要供应商" value="2" />
<el-option label="普通供应商" value="3" />
<el-option label="潜在供应商" value="4" />
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="filters.status" clearable placeholder="全部" style="width: 110px">
<el-option label="正常" value="1" />
<el-option label="禁用" value="0" />
<el-option label="冻结" value="2" />
<el-option label="已注销" value="3" />
</el-select>
</el-form-item>
<el-form-item label="数据状态">
<el-select v-model="filters.is_draft" clearable placeholder="全部" style="width: 110px">
<el-option label="正式" value="0" />
<el-option label="草稿" value="1" />
</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 prop="supplier_name" label="供应商名称" min-width="180" show-overflow-tooltip>
<template #default="{ row }">
<div class="name-cell-wrapper">
<span class="name-cell name-link" @click="openDetail(row)">{{ row.supplier_name }}</span>
<el-tag v-if="row.is_draft === 1" type="warning" size="small" effect="plain">草稿</el-tag>
<!-- 同一家单位可能既是供应商又是客户(已建客户档案) -->
<el-tag v-if="row.also_customer" type="warning" size="small" effect="plain">也是客户</el-tag>
</div>
</template>
</el-table-column>
<el-table-column prop="supplier_type" label="类型" width="120" align="center">
<template #default="{ row }">
<el-tag :type="supplierTypeTag(row.supplier_type)" size="small">
{{ supplierTypeText(row.supplier_type) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="supplier_level" label="等级" width="110" align="center">
<template #default="{ row }">
<el-tag :type="supplierLevelTag(row.supplier_level)" size="small" effect="plain">
{{ supplierLevelText(row.supplier_level) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="contact_person" label="联系人" width="100" />
<el-table-column prop="contact_phone" label="联系电话" width="130" />
<el-table-column prop="contact_email" label="邮箱" min-width="160" show-overflow-tooltip />
<el-table-column prop="industry" label="行业" width="100" show-overflow-tooltip />
<el-table-column prop="status" label="状态" width="80" 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 prop="update_time" label="更新时间" width="160" align="center">
<template #default="{ row }">
{{ formatDateTime(row.update_time) }}
</template>
</el-table-column>
<el-table-column label="操作" width="300" align="center" fixed="right">
<template #default="{ row }">
<el-button link type="primary" size="small" @click="openContactBook(row)">通讯录</el-button>
<el-button link type="primary" size="small" @click="openEdit(row)">编辑</el-button>
<el-button link type="success" size="small" @click="handleConvertCustomer(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>
</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="fetchList"
@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">
<div class="detail-actions">
<el-button type="success" size="small" @click="handleConvertCustomer(detailData)">转为客户</el-button>
<span class="detail-actions-tip">同一家单位可能既是供应商又是客户,转化后原供应商记录保留</span>
</div>
<el-descriptions title="基本信息" :column="2" border size="small" label-width="120px">
<el-descriptions-item label="供应商名称" :span="2">{{ detailData.supplier_name }}</el-descriptions-item>
<el-descriptions-item label="供应商类型">{{ supplierTypeText(detailData.supplier_type) }}</el-descriptions-item>
<el-descriptions-item label="供应商等级">{{ supplierLevelText(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="注册资本">{{ detailData.registered_capital ? detailData.registered_capital + ' 万元' : '-' }}</el-descriptions-item>
<el-descriptions-item label="实缴资本">{{ detailData.paid_capital ? detailData.paid_capital + ' 万元' : '-' }}</el-descriptions-item>
<el-descriptions-item label="成立日期">{{ formatDate(detailData.establish_date) }}</el-descriptions-item>
<el-descriptions-item label="行政区划">{{ detailData.administrative_division || '-' }}</el-descriptions-item>
<el-descriptions-item label="企业类型">{{ detailData.enterprise_type || '-' }}</el-descriptions-item>
<el-descriptions-item label="纳税人资质">{{ detailData.taxpayer_qualification || '-' }}</el-descriptions-item>
<el-descriptions-item label="经营范围" :span="2">{{ detailData.business_scope || '-' }}</el-descriptions-item>
<el-descriptions-item label="更新时间" :span="2">{{ formatDateTime(detailData.update_time) }}</el-descriptions-item>
</el-descriptions>
<el-descriptions title="联系信息" :column="2" border size="small" label-width="120px" 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-item label="供应商地址" :span="2">{{ detailData.address || '-' }}</el-descriptions-item>
</el-descriptions>
<el-descriptions :column="2" border size="small" label-width="120px" style="margin-top: 20px">
<template #title>
<div class="desc-title-row">
<span>开票信息</span>
<el-button link type="primary" size="small" @click="copyInvoiceInfo">一键复制</el-button>
</div>
</template>
<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" label-width="120px" style="margin-top: 20px">
<el-descriptions-item label="备注">{{ detailData.remark || '-' }}</el-descriptions-item>
</el-descriptions>
</div>
</el-drawer>
<!-- 通讯录 -->
<ContactBook
v-model:visible="contactBookVisible"
:company-type="'supplier'"
:company-id="currentCompany?.id"
:company-name="currentCompany?.supplier_name"
/>
<SmartAddCompany
v-model:visible="smartAddVisible"
type="supplier"
@success="fetchList"
/>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Search, Refresh, Cpu } from '@element-plus/icons-vue'
import { getSupplierList, deleteSupplier, toggleSupplierStatus, convertSupplierToCustomer } from '@/api/supplier'
import SupplierEdit from './components/edit.vue'
import ContactBook from '../components/contactBook.vue'
import SmartAddCompany from '../components/smartAddCompany.vue'
const loading = ref(false)
const tableData = ref([])
const editVisible = ref(false)
const currentEditData = ref(null)
const smartAddVisible = ref(false)
const detailVisible = ref(false)
const detailData = ref(null)
const contactBookVisible = ref(false)
const currentCompany = ref(null)
const filters = reactive({
keyword: '',
supplier_type: '',
supplier_level: '',
status: '',
is_draft: ''
})
const pagination = reactive({
page: 1,
pageSize: 20,
total: 0
})
onMounted(() => {
fetchList()
})
async function fetchList() {
loading.value = true
try {
const res = await getSupplierList({
page: pagination.page,
pageSize: pagination.pageSize,
...filters
})
if (res.code === 200) {
tableData.value = res.data.list || []
pagination.total = res.data.total || 0
}
} catch (e) {
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 = ''
filters.is_draft = ''
pagination.page = 1
fetchList()
}
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
}
function openContactBook(row) {
currentCompany.value = { ...row }
contactBookVisible.value = true
}
function formatDate(val) {
if (!val) return '-'
const d = new Date(val)
if (isNaN(d.getTime())) return '-'
return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0')
}
function formatDateTime(val) {
if (!val) return '-'
const d = new Date(val)
return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0') + ' ' + String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0')
}
function copyInvoiceInfo() {
const d = detailData.value
if (!d) return
const lines = [
`发票抬头:${d.invoice_title || ''}`,
`纳税人识别号:${d.tax_number || ''}`,
`注册地址:${d.registered_address || ''}`,
`注册电话:${d.registered_phone || ''}`,
`开户行名称:${d.bank_name || ''}`,
`开户行账号:${d.bank_account || ''}`
]
const text = lines.join('\n')
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(() => {
ElMessage.success('开票信息已复制到剪贴板')
}).catch(() => {
fallbackCopy(text)
})
} else {
fallbackCopy(text)
}
}
function fallbackCopy(text) {
const ta = document.createElement('textarea')
ta.value = text
ta.style.position = 'fixed'
ta.style.opacity = '0'
document.body.appendChild(ta)
ta.select()
try {
document.execCommand('copy')
ElMessage.success('开票信息已复制到剪贴板')
} catch {
ElMessage.error('复制失败,请手动复制')
}
document.body.removeChild(ta)
}
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 toggleSupplierStatus(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 deleteSupplier(row.id)
ElMessage.success('删除成功')
fetchList()
} catch (e) {
if (e !== 'cancel') {
ElMessage.error(e.message || '删除失败')
}
}
}
/**
* 供应商转客户:把供应商的企业资料与联系人复制一套到客户库。
* 同一家单位可能既是供应商又是客户,原供应商记录保留;客户库已有同名企业时只补同步联系人,不重复建档。
*/
async function handleConvertCustomer(row) {
try {
await ElMessageBox.confirm(
`将供应商「${row.supplier_name}」的企业资料与联系人复制一套到客户库?原供应商记录会保留,可在客户管理中继续完善。`,
'转为客户',
{ type: 'warning', confirmButtonText: '确认转化' }
)
} catch (e) {
return // 取消
}
try {
const res = await convertSupplierToCustomer(row.id)
const data = res?.data || {}
const c = data.contacts || {}
ElMessage.success(
data.existed
? `客户库已有同名企业,已同步联系人(新增 ${c.inserted || 0}、补充 ${c.merged || 0}),未重复建档`
: `已转为客户,并同步联系人(新增 ${c.inserted || 0})`
)
fetchList()
} catch (e) {
ElMessage.error(e.message || '转化失败')
}
}
function supplierTypeText(type) {
const map = { '1': '原材料供应商', '2': '设备供应商', '3': '服务供应商', '4': '其他' }
return map[type] || type
}
function supplierTypeTag(type) {
const map = { '1': 'primary', '2': 'warning', '3': 'success', '4': 'info' }
return map[type] || 'info'
}
function supplierLevelText(level) {
const map = { '1': '核心供应商', '2': '重要供应商', '3': '普通供应商', '4': '潜在供应商' }
return map[level] || level
}
function supplierLevelTag(level) {
const map = { '1': 'danger', '2': 'warning', '3': 'info', '4': '' }
return map[level] || ''
}
function statusText(status) {
const map = { '0': '禁用', '1': '正常', '2': '冻结', '3': '已注销' }
return map[status] || status
}
function statusTag(status) {
const map = { '0': 'info', '1': 'success', '2': 'warning', '3': 'danger' }
return map[status] || 'info'
}
</script>
<style lang="less" scoped>
.supplier-page {
padding: 20px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 20px;
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: 4px;
margin-bottom: 16px;
border: 1px solid var(--el-border-color-lighter);
}
.table-container {
background: var(--el-bg-color);
padding: 16px;
border-radius: 4px;
border: 1px solid var(--el-border-color-lighter);
}
.name-cell {
font-weight: 500;
color: var(--el-text-color-primary);
}
.name-cell-wrapper {
display: flex;
align-items: center;
gap: 8px;
}
.name-link {
cursor: pointer;
color: var(--el-color-primary);
&:hover {
text-decoration: underline;
}
}
.desc-title-row {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.pagination {
margin-top: 16px;
display: flex;
justify-content: flex-end;
}
.detail-actions {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 16px;
}
.detail-actions-tip {
font-size: 12px;
color: var(--el-text-color-secondary);
}
</style>