更新各项内容
This commit is contained in:
@@ -86,11 +86,6 @@ export function assignPoolCustomer(data) {
|
|||||||
return request({ url: "/backend/crm/pool/assign", method: "post", 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 移入原因 */
|
/** 移入公海:ids 客户ID集合,reason 移入原因 */
|
||||||
export function moveToPool(data) {
|
export function moveToPool(data) {
|
||||||
return request({ url: "/backend/crm/pool/move", method: "post", data });
|
return request({ url: "/backend/crm/pool/move", method: "post", data });
|
||||||
|
|||||||
@@ -80,17 +80,32 @@
|
|||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="座机" prop="phone">
|
<el-form-item label="座机" prop="phone">
|
||||||
<el-input v-model="form.phone" placeholder="请输入座机号(选填)" />
|
<el-input v-model="form.phone" placeholder="请输入座机号" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="邮箱" prop="email">
|
<el-form-item label="邮箱" prop="email">
|
||||||
<el-input v-model="form.email" placeholder="请输入邮箱(选填)" />
|
<el-input v-model="form.email" placeholder="请输入邮箱" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="微信" prop="wechat">
|
||||||
|
<el-input v-model="form.wechat" placeholder="请输入微信号" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="QQ" prop="qq">
|
||||||
|
<el-input v-model="form.qq" placeholder="请输入QQ号" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="钉钉" prop="dingtalk">
|
||||||
|
<el-input v-model="form.dingtalk" placeholder="请输入钉钉号" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="部门" prop="department">
|
<el-form-item label="部门" prop="department">
|
||||||
<el-input v-model="form.department" placeholder="请输入所属部门(选填)" />
|
<el-input v-model="form.department" placeholder="请输入所属部门" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
@@ -109,13 +124,29 @@
|
|||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="在职状态" prop="status">
|
||||||
|
<el-switch
|
||||||
|
v-model="form.status"
|
||||||
|
:active-value="1"
|
||||||
|
:inactive-value="0"
|
||||||
|
active-text="在职"
|
||||||
|
inactive-text="离职"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="家庭住址" prop="home_address">
|
||||||
|
<el-input v-model="form.home_address" placeholder="请输入家庭住址" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
<el-col :span="24">
|
<el-col :span="24">
|
||||||
<el-form-item label="备注" prop="remark">
|
<el-form-item label="备注" prop="remark">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="form.remark"
|
v-model="form.remark"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
:rows="3"
|
:rows="3"
|
||||||
placeholder="如:关键决策人、对接注意事项(选填)"
|
placeholder="如:关键决策人、对接注意事项"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
@@ -162,9 +193,14 @@ const defaultForm = () => ({
|
|||||||
mobile: "",
|
mobile: "",
|
||||||
phone: "",
|
phone: "",
|
||||||
email: "",
|
email: "",
|
||||||
|
wechat: "",
|
||||||
|
qq: "",
|
||||||
|
dingtalk: "",
|
||||||
department: "",
|
department: "",
|
||||||
position: "",
|
position: "",
|
||||||
|
home_address: "",
|
||||||
is_primary: 0,
|
is_primary: 0,
|
||||||
|
status: 1,
|
||||||
remark: "",
|
remark: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -175,10 +211,14 @@ const rules = {
|
|||||||
related_id: [{ required: true, message: "请选择关联对象", trigger: "change" }],
|
related_id: [{ required: true, message: "请选择关联对象", trigger: "change" }],
|
||||||
contact_name: [{ required: true, message: "请输入联系人姓名", trigger: "blur" }],
|
contact_name: [{ required: true, message: "请输入联系人姓名", trigger: "blur" }],
|
||||||
mobile: [
|
mobile: [
|
||||||
{ required: true, message: "请输入手机号", trigger: "blur" },
|
|
||||||
{
|
{
|
||||||
pattern: /^1[3-9]\d{9}$/,
|
// 手机号非必填,填写时才校验格式
|
||||||
message: "手机号格式不正确",
|
validator: (rule, value, callback) => {
|
||||||
|
const val = (value || "").trim();
|
||||||
|
if (!val) return callback();
|
||||||
|
if (!/^1[3-9]\d{9}$/.test(val)) return callback(new Error("手机号格式不正确"));
|
||||||
|
callback();
|
||||||
|
},
|
||||||
trigger: "blur",
|
trigger: "blur",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
<el-table-column label="姓名" min-width="140" show-overflow-tooltip>
|
<el-table-column label="姓名" min-width="140" show-overflow-tooltip>
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div class="name-cell-wrapper">
|
<div class="name-cell-wrapper">
|
||||||
<span class="name-cell">{{ row.contact_name }}</span>
|
<span class="name-cell name-link" @click="openDetail(row)">{{ row.contact_name }}</span>
|
||||||
<el-tag v-if="Number(row.is_primary) === 1" type="danger" size="small" effect="plain">
|
<el-tag v-if="Number(row.is_primary) === 1" type="danger" size="small" effect="plain">
|
||||||
主联系人
|
主联系人
|
||||||
</el-tag>
|
</el-tag>
|
||||||
@@ -63,8 +63,6 @@
|
|||||||
<template #default="{ row }">{{ genderText(row.gender) }}</template>
|
<template #default="{ row }">{{ genderText(row.gender) }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="mobile" label="手机号" width="130" />
|
<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">
|
<el-table-column label="关联类型" width="100" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag size="small" :type="Number(row.related_type) === 1 ? 'primary' : 'warning'">
|
<el-tag size="small" :type="Number(row.related_type) === 1 ? 'primary' : 'warning'">
|
||||||
@@ -118,6 +116,51 @@
|
|||||||
:edit-data="currentEditData"
|
:edit-data="currentEditData"
|
||||||
@success="fetchList"
|
@success="fetchList"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- 联系人详情 -->
|
||||||
|
<el-drawer
|
||||||
|
v-model="detailVisible"
|
||||||
|
:title="detailData?.contact_name || '联系人详情'"
|
||||||
|
direction="rtl"
|
||||||
|
size="540px"
|
||||||
|
>
|
||||||
|
<div v-if="detailData">
|
||||||
|
<el-descriptions title="基本信息" :column="2" border size="small">
|
||||||
|
<el-descriptions-item label="姓名">{{ detailData.contact_name }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="性别">{{ genderText(detailData.gender) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="关联类型">
|
||||||
|
<el-tag size="small" :type="Number(detailData.related_type) === 1 ? 'primary' : 'warning'">
|
||||||
|
{{ relatedTypeText(detailData.related_type) }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="关联对象">{{ detailData.related_name || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="部门">{{ detailData.department || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="职位">{{ detailData.position || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="手机号">{{ detailData.mobile || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="座机">{{ detailData.phone || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="邮箱">{{ detailData.email || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="微信">{{ detailData.wechat || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="QQ">{{ detailData.qq || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="钉钉">{{ detailData.dingtalk || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="主联系人">
|
||||||
|
<el-tag v-if="Number(detailData.is_primary) === 1" type="danger" size="small">是</el-tag>
|
||||||
|
<span v-else>否</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="在职状态">
|
||||||
|
<el-tag :type="Number(detailData.status) === 1 ? 'success' : 'info'" size="small">
|
||||||
|
{{ Number(detailData.status) === 1 ? '在职' : '离职' }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="家庭住址" :span="2">
|
||||||
|
{{ detailData.home_address || '-' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="备注" :span="2">{{ detailData.remark || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="更新时间" :span="2">
|
||||||
|
{{ formatDateTime(detailData.update_time) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
|
</el-drawer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -139,6 +182,10 @@ const tableData = ref([]);
|
|||||||
const editVisible = ref(false);
|
const editVisible = ref(false);
|
||||||
const currentEditData = ref(null);
|
const currentEditData = ref(null);
|
||||||
|
|
||||||
|
// 详情
|
||||||
|
const detailVisible = ref(false);
|
||||||
|
const detailData = ref(null);
|
||||||
|
|
||||||
const filters = reactive({
|
const filters = reactive({
|
||||||
keyword: "",
|
keyword: "",
|
||||||
related_type: "",
|
related_type: "",
|
||||||
@@ -193,6 +240,11 @@ function openCreate() {
|
|||||||
editVisible.value = true;
|
editVisible.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openDetail(row) {
|
||||||
|
detailData.value = { ...row };
|
||||||
|
detailVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
function openEdit(row) {
|
function openEdit(row) {
|
||||||
currentEditData.value = { ...row };
|
currentEditData.value = { ...row };
|
||||||
editVisible.value = true;
|
editVisible.value = true;
|
||||||
@@ -277,6 +329,15 @@ async function handleDelete(row) {
|
|||||||
color: var(--el-text-color-primary);
|
color: var(--el-text-color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.name-link {
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.pagination {
|
.pagination {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,188 +1,254 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog
|
<el-dialog
|
||||||
:model-value="visible"
|
:model-value="visible"
|
||||||
:title="isEdit ? '编辑客户' : '新增客户'"
|
:title="dialogTitle"
|
||||||
width="760px"
|
width="680px"
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
@update:model-value="handleClose"
|
@update:model-value="handleClose"
|
||||||
@closed="handleClosed"
|
@closed="handleClosed"
|
||||||
>
|
>
|
||||||
<el-tabs v-model="activeTab">
|
<!-- 步骤条 -->
|
||||||
<el-tab-pane label="基本信息" name="basic">
|
<div class="steps-wrapper">
|
||||||
<el-form
|
<el-steps :active="activeStep" finish-status="success" align-center>
|
||||||
ref="formRef"
|
<el-step title="基本信息" />
|
||||||
:model="form"
|
<el-step title="联系信息" />
|
||||||
:rules="rules"
|
<el-step title="开票信息" />
|
||||||
label-width="110px"
|
<el-step title="备注信息" />
|
||||||
label-position="right"
|
</el-steps>
|
||||||
>
|
</div>
|
||||||
<el-row :gutter="16">
|
|
||||||
<el-col :span="12">
|
<el-form
|
||||||
<el-form-item label="客户名称" prop="customer_name">
|
ref="formRef"
|
||||||
<el-input v-model="form.customer_name" placeholder="请输入客户名称" />
|
:model="form"
|
||||||
</el-form-item>
|
:rules="allRules"
|
||||||
</el-col>
|
label-width="100px"
|
||||||
<el-col :span="12">
|
label-position="right"
|
||||||
<el-form-item label="客户类型" prop="customer_type">
|
style="margin-top: 24px"
|
||||||
<el-select v-model="form.customer_type" placeholder="请选择" style="width: 100%">
|
>
|
||||||
<el-option
|
<!-- 步骤1:基本信息 -->
|
||||||
v-for="item in CUSTOMER_TYPE_OPTIONS"
|
<div v-show="activeStep === 0">
|
||||||
:key="item.value"
|
<el-row :gutter="20">
|
||||||
:label="item.label"
|
<el-col :span="12">
|
||||||
:value="item.value"
|
<el-form-item label="客户名称" prop="customer_name">
|
||||||
/>
|
<el-input v-model="form.customer_name" placeholder="请输入客户名称" />
|
||||||
</el-select>
|
</el-form-item>
|
||||||
</el-form-item>
|
</el-col>
|
||||||
</el-col>
|
<el-col :span="12">
|
||||||
<el-col :span="12">
|
<el-form-item label="客户类型" prop="customer_type">
|
||||||
<el-form-item label="客户等级" prop="customer_level">
|
<el-select v-model="form.customer_type" placeholder="请选择" style="width: 100%">
|
||||||
<el-select v-model="form.customer_level" placeholder="请选择" style="width: 100%">
|
<el-option
|
||||||
<el-option
|
v-for="item in CUSTOMER_TYPE_OPTIONS"
|
||||||
v-for="item in levelOptions('customer')"
|
:key="item.value"
|
||||||
:key="item.value"
|
:label="item.label"
|
||||||
:label="item.label"
|
:value="item.value"
|
||||||
: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-select>
|
||||||
</el-col>
|
</el-form-item>
|
||||||
<el-col :span="12">
|
</el-col>
|
||||||
<el-form-item label="合作到期" prop="expire_time">
|
<el-col :span="12">
|
||||||
<el-date-picker
|
<el-form-item label="客户等级" prop="customer_level">
|
||||||
v-model="form.expire_time"
|
<el-select v-model="form.customer_level" placeholder="请选择" style="width: 100%">
|
||||||
type="date"
|
<el-option
|
||||||
value-format="YYYY-MM-DD"
|
v-for="item in levelOptions('customer')"
|
||||||
placeholder="选择日期"
|
:key="item.value"
|
||||||
style="width: 100%"
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-select>
|
||||||
</el-col>
|
</el-form-item>
|
||||||
<el-col :span="24">
|
</el-col>
|
||||||
<el-form-item label="客户地址" prop="address">
|
<el-col :span="12">
|
||||||
<el-input v-model="form.address" placeholder="请输入详细地址" />
|
<el-form-item label="所属行业" prop="industry">
|
||||||
</el-form-item>
|
<el-input v-model="form.industry" placeholder="如:互联网、制造业" />
|
||||||
</el-col>
|
</el-form-item>
|
||||||
<el-col :span="12">
|
</el-col>
|
||||||
<el-form-item label="状态" prop="status">
|
<el-col :span="12">
|
||||||
<el-select v-model="form.status" placeholder="请选择" style="width: 100%">
|
<el-form-item label="注册资本" prop="registered_capital">
|
||||||
<el-option
|
<el-input v-model="form.registered_capital" placeholder="万元">
|
||||||
v-for="item in STATUS_OPTIONS"
|
<template #append>万元</template>
|
||||||
:key="item.value"
|
</el-input>
|
||||||
:label="item.label"
|
</el-form-item>
|
||||||
:value="item.value"
|
</el-col>
|
||||||
/>
|
<el-col :span="12">
|
||||||
</el-select>
|
<el-form-item label="实缴资本" prop="paid_capital">
|
||||||
</el-form-item>
|
<el-input v-model="form.paid_capital" placeholder="万元">
|
||||||
</el-col>
|
<template #append>万元</template>
|
||||||
</el-row>
|
</el-input>
|
||||||
</el-form>
|
</el-form-item>
|
||||||
</el-tab-pane>
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="成立日期" prop="establish_date">
|
||||||
|
<el-date-picker v-model="form.establish_date" 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="administrative_division">
|
||||||
|
<el-input v-model="form.administrative_division" placeholder="省/市/区" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="企业类型" prop="enterprise_type">
|
||||||
|
<el-select v-model="form.enterprise_type" placeholder="请选择" style="width: 100%">
|
||||||
|
<el-option label="有限责任公司" value="有限责任公司" />
|
||||||
|
<el-option label="股份有限公司" value="股份有限公司" />
|
||||||
|
<el-option label="合伙企业" value="合伙企业" />
|
||||||
|
<el-option label="个人独资企业" value="个人独资企业" />
|
||||||
|
<el-option label="国有企业" value="国有企业" />
|
||||||
|
<el-option label="集体企业" value="集体企业" />
|
||||||
|
<el-option label="外商投资企业" value="外商投资企业" />
|
||||||
|
<el-option label="其他" value="其他" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="纳税人资质" prop="taxpayer_qualification">
|
||||||
|
<el-select v-model="form.taxpayer_qualification" placeholder="请选择" style="width: 100%">
|
||||||
|
<el-option label="一般纳税人" value="一般纳税人" />
|
||||||
|
<el-option label="小规模纳税人" value="小规模纳税人" />
|
||||||
|
<el-option label="其他" value="其他" />
|
||||||
|
</el-select>
|
||||||
|
</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-col :span="24">
|
||||||
|
<el-form-item label="经营范围" prop="business_scope">
|
||||||
|
<el-input v-model="form.business_scope" type="textarea" :rows="3" placeholder="请输入经营范围(选填)" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
|
||||||
<el-tab-pane label="联系信息" name="contact">
|
<!-- 步骤2:联系信息 -->
|
||||||
<el-form :model="form" label-width="110px" label-position="right">
|
<div v-show="activeStep === 1">
|
||||||
<el-row :gutter="16">
|
<el-row :gutter="20">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="联系人">
|
<el-form-item label="联系人" prop="contact_person">
|
||||||
<el-input v-model="form.contact_person" placeholder="请输入联系人姓名" />
|
<el-input v-model="form.contact_person" placeholder="请输入联系人姓名" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="联系电话">
|
<el-form-item label="联系电话" prop="contact_phone">
|
||||||
<el-input v-model="form.contact_phone" placeholder="请输入联系电话" />
|
<el-input v-model="form.contact_phone" placeholder="请输入联系电话" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="联系邮箱">
|
<el-form-item label="联系邮箱" prop="contact_email">
|
||||||
<el-input v-model="form.contact_email" placeholder="请输入邮箱" />
|
<el-input v-model="form.contact_email" placeholder="请输入邮箱" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
<el-col :span="24">
|
||||||
</el-form>
|
<el-form-item label="客户地址" prop="address">
|
||||||
</el-tab-pane>
|
<el-input v-model="form.address" placeholder="请输入详细地址" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
|
||||||
<el-tab-pane label="开票信息" name="invoice">
|
<!-- 步骤3:开票信息(发票+银行合并) -->
|
||||||
<el-form :model="form" label-width="110px" label-position="right">
|
<div v-show="activeStep === 2">
|
||||||
<el-row :gutter="16">
|
<el-divider content-position="left">发票信息</el-divider>
|
||||||
<el-col :span="12">
|
<el-row :gutter="20">
|
||||||
<el-form-item label="发票抬头">
|
<el-col :span="12">
|
||||||
<el-input v-model="form.invoice_title" placeholder="请输入发票抬头" />
|
<el-form-item label="发票抬头" prop="invoice_title">
|
||||||
</el-form-item>
|
<el-input v-model="form.invoice_title" placeholder="请输入发票抬头" />
|
||||||
</el-col>
|
</el-form-item>
|
||||||
<el-col :span="12">
|
</el-col>
|
||||||
<el-form-item label="纳税人识别号">
|
<el-col :span="12">
|
||||||
<el-input v-model="form.tax_number" placeholder="请输入纳税人识别号" />
|
<el-form-item label="纳税人识别号" prop="tax_number">
|
||||||
</el-form-item>
|
<el-input v-model="form.tax_number" placeholder="请输入纳税人识别号" />
|
||||||
</el-col>
|
</el-form-item>
|
||||||
<el-col :span="24">
|
</el-col>
|
||||||
<el-form-item label="注册地址">
|
<el-col :span="24">
|
||||||
<el-input v-model="form.registered_address" placeholder="请输入注册地址" />
|
<el-form-item label="注册地址" prop="registered_address">
|
||||||
</el-form-item>
|
<el-input v-model="form.registered_address" placeholder="请输入注册地址" />
|
||||||
</el-col>
|
</el-form-item>
|
||||||
<el-col :span="12">
|
</el-col>
|
||||||
<el-form-item label="注册电话">
|
<el-col :span="12">
|
||||||
<el-input v-model="form.registered_phone" placeholder="请输入注册电话" />
|
<el-form-item label="注册电话" prop="registered_phone">
|
||||||
</el-form-item>
|
<el-input v-model="form.registered_phone" placeholder="请输入注册电话" />
|
||||||
</el-col>
|
</el-form-item>
|
||||||
<el-col :span="12">
|
</el-col>
|
||||||
<el-form-item label="开户行名称">
|
</el-row>
|
||||||
<el-input v-model="form.bank_name" placeholder="请输入开户行名称" />
|
<el-divider content-position="left">银行信息</el-divider>
|
||||||
</el-form-item>
|
<el-row :gutter="20">
|
||||||
</el-col>
|
<el-col :span="12">
|
||||||
<el-col :span="12">
|
<el-form-item label="开户行名称" prop="bank_name">
|
||||||
<el-form-item label="开户行账号">
|
<el-input v-model="form.bank_name" placeholder="请输入开户行名称" />
|
||||||
<el-input v-model="form.bank_account" placeholder="请输入银行账号" />
|
</el-form-item>
|
||||||
</el-form-item>
|
</el-col>
|
||||||
</el-col>
|
<el-col :span="12">
|
||||||
</el-row>
|
<el-form-item label="开户行账号" prop="bank_account">
|
||||||
</el-form>
|
<el-input v-model="form.bank_account" placeholder="请输入开户行账号" />
|
||||||
</el-tab-pane>
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
|
||||||
<el-tab-pane label="备注" name="remark">
|
<!-- 步骤4:备注信息 -->
|
||||||
<el-form :model="form" label-width="110px" label-position="right">
|
<div v-show="activeStep === 3">
|
||||||
<el-form-item label="备注">
|
<el-form-item label="备注" prop="remark">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="form.remark"
|
v-model="form.remark"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
:rows="8"
|
:rows="10"
|
||||||
placeholder="请输入备注信息(选填)"
|
placeholder="请输入备注信息(选填)"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
<div class="final-step-tip">
|
||||||
</el-tab-pane>
|
<el-icon><InfoFilled /></el-icon>
|
||||||
</el-tabs>
|
<span>确认所有信息无误后,点击「保存」完成创建。</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="handleClose">取消</el-button>
|
<div class="dialog-footer">
|
||||||
<el-button type="primary" :loading="submitting" @click="handleSave">保存</el-button>
|
<div class="footer-left">
|
||||||
|
<el-button v-if="activeStep > 0" @click="prevStep">上一步</el-button>
|
||||||
|
<el-button v-if="activeStep < 3" type="primary" @click="nextStep"
|
||||||
|
>下一步</el-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="footer-right">
|
||||||
|
<el-button @click="handleClose">取消</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="!isEdit || form.is_draft === 1"
|
||||||
|
:loading="submitting"
|
||||||
|
@click="handleSaveDraft"
|
||||||
|
>
|
||||||
|
保存草稿
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="activeStep === 3"
|
||||||
|
type="primary"
|
||||||
|
:loading="submitting"
|
||||||
|
@click="handleSave(false)"
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, watch, nextTick } from "vue";
|
import { ref, reactive, computed, watch } from "vue";
|
||||||
import { ElMessage } from "element-plus";
|
import { ElMessage } from "element-plus";
|
||||||
|
import { InfoFilled } from "@element-plus/icons-vue";
|
||||||
import { createCrmCustomer, updateCrmCustomer } from "@/api/crm";
|
import { createCrmCustomer, updateCrmCustomer } from "@/api/crm";
|
||||||
import {
|
import { CUSTOMER_TYPE_OPTIONS, STATUS_OPTIONS, levelOptions } from "../../dict";
|
||||||
CUSTOMER_TYPE_OPTIONS,
|
|
||||||
STATUS_OPTIONS,
|
|
||||||
levelOptions,
|
|
||||||
} from "../../dict";
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
visible: { type: Boolean, default: false },
|
visible: { type: Boolean, default: false },
|
||||||
@@ -192,23 +258,36 @@ const props = defineProps({
|
|||||||
const emit = defineEmits(["update:visible", "success"]);
|
const emit = defineEmits(["update:visible", "success"]);
|
||||||
|
|
||||||
const formRef = ref();
|
const formRef = ref();
|
||||||
const activeTab = ref("basic");
|
const activeStep = ref(0);
|
||||||
const submitting = ref(false);
|
const submitting = ref(false);
|
||||||
const isEdit = ref(false);
|
const isEdit = ref(false);
|
||||||
const internalId = ref(null);
|
const internalId = ref(null);
|
||||||
|
|
||||||
|
const dialogTitle = computed(() => {
|
||||||
|
if (isEdit.value) {
|
||||||
|
return form.is_draft === 1 ? "编辑客户(草稿)" : "编辑客户";
|
||||||
|
}
|
||||||
|
return "新增客户";
|
||||||
|
});
|
||||||
|
|
||||||
const defaultForm = () => ({
|
const defaultForm = () => ({
|
||||||
customer_name: "",
|
customer_name: "",
|
||||||
customer_type: "1",
|
customer_type: "1",
|
||||||
customer_level: "3",
|
customer_level: "3",
|
||||||
industry: "",
|
industry: "",
|
||||||
|
registered_capital: "",
|
||||||
|
paid_capital: "",
|
||||||
|
establish_date: "",
|
||||||
|
administrative_division: "",
|
||||||
|
enterprise_type: "有限责任公司",
|
||||||
|
taxpayer_qualification: "一般纳税人",
|
||||||
|
business_scope: "",
|
||||||
|
address: "",
|
||||||
contact_person: "",
|
contact_person: "",
|
||||||
contact_phone: "",
|
contact_phone: "",
|
||||||
contact_email: "",
|
contact_email: "",
|
||||||
address: "",
|
|
||||||
register_time: "",
|
|
||||||
expire_time: "",
|
|
||||||
status: "1",
|
status: "1",
|
||||||
|
is_draft: 0,
|
||||||
remark: "",
|
remark: "",
|
||||||
invoice_title: "",
|
invoice_title: "",
|
||||||
tax_number: "",
|
tax_number: "",
|
||||||
@@ -220,33 +299,57 @@ const defaultForm = () => ({
|
|||||||
|
|
||||||
const form = reactive(defaultForm());
|
const form = reactive(defaultForm());
|
||||||
|
|
||||||
const rules = {
|
// 各步骤的验证规则
|
||||||
customer_name: [{ required: true, message: "请输入客户名称", trigger: "blur" }],
|
const stepRules = [
|
||||||
customer_type: [{ required: true, message: "请选择客户类型", trigger: "change" }],
|
// 步骤1:基本信息
|
||||||
contact_phone: [
|
{
|
||||||
{
|
customer_name: [
|
||||||
pattern: /^(1[3-9]\d{9}|0\d{2,3}-?\d{7,8}|\d{7,8})$/,
|
{ required: true, message: "请输入客户名称", trigger: "blur" },
|
||||||
message: "联系电话格式不正确",
|
],
|
||||||
trigger: "blur",
|
customer_type: [
|
||||||
},
|
{ required: true, message: "请选择客户类型", trigger: "change" },
|
||||||
],
|
],
|
||||||
contact_email: [{ type: "email", message: "邮箱格式不正确", trigger: "blur" }],
|
},
|
||||||
};
|
// 步骤2:联系信息
|
||||||
|
{
|
||||||
|
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" }],
|
||||||
|
},
|
||||||
|
// 步骤3:开票信息(无必填)
|
||||||
|
{},
|
||||||
|
// 步骤4:备注信息(无必填)
|
||||||
|
{},
|
||||||
|
];
|
||||||
|
|
||||||
|
const allRules = computed(() => {
|
||||||
|
const merged = {};
|
||||||
|
stepRules.forEach((rules) => {
|
||||||
|
Object.assign(merged, rules);
|
||||||
|
});
|
||||||
|
return merged;
|
||||||
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.visible,
|
() => props.visible,
|
||||||
async (val) => {
|
(val) => {
|
||||||
if (!val) return;
|
if (val) {
|
||||||
activeTab.value = "basic";
|
activeStep.value = 0;
|
||||||
isEdit.value = !!props.editData;
|
internalId.value = null;
|
||||||
internalId.value = props.editData?.id ?? null;
|
if (props.editData) {
|
||||||
if (props.editData) {
|
isEdit.value = true;
|
||||||
Object.assign(form, defaultForm(), props.editData);
|
internalId.value = props.editData.id;
|
||||||
} else {
|
Object.assign(form, defaultForm(), props.editData);
|
||||||
Object.assign(form, defaultForm());
|
} else {
|
||||||
|
isEdit.value = false;
|
||||||
|
Object.assign(form, defaultForm());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
await nextTick();
|
|
||||||
formRef.value?.clearValidate();
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -256,33 +359,109 @@ function handleClose() {
|
|||||||
|
|
||||||
function handleClosed() {
|
function handleClosed() {
|
||||||
formRef.value?.resetFields();
|
formRef.value?.resetFields();
|
||||||
activeTab.value = "basic";
|
activeStep.value = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSave() {
|
async function nextStep() {
|
||||||
if (!formRef.value) return;
|
const currentRules = stepRules[activeStep.value];
|
||||||
try {
|
if (Object.keys(currentRules).length > 0) {
|
||||||
await formRef.value.validate();
|
try {
|
||||||
} catch (e) {
|
await formRef.value.validateField(Object.keys(currentRules));
|
||||||
activeTab.value = "basic";
|
} catch (e) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if (activeStep.value < 3) {
|
||||||
|
activeStep.value++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function prevStep() {
|
||||||
|
if (activeStep.value > 0) {
|
||||||
|
activeStep.value--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSave(isDraft) {
|
||||||
|
if (!formRef.value) return;
|
||||||
|
if (isDraft) {
|
||||||
|
if (!form.customer_name || form.customer_name.trim() === "") {
|
||||||
|
ElMessage.warning("客户名称不能为空");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
await formRef.value.validate();
|
||||||
|
} catch (e) {
|
||||||
|
for (let i = 0; i < stepRules.length; i++) {
|
||||||
|
const fields = Object.keys(stepRules[i]);
|
||||||
|
for (const field of fields) {
|
||||||
|
if (!form[field] || form[field] === "") {
|
||||||
|
activeStep.value = i;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
submitting.value = true;
|
submitting.value = true;
|
||||||
try {
|
try {
|
||||||
const payload = { ...form, is_draft: 0 };
|
const payload = { ...form, is_draft: isDraft ? 1 : 0 };
|
||||||
if (internalId.value) {
|
if (internalId.value) {
|
||||||
await updateCrmCustomer(internalId.value, payload);
|
await updateCrmCustomer(internalId.value, payload);
|
||||||
ElMessage.success("更新成功");
|
ElMessage.success(isDraft ? "草稿已保存" : "更新成功");
|
||||||
} else {
|
} else {
|
||||||
await createCrmCustomer(payload);
|
const res = await createCrmCustomer(payload);
|
||||||
ElMessage.success("新增成功");
|
ElMessage.success(isDraft ? "草稿已保存" : "创建成功");
|
||||||
|
if (res && res.data && res.data.id) {
|
||||||
|
internalId.value = res.data.id;
|
||||||
|
isEdit.value = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
emit("success");
|
emit("success");
|
||||||
handleClose();
|
if (!isDraft) {
|
||||||
|
handleClose();
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ElMessage.error(e.message || "操作失败");
|
ElMessage.error(e.message || "操作失败");
|
||||||
} finally {
|
} finally {
|
||||||
submitting.value = false;
|
submitting.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleSaveDraft() {
|
||||||
|
handleSave(true);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.steps-wrapper {
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.footer-left,
|
||||||
|
.footer-right {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-step-tip {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 24px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: var(--el-color-info-light-9);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,520 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
:model-value="visible"
|
||||||
|
@update:model-value="emit('update:visible', $event)"
|
||||||
|
title="智能添加客户"
|
||||||
|
width="780px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
@closed="handleClosed"
|
||||||
|
>
|
||||||
|
<!-- 第一步:输入名称 -->
|
||||||
|
<div v-if="step === 1" class="step-input">
|
||||||
|
<el-form label-width="100px">
|
||||||
|
<el-form-item label="客户名称" required>
|
||||||
|
<el-input
|
||||||
|
v-model="companyName"
|
||||||
|
placeholder="请输入客户全称或简称"
|
||||||
|
@keyup.enter="handleGenerate"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-alert
|
||||||
|
title="AI将根据公司名称自动生成企业信息、联系方式、开票信息等,生成后可手动修改"
|
||||||
|
type="info"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
/>
|
||||||
|
</el-form>
|
||||||
|
<div class="step-actions">
|
||||||
|
<el-button type="primary" :icon="Cpu" :loading="generating" @click="handleGenerate">
|
||||||
|
智能生成
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 第二步:确认编辑 -->
|
||||||
|
<div v-else class="step-confirm">
|
||||||
|
<div class="confirm-header">
|
||||||
|
<el-alert
|
||||||
|
title="AI已生成以下信息,请检查确认,可直接修改错误内容,确认无误后点击保存"
|
||||||
|
type="success"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-form ref="formRef" :model="form" label-width="110px" class="smart-form">
|
||||||
|
<el-divider content-position="left">基本信息</el-divider>
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="客户名称">
|
||||||
|
<el-input v-model="form.customer_name" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="客户类型">
|
||||||
|
<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="客户等级">
|
||||||
|
<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="所属行业">
|
||||||
|
<el-input v-model="form.industry" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="企业类型">
|
||||||
|
<el-select v-model="form.enterprise_type" placeholder="请选择" style="width: 100%">
|
||||||
|
<el-option label="有限责任公司" value="有限责任公司" />
|
||||||
|
<el-option label="股份有限公司" value="股份有限公司" />
|
||||||
|
<el-option label="合伙企业" value="合伙企业" />
|
||||||
|
<el-option label="个人独资企业" value="个人独资企业" />
|
||||||
|
<el-option label="国有企业" value="国有企业" />
|
||||||
|
<el-option label="集体企业" value="集体企业" />
|
||||||
|
<el-option label="外商投资企业" value="外商投资企业" />
|
||||||
|
<el-option label="其他" value="其他" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="纳税人资质">
|
||||||
|
<el-select v-model="form.taxpayer_qualification" placeholder="请选择" style="width: 100%">
|
||||||
|
<el-option label="一般纳税人" value="一般纳税人" />
|
||||||
|
<el-option label="小规模纳税人" value="小规模纳税人" />
|
||||||
|
<el-option label="其他" value="其他" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="注册资本">
|
||||||
|
<el-input v-model="form.registered_capital" placeholder="万元">
|
||||||
|
<template #append>万元</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="实缴资本">
|
||||||
|
<el-input v-model="form.paid_capital" placeholder="万元">
|
||||||
|
<template #append>万元</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="成立日期">
|
||||||
|
<el-date-picker v-model="form.establish_date" type="date" value-format="YYYY-MM-DD" style="width: 100%" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="行政区划">
|
||||||
|
<el-input v-model="form.administrative_division" placeholder="如:江苏省连云港市海州区" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="状态">
|
||||||
|
<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-col :span="24">
|
||||||
|
<el-form-item label="公司地址">
|
||||||
|
<el-input v-model="form.address" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="经营范围">
|
||||||
|
<el-input v-model="form.business_scope" type="textarea" :rows="2" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-divider content-position="left">联系信息</el-divider>
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-form-item label="联系人">
|
||||||
|
<el-input v-model="form.contact_person" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-form-item label="联系电话">
|
||||||
|
<el-input v-model="form.contact_phone" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-form-item label="邮箱">
|
||||||
|
<el-input v-model="form.contact_email" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-divider content-position="left">开票信息</el-divider>
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="发票抬头">
|
||||||
|
<el-input v-model="form.invoice_title" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="税号">
|
||||||
|
<el-input v-model="form.tax_number" placeholder="18位统一社会信用代码" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="开户银行">
|
||||||
|
<el-input v-model="form.bank_name" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="银行账号">
|
||||||
|
<el-input v-model="form.bank_account" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="注册地址">
|
||||||
|
<el-input v-model="form.registered_address" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="注册电话">
|
||||||
|
<el-input v-model="form.registered_phone" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-divider content-position="left">备注</el-divider>
|
||||||
|
<el-form-item label="备注">
|
||||||
|
<el-input v-model="form.remark" type="textarea" :rows="2" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<!-- 显示原始AI响应数据 -->
|
||||||
|
<el-divider content-position="left">AI响应数据</el-divider>
|
||||||
|
<el-form-item label="查询信息" v-if="genMeta.model || (genMeta.tools && genMeta.tools.length)">
|
||||||
|
<div class="ai-meta">
|
||||||
|
<div class="ai-meta-row">
|
||||||
|
<span class="ai-meta-label">使用模型:</span>
|
||||||
|
<span>{{ genMeta.model || '—' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="ai-meta-row">
|
||||||
|
<span class="ai-meta-label">调用工具:</span>
|
||||||
|
<template v-if="genMeta.tools && genMeta.tools.length">
|
||||||
|
<el-tag
|
||||||
|
v-for="t in genMeta.tools"
|
||||||
|
:key="t.server_id + '-' + t.tool_name"
|
||||||
|
size="small"
|
||||||
|
type="info"
|
||||||
|
class="ai-tool-tag"
|
||||||
|
>{{ t.server_name }} / {{ t.tool_name }}</el-tag>
|
||||||
|
</template>
|
||||||
|
<span v-else>无</span>
|
||||||
|
</div>
|
||||||
|
<div class="ai-meta-row" v-if="genMeta.found !== undefined">
|
||||||
|
<span class="ai-meta-label">查到结果:</span>
|
||||||
|
<el-tag v-if="genMeta.found" size="small" type="success">已查到工商信息</el-tag>
|
||||||
|
<el-tag v-else size="small" type="warning">未查到(请手动补全)</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="详细数据">
|
||||||
|
<div class="ai-data-box">
|
||||||
|
<pre>{{ formattedAiData }}</pre>
|
||||||
|
</div>
|
||||||
|
<div class="form-tip">此数据为AI返回的原始响应,仅作查看参考,已自动填充可编辑字段到上方表单</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button v-if="step === 2" @click="step = 1">返回修改名称</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="step === 2"
|
||||||
|
type="primary"
|
||||||
|
:loading="saving"
|
||||||
|
@click="handleSave"
|
||||||
|
>确认保存</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, watch } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { Cpu } from '@element-plus/icons-vue'
|
||||||
|
import { smartGenerateCompany } from '@/api/ai'
|
||||||
|
import { createCrmCustomer } from '@/api/crm'
|
||||||
|
import {
|
||||||
|
CUSTOMER_TYPE_OPTIONS,
|
||||||
|
STATUS_OPTIONS,
|
||||||
|
levelOptions,
|
||||||
|
} from '../../dict'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
visible: { type: Boolean, default: false },
|
||||||
|
type: { type: String, default: 'customer' } // 当前仅支持 customer
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:visible', 'success'])
|
||||||
|
|
||||||
|
const step = ref(1)
|
||||||
|
const companyName = ref('')
|
||||||
|
const generating = ref(false)
|
||||||
|
const saving = ref(false)
|
||||||
|
const formRef = ref()
|
||||||
|
|
||||||
|
const defaultForm = () => ({
|
||||||
|
customer_name: '',
|
||||||
|
customer_type: '1',
|
||||||
|
customer_level: '3',
|
||||||
|
industry: '',
|
||||||
|
enterprise_type: '有限责任公司',
|
||||||
|
taxpayer_qualification: '一般纳税人',
|
||||||
|
status: '1',
|
||||||
|
registered_capital: '',
|
||||||
|
paid_capital: '',
|
||||||
|
establish_date: '',
|
||||||
|
administrative_division: '',
|
||||||
|
business_scope: '',
|
||||||
|
address: '',
|
||||||
|
contact_person: '',
|
||||||
|
contact_phone: '',
|
||||||
|
contact_email: '',
|
||||||
|
invoice_title: '',
|
||||||
|
tax_number: '',
|
||||||
|
bank_name: '',
|
||||||
|
bank_account: '',
|
||||||
|
registered_address: '',
|
||||||
|
registered_phone: '',
|
||||||
|
remark: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const form = reactive(defaultForm())
|
||||||
|
|
||||||
|
// 本次查询使用的模型与 MCP 工具(后端返回)
|
||||||
|
const genMeta = ref({ model: '', tools: [], found: undefined })
|
||||||
|
|
||||||
|
// 原始AI响应数据(JSON格式字符串)
|
||||||
|
const rawAiData = ref('')
|
||||||
|
|
||||||
|
// formattedAiData 直接显示 rawAiData 的内容
|
||||||
|
const formattedAiData = ref('')
|
||||||
|
watch(rawAiData, (newVal) => {
|
||||||
|
formattedAiData.value = newVal
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
async function handleGenerate() {
|
||||||
|
if (!companyName.value.trim()) {
|
||||||
|
ElMessage.warning('请输入公司名称')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
generating.value = true
|
||||||
|
try {
|
||||||
|
const res = await smartGenerateCompany({
|
||||||
|
company_name: companyName.value.trim(),
|
||||||
|
type: props.type
|
||||||
|
})
|
||||||
|
if (res.code === 200) {
|
||||||
|
// 记录本次查询使用的模型与 MCP 工具,便于前端透明展示
|
||||||
|
genMeta.value = {
|
||||||
|
model: res.data.model || '',
|
||||||
|
tools: res.data.tools || [],
|
||||||
|
found: res.data.found
|
||||||
|
}
|
||||||
|
if (res.data.parsed && res.data.data) {
|
||||||
|
const data = res.data.data
|
||||||
|
form.customer_name = data.customer_name || data.name || companyName.value.trim()
|
||||||
|
form.customer_type = data.customer_type || '1'
|
||||||
|
form.customer_level = data.customer_level || '3'
|
||||||
|
form.status = data.status || '1'
|
||||||
|
form.industry = data.industry || ''
|
||||||
|
form.enterprise_type = data.enterprise_type || ''
|
||||||
|
form.taxpayer_qualification = data.taxpayer_qualification || ''
|
||||||
|
form.registered_capital = data.registered_capital || ''
|
||||||
|
form.paid_capital = data.paid_capital || ''
|
||||||
|
form.establish_date = data.establish_date || ''
|
||||||
|
form.administrative_division = data.administrative_division || ''
|
||||||
|
form.business_scope = data.business_scope || ''
|
||||||
|
form.address = data.address || ''
|
||||||
|
form.contact_person = data.contact_person || ''
|
||||||
|
form.contact_phone = data.contact_phone || ''
|
||||||
|
form.contact_email = data.contact_email || ''
|
||||||
|
form.invoice_title = data.invoice_title || form.customer_name
|
||||||
|
form.tax_number = data.tax_number || ''
|
||||||
|
form.bank_name = data.bank_name || ''
|
||||||
|
form.bank_account = data.bank_account || ''
|
||||||
|
form.registered_address = data.registered_address || ''
|
||||||
|
form.registered_phone = data.registered_phone || ''
|
||||||
|
form.remark = data.remark || ''
|
||||||
|
// 保存原始AI响应数据(使用后端返回的原始数据)
|
||||||
|
if (res.data.raw) {
|
||||||
|
rawAiData.value = res.data.raw
|
||||||
|
} else {
|
||||||
|
rawAiData.value = JSON.stringify(data, null, 2)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 解析失败,显示AI原始返回内容
|
||||||
|
let rawContent = res.data.raw || '无原始数据'
|
||||||
|
let errorMsg = res.data.error || '未知错误'
|
||||||
|
rawAiData.value = rawContent
|
||||||
|
if (rawContent.length > 1500) {
|
||||||
|
rawContent = rawContent.substring(0, 1500) + '...'
|
||||||
|
}
|
||||||
|
ElMessage.error({
|
||||||
|
message: `AI生成失败:${errorMsg}\n\n原始响应:\n${rawContent}`,
|
||||||
|
showClose: true,
|
||||||
|
duration: 0
|
||||||
|
})
|
||||||
|
form.customer_name = companyName.value.trim()
|
||||||
|
}
|
||||||
|
step.value = 2
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error(e.message || '生成失败')
|
||||||
|
} finally {
|
||||||
|
generating.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSave() {
|
||||||
|
if (!form.customer_name.trim()) {
|
||||||
|
ElMessage.warning('客户名称不能为空')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
customer_name: form.customer_name,
|
||||||
|
customer_type: form.customer_type,
|
||||||
|
customer_level: form.customer_level,
|
||||||
|
status: form.status,
|
||||||
|
industry: form.industry,
|
||||||
|
enterprise_type: form.enterprise_type,
|
||||||
|
taxpayer_qualification: form.taxpayer_qualification,
|
||||||
|
registered_capital: form.registered_capital,
|
||||||
|
paid_capital: form.paid_capital,
|
||||||
|
establish_date: form.establish_date,
|
||||||
|
administrative_division: form.administrative_division,
|
||||||
|
business_scope: form.business_scope,
|
||||||
|
address: form.address,
|
||||||
|
contact_person: form.contact_person,
|
||||||
|
contact_phone: form.contact_phone,
|
||||||
|
contact_email: form.contact_email,
|
||||||
|
invoice_title: form.invoice_title,
|
||||||
|
tax_number: form.tax_number,
|
||||||
|
bank_name: form.bank_name,
|
||||||
|
bank_account: form.bank_account,
|
||||||
|
registered_address: form.registered_address,
|
||||||
|
registered_phone: form.registered_phone,
|
||||||
|
remark: form.remark,
|
||||||
|
is_draft: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
await createCrmCustomer(payload)
|
||||||
|
|
||||||
|
ElMessage.success('保存成功')
|
||||||
|
emit('success')
|
||||||
|
emit('update:visible', false)
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error(e.message || '保存失败')
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClosed() {
|
||||||
|
step.value = 1
|
||||||
|
companyName.value = ''
|
||||||
|
Object.assign(form, defaultForm())
|
||||||
|
rawAiData.value = ''
|
||||||
|
formattedAiData.value = ''
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.step-input {
|
||||||
|
padding: 20px 0;
|
||||||
|
|
||||||
|
.step-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-confirm {
|
||||||
|
.confirm-header {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.smart-form {
|
||||||
|
max-height: 60vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-data-box {
|
||||||
|
background: var(--el-fill-color-lighter);
|
||||||
|
border: 1px solid var(--el-border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 12px;
|
||||||
|
font-family: 'Consolas', 'Monaco', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-meta {
|
||||||
|
background: var(--el-fill-color-lighter);
|
||||||
|
border: 1px solid var(--el-border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
|
||||||
|
.ai-meta-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-meta-label {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-tool-tag {
|
||||||
|
margin-right: 6px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
移入公海
|
移入公海
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button type="primary" :icon="Plus" @click="openCreate">新增客户</el-button>
|
<el-button type="primary" :icon="Plus" @click="openCreate">新增客户</el-button>
|
||||||
|
<el-button type="success" :icon="Cpu" @click="smartAddVisible = true">智能添加</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -147,7 +148,7 @@
|
|||||||
v-model="detailVisible"
|
v-model="detailVisible"
|
||||||
:title="detailData?.customer_name || '客户详情'"
|
:title="detailData?.customer_name || '客户详情'"
|
||||||
direction="rtl"
|
direction="rtl"
|
||||||
size="560px"
|
size="700px"
|
||||||
>
|
>
|
||||||
<div v-if="detailData" class="detail-content">
|
<div v-if="detailData" class="detail-content">
|
||||||
<el-descriptions title="基本信息" :column="2" border size="small">
|
<el-descriptions title="基本信息" :column="2" border size="small">
|
||||||
@@ -167,9 +168,14 @@
|
|||||||
</el-tag>
|
</el-tag>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="负责人">{{ detailData.owner_name || '未分配' }}</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="注册资本">{{ detailData.registered_capital ? detailData.registered_capital + ' 万元' : '-' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="合作到期">{{ formatDate(detailData.expire_time) }}</el-descriptions-item>
|
<el-descriptions-item label="实缴资本">{{ detailData.paid_capital ? detailData.paid_capital + ' 万元' : '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="成立日期">{{ formatDate(detailData.establish_date) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="行政区划">{{ detailData.administrative_division || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="企业类型">{{ detailData.enterprise_type || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="纳税人资质">{{ detailData.taxpayer_qualification || '-' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="客户地址" :span="2">{{ detailData.address || '-' }}</el-descriptions-item>
|
<el-descriptions-item label="客户地址" :span="2">{{ detailData.address || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="经营范围" :span="2">{{ detailData.business_scope || '-' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="创建时间">{{ formatDateTime(detailData.create_time) }}</el-descriptions-item>
|
<el-descriptions-item label="创建时间">{{ formatDateTime(detailData.create_time) }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="更新时间">{{ formatDateTime(detailData.update_time) }}</el-descriptions-item>
|
<el-descriptions-item label="更新时间">{{ formatDateTime(detailData.update_time) }}</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
@@ -250,6 +256,8 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="mobile" label="手机号" width="130" />
|
<el-table-column prop="mobile" label="手机号" width="130" />
|
||||||
<el-table-column prop="email" label="邮箱" min-width="180" show-overflow-tooltip />
|
<el-table-column prop="email" label="邮箱" min-width="180" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="wechat" label="微信" width="120" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="qq" label="QQ" width="110" show-overflow-tooltip />
|
||||||
<el-table-column prop="department" label="部门" width="120" 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 prop="position" label="职位" width="130" show-overflow-tooltip />
|
||||||
<el-table-column label="操作" width="120" align="center" fixed="right">
|
<el-table-column label="操作" width="120" align="center" fixed="right">
|
||||||
@@ -274,6 +282,12 @@
|
|||||||
@success="loadContacts"
|
@success="loadContacts"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<SmartAddCompany
|
||||||
|
v-model:visible="smartAddVisible"
|
||||||
|
type="customer"
|
||||||
|
@success="fetchList"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- 移入公海 -->
|
<!-- 移入公海 -->
|
||||||
<el-dialog v-model="poolVisible" title="移入客户公海" width="460px" :close-on-click-modal="false">
|
<el-dialog v-model="poolVisible" title="移入客户公海" width="460px" :close-on-click-modal="false">
|
||||||
<el-alert
|
<el-alert
|
||||||
@@ -304,7 +318,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, computed, onMounted } from "vue";
|
import { ref, reactive, computed, onMounted } from "vue";
|
||||||
import { ElMessage, ElMessageBox } from "element-plus";
|
import { ElMessage, ElMessageBox } from "element-plus";
|
||||||
import { Plus, Search, Refresh } from "@element-plus/icons-vue";
|
import { Plus, Search, Refresh, Cpu } from "@element-plus/icons-vue";
|
||||||
import {
|
import {
|
||||||
getCrmCustomerList,
|
getCrmCustomerList,
|
||||||
deleteCrmCustomer,
|
deleteCrmCustomer,
|
||||||
@@ -313,6 +327,7 @@ import {
|
|||||||
import { listContacts, deleteContact } from "@/api/contact";
|
import { listContacts, deleteContact } from "@/api/contact";
|
||||||
import CustomerEdit from "./components/edit.vue";
|
import CustomerEdit from "./components/edit.vue";
|
||||||
import ContactEdit from "../contact/components/edit.vue";
|
import ContactEdit from "../contact/components/edit.vue";
|
||||||
|
import SmartAddCompany from "./components/smartAddCompany.vue";
|
||||||
import {
|
import {
|
||||||
CUSTOMER_TYPE_OPTIONS,
|
CUSTOMER_TYPE_OPTIONS,
|
||||||
STATUS_OPTIONS,
|
STATUS_OPTIONS,
|
||||||
@@ -343,6 +358,7 @@ const contactList = ref([]);
|
|||||||
const currentRow = ref(null);
|
const currentRow = ref(null);
|
||||||
const contactEditVisible = ref(false);
|
const contactEditVisible = ref(false);
|
||||||
const contactEditData = ref(null);
|
const contactEditData = ref(null);
|
||||||
|
const smartAddVisible = ref(false);
|
||||||
|
|
||||||
// 移入公海
|
// 移入公海
|
||||||
const poolVisible = ref(false);
|
const poolVisible = ref(false);
|
||||||
@@ -601,4 +617,20 @@ async function submitMovePool() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.detail-content {
|
||||||
|
padding: 4px 8px;
|
||||||
|
|
||||||
|
:deep(.el-descriptions__label) {
|
||||||
|
width: 130px;
|
||||||
|
min-width: 130px;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
font-weight: 500;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-descriptions__content) {
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -128,7 +128,6 @@
|
|||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button link type="primary" size="small" @click="handleClaim([row])">领取</el-button>
|
<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="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>
|
<el-button link type="danger" size="small" @click="handleDelete(row)">删除</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -233,7 +232,6 @@ import {
|
|||||||
getPoolList,
|
getPoolList,
|
||||||
claimPoolCustomer,
|
claimPoolCustomer,
|
||||||
assignPoolCustomer,
|
assignPoolCustomer,
|
||||||
removeFromPool,
|
|
||||||
deleteCrmCustomer,
|
deleteCrmCustomer,
|
||||||
} from "@/api/crm";
|
} from "@/api/crm";
|
||||||
import { getAllUsers } from "@/api/user";
|
import { getAllUsers } from "@/api/user";
|
||||||
@@ -391,9 +389,11 @@ async function submitAssign() {
|
|||||||
}
|
}
|
||||||
assignSubmitting.value = true;
|
assignSubmitting.value = true;
|
||||||
try {
|
try {
|
||||||
|
const user = userOptions.value.find((u) => u.id === assignUserId.value);
|
||||||
await assignPoolCustomer({
|
await assignPoolCustomer({
|
||||||
ids: assignTargets.value.map((item) => item.id),
|
ids: assignTargets.value.map((item) => item.id),
|
||||||
owner_user_id: assignUserId.value,
|
owner_user_id: assignUserId.value,
|
||||||
|
owner_user_name: user?.name || "",
|
||||||
});
|
});
|
||||||
ElMessage.success("分配成功");
|
ElMessage.success("分配成功");
|
||||||
assignVisible.value = false;
|
assignVisible.value = false;
|
||||||
@@ -406,26 +406,7 @@ async function submitAssign() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------------------- 移出 / 删除 ---------------------------- */
|
/* -------------------------------- 删除 -------------------------------- */
|
||||||
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) {
|
async function handleDelete(row) {
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm(
|
await ElMessageBox.confirm(
|
||||||
|
|||||||
@@ -1,182 +1,252 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog
|
<el-dialog
|
||||||
:model-value="visible"
|
:model-value="visible"
|
||||||
:title="isEdit ? '编辑供应商' : '新增供应商'"
|
:title="dialogTitle"
|
||||||
width="760px"
|
width="680px"
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
@update:model-value="handleClose"
|
@update:model-value="handleClose"
|
||||||
@closed="handleClosed"
|
@closed="handleClosed"
|
||||||
>
|
>
|
||||||
<el-tabs v-model="activeTab">
|
<!-- 步骤条 -->
|
||||||
<el-tab-pane label="基本信息" name="basic">
|
<div class="steps-wrapper">
|
||||||
<el-form
|
<el-steps :active="activeStep" finish-status="success" align-center>
|
||||||
ref="formRef"
|
<el-step title="基本信息" />
|
||||||
:model="form"
|
<el-step title="联系信息" />
|
||||||
:rules="rules"
|
<el-step title="开票信息" />
|
||||||
label-width="110px"
|
<el-step title="备注信息" />
|
||||||
label-position="right"
|
</el-steps>
|
||||||
>
|
</div>
|
||||||
<el-row :gutter="16">
|
|
||||||
<el-col :span="12">
|
<el-form
|
||||||
<el-form-item label="供应商名称" prop="supplier_name">
|
ref="formRef"
|
||||||
<el-input v-model="form.supplier_name" placeholder="请输入供应商名称" />
|
:model="form"
|
||||||
</el-form-item>
|
:rules="allRules"
|
||||||
</el-col>
|
label-width="100px"
|
||||||
<el-col :span="12">
|
label-position="right"
|
||||||
<el-form-item label="供应商类型" prop="supplier_type">
|
style="margin-top: 24px"
|
||||||
<el-select v-model="form.supplier_type" placeholder="请选择" style="width: 100%">
|
>
|
||||||
<el-option
|
<!-- 步骤1:基本信息 -->
|
||||||
v-for="item in SUPPLIER_TYPE_OPTIONS"
|
<div v-show="activeStep === 0">
|
||||||
:key="item.value"
|
<el-row :gutter="20">
|
||||||
:label="item.label"
|
<el-col :span="12">
|
||||||
:value="item.value"
|
<el-form-item label="供应商名称" prop="supplier_name">
|
||||||
/>
|
<el-input v-model="form.supplier_name" placeholder="请输入供应商名称" />
|
||||||
</el-select>
|
</el-form-item>
|
||||||
</el-form-item>
|
</el-col>
|
||||||
</el-col>
|
<el-col :span="12">
|
||||||
<el-col :span="12">
|
<el-form-item label="供应商类型" prop="supplier_type">
|
||||||
<el-form-item label="供应商等级" prop="supplier_level">
|
<el-select v-model="form.supplier_type" placeholder="请选择" style="width: 100%">
|
||||||
<el-select v-model="form.supplier_level" placeholder="请选择" style="width: 100%">
|
<el-option
|
||||||
<el-option
|
v-for="item in SUPPLIER_TYPE_OPTIONS"
|
||||||
v-for="item in levelOptions('supplier')"
|
:key="item.value"
|
||||||
:key="item.value"
|
:label="item.label"
|
||||||
:label="item.label"
|
:value="item.value"
|
||||||
: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-select>
|
||||||
</el-col>
|
</el-form-item>
|
||||||
<el-col :span="12">
|
</el-col>
|
||||||
<el-form-item label="合作到期" prop="expire_time">
|
<el-col :span="12">
|
||||||
<el-date-picker
|
<el-form-item label="供应商等级" prop="supplier_level">
|
||||||
v-model="form.expire_time"
|
<el-select v-model="form.supplier_level" placeholder="请选择" style="width: 100%">
|
||||||
type="date"
|
<el-option
|
||||||
value-format="YYYY-MM-DD"
|
v-for="item in levelOptions('supplier')"
|
||||||
placeholder="选择日期"
|
:key="item.value"
|
||||||
style="width: 100%"
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-select>
|
||||||
</el-col>
|
</el-form-item>
|
||||||
<el-col :span="24">
|
</el-col>
|
||||||
<el-form-item label="供应商地址" prop="address">
|
<el-col :span="12">
|
||||||
<el-input v-model="form.address" placeholder="请输入详细地址" />
|
<el-form-item label="所属行业" prop="industry">
|
||||||
</el-form-item>
|
<el-input v-model="form.industry" placeholder="如:机械设备、电子元器件" />
|
||||||
</el-col>
|
</el-form-item>
|
||||||
<el-col :span="12">
|
</el-col>
|
||||||
<el-form-item label="状态" prop="status">
|
<el-col :span="12">
|
||||||
<el-select v-model="form.status" placeholder="请选择" style="width: 100%">
|
<el-form-item label="注册资本" prop="registered_capital">
|
||||||
<el-option
|
<el-input v-model="form.registered_capital" placeholder="万元">
|
||||||
v-for="item in STATUS_OPTIONS"
|
<template #append>万元</template>
|
||||||
:key="item.value"
|
</el-input>
|
||||||
:label="item.label"
|
</el-form-item>
|
||||||
:value="item.value"
|
</el-col>
|
||||||
/>
|
<el-col :span="12">
|
||||||
</el-select>
|
<el-form-item label="实缴资本" prop="paid_capital">
|
||||||
</el-form-item>
|
<el-input v-model="form.paid_capital" placeholder="万元">
|
||||||
</el-col>
|
<template #append>万元</template>
|
||||||
</el-row>
|
</el-input>
|
||||||
</el-form>
|
</el-form-item>
|
||||||
</el-tab-pane>
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="成立日期" prop="establish_date">
|
||||||
|
<el-date-picker v-model="form.establish_date" 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="administrative_division">
|
||||||
|
<el-input v-model="form.administrative_division" placeholder="省/市/区" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="企业类型" prop="enterprise_type">
|
||||||
|
<el-select v-model="form.enterprise_type" placeholder="请选择" style="width: 100%">
|
||||||
|
<el-option label="有限责任公司" value="有限责任公司" />
|
||||||
|
<el-option label="股份有限公司" value="股份有限公司" />
|
||||||
|
<el-option label="合伙企业" value="合伙企业" />
|
||||||
|
<el-option label="个人独资企业" value="个人独资企业" />
|
||||||
|
<el-option label="国有企业" value="国有企业" />
|
||||||
|
<el-option label="集体企业" value="集体企业" />
|
||||||
|
<el-option label="外商投资企业" value="外商投资企业" />
|
||||||
|
<el-option label="其他" value="其他" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="纳税人资质" prop="taxpayer_qualification">
|
||||||
|
<el-select v-model="form.taxpayer_qualification" placeholder="请选择" style="width: 100%">
|
||||||
|
<el-option label="一般纳税人" value="一般纳税人" />
|
||||||
|
<el-option label="小规模纳税人" value="小规模纳税人" />
|
||||||
|
<el-option label="其他" value="其他" />
|
||||||
|
</el-select>
|
||||||
|
</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-col :span="24">
|
||||||
|
<el-form-item label="经营范围" prop="business_scope">
|
||||||
|
<el-input v-model="form.business_scope" type="textarea" :rows="3" placeholder="请输入经营范围(选填)" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
|
||||||
<el-tab-pane label="联系信息" name="contact">
|
<!-- 步骤2:联系信息 -->
|
||||||
<el-form :model="form" label-width="110px" label-position="right">
|
<div v-show="activeStep === 1">
|
||||||
<el-row :gutter="16">
|
<el-row :gutter="20">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="联系人">
|
<el-form-item label="联系人" prop="contact_person">
|
||||||
<el-input v-model="form.contact_person" placeholder="请输入联系人姓名" />
|
<el-input v-model="form.contact_person" placeholder="请输入联系人姓名" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="联系电话">
|
<el-form-item label="联系电话" prop="contact_phone">
|
||||||
<el-input v-model="form.contact_phone" placeholder="请输入联系电话" />
|
<el-input v-model="form.contact_phone" placeholder="请输入联系电话" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="联系邮箱">
|
<el-form-item label="联系邮箱" prop="contact_email">
|
||||||
<el-input v-model="form.contact_email" placeholder="请输入邮箱" />
|
<el-input v-model="form.contact_email" placeholder="请输入邮箱" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
<el-col :span="24">
|
||||||
</el-form>
|
<el-form-item label="供应商地址" prop="address">
|
||||||
</el-tab-pane>
|
<el-input v-model="form.address" placeholder="请输入详细地址" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
|
||||||
<el-tab-pane label="开票信息" name="invoice">
|
<!-- 步骤3:开票信息(发票+银行合并) -->
|
||||||
<el-form :model="form" label-width="110px" label-position="right">
|
<div v-show="activeStep === 2">
|
||||||
<el-row :gutter="16">
|
<el-divider content-position="left">发票信息</el-divider>
|
||||||
<el-col :span="12">
|
<el-row :gutter="20">
|
||||||
<el-form-item label="发票抬头">
|
<el-col :span="12">
|
||||||
<el-input v-model="form.invoice_title" placeholder="请输入发票抬头" />
|
<el-form-item label="发票抬头" prop="invoice_title">
|
||||||
</el-form-item>
|
<el-input v-model="form.invoice_title" placeholder="请输入发票抬头" />
|
||||||
</el-col>
|
</el-form-item>
|
||||||
<el-col :span="12">
|
</el-col>
|
||||||
<el-form-item label="纳税人识别号">
|
<el-col :span="12">
|
||||||
<el-input v-model="form.tax_number" placeholder="请输入纳税人识别号" />
|
<el-form-item label="纳税人识别号" prop="tax_number">
|
||||||
</el-form-item>
|
<el-input v-model="form.tax_number" placeholder="请输入纳税人识别号" />
|
||||||
</el-col>
|
</el-form-item>
|
||||||
<el-col :span="24">
|
</el-col>
|
||||||
<el-form-item label="注册地址">
|
<el-col :span="24">
|
||||||
<el-input v-model="form.registered_address" placeholder="请输入注册地址" />
|
<el-form-item label="注册地址" prop="registered_address">
|
||||||
</el-form-item>
|
<el-input v-model="form.registered_address" placeholder="请输入注册地址" />
|
||||||
</el-col>
|
</el-form-item>
|
||||||
<el-col :span="12">
|
</el-col>
|
||||||
<el-form-item label="注册电话">
|
<el-col :span="12">
|
||||||
<el-input v-model="form.registered_phone" placeholder="请输入注册电话" />
|
<el-form-item label="注册电话" prop="registered_phone">
|
||||||
</el-form-item>
|
<el-input v-model="form.registered_phone" placeholder="请输入注册电话" />
|
||||||
</el-col>
|
</el-form-item>
|
||||||
<el-col :span="12">
|
</el-col>
|
||||||
<el-form-item label="开户行名称">
|
</el-row>
|
||||||
<el-input v-model="form.bank_name" placeholder="请输入开户行名称" />
|
<el-divider content-position="left">银行信息</el-divider>
|
||||||
</el-form-item>
|
<el-row :gutter="20">
|
||||||
</el-col>
|
<el-col :span="12">
|
||||||
<el-col :span="12">
|
<el-form-item label="开户行名称" prop="bank_name">
|
||||||
<el-form-item label="开户行账号">
|
<el-input v-model="form.bank_name" placeholder="请输入开户行名称" />
|
||||||
<el-input v-model="form.bank_account" placeholder="请输入银行账号" />
|
</el-form-item>
|
||||||
</el-form-item>
|
</el-col>
|
||||||
</el-col>
|
<el-col :span="12">
|
||||||
</el-row>
|
<el-form-item label="开户行账号" prop="bank_account">
|
||||||
</el-form>
|
<el-input v-model="form.bank_account" placeholder="请输入开户行账号" />
|
||||||
</el-tab-pane>
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
|
||||||
<el-tab-pane label="备注" name="remark">
|
<!-- 步骤4:备注信息 -->
|
||||||
<el-form :model="form" label-width="110px" label-position="right">
|
<div v-show="activeStep === 3">
|
||||||
<el-form-item label="备注">
|
<el-form-item label="备注" prop="remark">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="form.remark"
|
v-model="form.remark"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
:rows="8"
|
:rows="10"
|
||||||
placeholder="请输入备注信息(选填)"
|
placeholder="请输入备注信息(选填)"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
<div class="final-step-tip">
|
||||||
</el-tab-pane>
|
<el-icon><InfoFilled /></el-icon>
|
||||||
</el-tabs>
|
<span>确认所有信息无误后,点击「保存」完成创建。</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="handleClose">取消</el-button>
|
<div class="dialog-footer">
|
||||||
<el-button type="primary" :loading="submitting" @click="handleSave">保存</el-button>
|
<div class="footer-left">
|
||||||
|
<el-button v-if="activeStep > 0" @click="prevStep">上一步</el-button>
|
||||||
|
<el-button v-if="activeStep < 3" type="primary" @click="nextStep"
|
||||||
|
>下一步</el-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="footer-right">
|
||||||
|
<el-button @click="handleClose">取消</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="!isEdit || form.is_draft === 1"
|
||||||
|
:loading="submitting"
|
||||||
|
@click="handleSaveDraft"
|
||||||
|
>
|
||||||
|
保存草稿
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="activeStep === 3"
|
||||||
|
type="primary"
|
||||||
|
:loading="submitting"
|
||||||
|
@click="handleSave(false)"
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, watch, nextTick } from "vue";
|
import { ref, reactive, computed, watch } from "vue";
|
||||||
import { ElMessage } from "element-plus";
|
import { ElMessage } from "element-plus";
|
||||||
|
import { InfoFilled } from "@element-plus/icons-vue";
|
||||||
import { createCrmSupplier, updateCrmSupplier } from "@/api/crm";
|
import { createCrmSupplier, updateCrmSupplier } from "@/api/crm";
|
||||||
import { SUPPLIER_TYPE_OPTIONS, STATUS_OPTIONS, levelOptions } from "../../dict";
|
import { SUPPLIER_TYPE_OPTIONS, STATUS_OPTIONS, levelOptions } from "../../dict";
|
||||||
|
|
||||||
@@ -188,23 +258,36 @@ const props = defineProps({
|
|||||||
const emit = defineEmits(["update:visible", "success"]);
|
const emit = defineEmits(["update:visible", "success"]);
|
||||||
|
|
||||||
const formRef = ref();
|
const formRef = ref();
|
||||||
const activeTab = ref("basic");
|
const activeStep = ref(0);
|
||||||
const submitting = ref(false);
|
const submitting = ref(false);
|
||||||
const isEdit = ref(false);
|
const isEdit = ref(false);
|
||||||
const internalId = ref(null);
|
const internalId = ref(null);
|
||||||
|
|
||||||
|
const dialogTitle = computed(() => {
|
||||||
|
if (isEdit.value) {
|
||||||
|
return form.is_draft === 1 ? "编辑供应商(草稿)" : "编辑供应商";
|
||||||
|
}
|
||||||
|
return "新增供应商";
|
||||||
|
});
|
||||||
|
|
||||||
const defaultForm = () => ({
|
const defaultForm = () => ({
|
||||||
supplier_name: "",
|
supplier_name: "",
|
||||||
supplier_type: "1",
|
supplier_type: "1",
|
||||||
supplier_level: "3",
|
supplier_level: "3",
|
||||||
industry: "",
|
industry: "",
|
||||||
|
registered_capital: "",
|
||||||
|
paid_capital: "",
|
||||||
|
establish_date: "",
|
||||||
|
administrative_division: "",
|
||||||
|
enterprise_type: "有限责任公司",
|
||||||
|
taxpayer_qualification: "一般纳税人",
|
||||||
|
business_scope: "",
|
||||||
|
address: "",
|
||||||
contact_person: "",
|
contact_person: "",
|
||||||
contact_phone: "",
|
contact_phone: "",
|
||||||
contact_email: "",
|
contact_email: "",
|
||||||
address: "",
|
|
||||||
register_time: "",
|
|
||||||
expire_time: "",
|
|
||||||
status: "1",
|
status: "1",
|
||||||
|
is_draft: 0,
|
||||||
remark: "",
|
remark: "",
|
||||||
invoice_title: "",
|
invoice_title: "",
|
||||||
tax_number: "",
|
tax_number: "",
|
||||||
@@ -216,33 +299,57 @@ const defaultForm = () => ({
|
|||||||
|
|
||||||
const form = reactive(defaultForm());
|
const form = reactive(defaultForm());
|
||||||
|
|
||||||
const rules = {
|
// 各步骤的验证规则
|
||||||
supplier_name: [{ required: true, message: "请输入供应商名称", trigger: "blur" }],
|
const stepRules = [
|
||||||
supplier_type: [{ required: true, message: "请选择供应商类型", trigger: "change" }],
|
// 步骤1:基本信息
|
||||||
contact_phone: [
|
{
|
||||||
{
|
supplier_name: [
|
||||||
pattern: /^(1[3-9]\d{9}|0\d{2,3}-?\d{7,8}|\d{7,8})$/,
|
{ required: true, message: "请输入供应商名称", trigger: "blur" },
|
||||||
message: "联系电话格式不正确",
|
],
|
||||||
trigger: "blur",
|
supplier_type: [
|
||||||
},
|
{ required: true, message: "请选择供应商类型", trigger: "change" },
|
||||||
],
|
],
|
||||||
contact_email: [{ type: "email", message: "邮箱格式不正确", trigger: "blur" }],
|
},
|
||||||
};
|
// 步骤2:联系信息
|
||||||
|
{
|
||||||
|
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" }],
|
||||||
|
},
|
||||||
|
// 步骤3:开票信息(无必填)
|
||||||
|
{},
|
||||||
|
// 步骤4:备注信息(无必填)
|
||||||
|
{},
|
||||||
|
];
|
||||||
|
|
||||||
|
const allRules = computed(() => {
|
||||||
|
const merged = {};
|
||||||
|
stepRules.forEach((rules) => {
|
||||||
|
Object.assign(merged, rules);
|
||||||
|
});
|
||||||
|
return merged;
|
||||||
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.visible,
|
() => props.visible,
|
||||||
async (val) => {
|
(val) => {
|
||||||
if (!val) return;
|
if (val) {
|
||||||
activeTab.value = "basic";
|
activeStep.value = 0;
|
||||||
isEdit.value = !!props.editData;
|
internalId.value = null;
|
||||||
internalId.value = props.editData?.id ?? null;
|
if (props.editData) {
|
||||||
if (props.editData) {
|
isEdit.value = true;
|
||||||
Object.assign(form, defaultForm(), props.editData);
|
internalId.value = props.editData.id;
|
||||||
} else {
|
Object.assign(form, defaultForm(), props.editData);
|
||||||
Object.assign(form, defaultForm());
|
} else {
|
||||||
|
isEdit.value = false;
|
||||||
|
Object.assign(form, defaultForm());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
await nextTick();
|
|
||||||
formRef.value?.clearValidate();
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -252,33 +359,109 @@ function handleClose() {
|
|||||||
|
|
||||||
function handleClosed() {
|
function handleClosed() {
|
||||||
formRef.value?.resetFields();
|
formRef.value?.resetFields();
|
||||||
activeTab.value = "basic";
|
activeStep.value = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSave() {
|
async function nextStep() {
|
||||||
if (!formRef.value) return;
|
const currentRules = stepRules[activeStep.value];
|
||||||
try {
|
if (Object.keys(currentRules).length > 0) {
|
||||||
await formRef.value.validate();
|
try {
|
||||||
} catch (e) {
|
await formRef.value.validateField(Object.keys(currentRules));
|
||||||
activeTab.value = "basic";
|
} catch (e) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if (activeStep.value < 3) {
|
||||||
|
activeStep.value++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function prevStep() {
|
||||||
|
if (activeStep.value > 0) {
|
||||||
|
activeStep.value--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSave(isDraft) {
|
||||||
|
if (!formRef.value) return;
|
||||||
|
if (isDraft) {
|
||||||
|
if (!form.supplier_name || form.supplier_name.trim() === "") {
|
||||||
|
ElMessage.warning("供应商名称不能为空");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
await formRef.value.validate();
|
||||||
|
} catch (e) {
|
||||||
|
for (let i = 0; i < stepRules.length; i++) {
|
||||||
|
const fields = Object.keys(stepRules[i]);
|
||||||
|
for (const field of fields) {
|
||||||
|
if (!form[field] || form[field] === "") {
|
||||||
|
activeStep.value = i;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
submitting.value = true;
|
submitting.value = true;
|
||||||
try {
|
try {
|
||||||
const payload = { ...form, is_draft: 0 };
|
const payload = { ...form, is_draft: isDraft ? 1 : 0 };
|
||||||
if (internalId.value) {
|
if (internalId.value) {
|
||||||
await updateCrmSupplier(internalId.value, payload);
|
await updateCrmSupplier(internalId.value, payload);
|
||||||
ElMessage.success("更新成功");
|
ElMessage.success(isDraft ? "草稿已保存" : "更新成功");
|
||||||
} else {
|
} else {
|
||||||
await createCrmSupplier(payload);
|
const res = await createCrmSupplier(payload);
|
||||||
ElMessage.success("新增成功");
|
ElMessage.success(isDraft ? "草稿已保存" : "创建成功");
|
||||||
|
if (res && res.data && res.data.id) {
|
||||||
|
internalId.value = res.data.id;
|
||||||
|
isEdit.value = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
emit("success");
|
emit("success");
|
||||||
handleClose();
|
if (!isDraft) {
|
||||||
|
handleClose();
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ElMessage.error(e.message || "操作失败");
|
ElMessage.error(e.message || "操作失败");
|
||||||
} finally {
|
} finally {
|
||||||
submitting.value = false;
|
submitting.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleSaveDraft() {
|
||||||
|
handleSave(true);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.steps-wrapper {
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.footer-left,
|
||||||
|
.footer-right {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-step-tip {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 24px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: var(--el-color-info-light-9);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,520 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
:model-value="visible"
|
||||||
|
@update:model-value="emit('update:visible', $event)"
|
||||||
|
title="智能添加供应商"
|
||||||
|
width="780px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
@closed="handleClosed"
|
||||||
|
>
|
||||||
|
<!-- 第一步:输入名称 -->
|
||||||
|
<div v-if="step === 1" class="step-input">
|
||||||
|
<el-form label-width="100px">
|
||||||
|
<el-form-item label="供应商名称" required>
|
||||||
|
<el-input
|
||||||
|
v-model="companyName"
|
||||||
|
placeholder="请输入供应商全称或简称"
|
||||||
|
@keyup.enter="handleGenerate"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-alert
|
||||||
|
title="AI将根据公司名称自动生成企业信息、联系方式、开票信息等,生成后可手动修改"
|
||||||
|
type="info"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
/>
|
||||||
|
</el-form>
|
||||||
|
<div class="step-actions">
|
||||||
|
<el-button type="primary" :icon="Cpu" :loading="generating" @click="handleGenerate">
|
||||||
|
智能生成
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 第二步:确认编辑 -->
|
||||||
|
<div v-else class="step-confirm">
|
||||||
|
<div class="confirm-header">
|
||||||
|
<el-alert
|
||||||
|
title="AI已生成以下信息,请检查确认,可直接修改错误内容,确认无误后点击保存"
|
||||||
|
type="success"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-form ref="formRef" :model="form" label-width="110px" class="smart-form">
|
||||||
|
<el-divider content-position="left">基本信息</el-divider>
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="供应商名称">
|
||||||
|
<el-input v-model="form.supplier_name" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="供应商类型">
|
||||||
|
<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="供应商等级">
|
||||||
|
<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="所属行业">
|
||||||
|
<el-input v-model="form.industry" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="企业类型">
|
||||||
|
<el-select v-model="form.enterprise_type" placeholder="请选择" style="width: 100%">
|
||||||
|
<el-option label="有限责任公司" value="有限责任公司" />
|
||||||
|
<el-option label="股份有限公司" value="股份有限公司" />
|
||||||
|
<el-option label="合伙企业" value="合伙企业" />
|
||||||
|
<el-option label="个人独资企业" value="个人独资企业" />
|
||||||
|
<el-option label="国有企业" value="国有企业" />
|
||||||
|
<el-option label="集体企业" value="集体企业" />
|
||||||
|
<el-option label="外商投资企业" value="外商投资企业" />
|
||||||
|
<el-option label="其他" value="其他" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="纳税人资质">
|
||||||
|
<el-select v-model="form.taxpayer_qualification" placeholder="请选择" style="width: 100%">
|
||||||
|
<el-option label="一般纳税人" value="一般纳税人" />
|
||||||
|
<el-option label="小规模纳税人" value="小规模纳税人" />
|
||||||
|
<el-option label="其他" value="其他" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="状态">
|
||||||
|
<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-col :span="12">
|
||||||
|
<el-form-item label="注册资本">
|
||||||
|
<el-input v-model="form.registered_capital" placeholder="万元">
|
||||||
|
<template #append>万元</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="实缴资本">
|
||||||
|
<el-input v-model="form.paid_capital" placeholder="万元">
|
||||||
|
<template #append>万元</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="成立日期">
|
||||||
|
<el-date-picker v-model="form.establish_date" type="date" value-format="YYYY-MM-DD" style="width: 100%" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="行政区划">
|
||||||
|
<el-input v-model="form.administrative_division" placeholder="如:江苏省连云港市海州区" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="经营范围">
|
||||||
|
<el-input v-model="form.business_scope" type="textarea" :rows="2" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="公司地址">
|
||||||
|
<el-input v-model="form.address" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-divider content-position="left">联系信息</el-divider>
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-form-item label="联系人">
|
||||||
|
<el-input v-model="form.contact_person" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-form-item label="联系电话">
|
||||||
|
<el-input v-model="form.contact_phone" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-form-item label="邮箱">
|
||||||
|
<el-input v-model="form.contact_email" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-divider content-position="left">开票信息</el-divider>
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="发票抬头">
|
||||||
|
<el-input v-model="form.invoice_title" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="税号">
|
||||||
|
<el-input v-model="form.tax_number" placeholder="18位统一社会信用代码" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="开户银行">
|
||||||
|
<el-input v-model="form.bank_name" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="银行账号">
|
||||||
|
<el-input v-model="form.bank_account" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="注册地址">
|
||||||
|
<el-input v-model="form.registered_address" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="注册电话">
|
||||||
|
<el-input v-model="form.registered_phone" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-divider content-position="left">备注</el-divider>
|
||||||
|
<el-form-item label="备注">
|
||||||
|
<el-input v-model="form.remark" type="textarea" :rows="2" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<!-- 显示原始AI响应数据 -->
|
||||||
|
<el-divider content-position="left">AI响应数据</el-divider>
|
||||||
|
<el-form-item label="查询信息" v-if="genMeta.model || (genMeta.tools && genMeta.tools.length)">
|
||||||
|
<div class="ai-meta">
|
||||||
|
<div class="ai-meta-row">
|
||||||
|
<span class="ai-meta-label">使用模型:</span>
|
||||||
|
<span>{{ genMeta.model || '—' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="ai-meta-row">
|
||||||
|
<span class="ai-meta-label">调用工具:</span>
|
||||||
|
<template v-if="genMeta.tools && genMeta.tools.length">
|
||||||
|
<el-tag
|
||||||
|
v-for="t in genMeta.tools"
|
||||||
|
:key="t.server_id + '-' + t.tool_name"
|
||||||
|
size="small"
|
||||||
|
type="info"
|
||||||
|
class="ai-tool-tag"
|
||||||
|
>{{ t.server_name }} / {{ t.tool_name }}</el-tag>
|
||||||
|
</template>
|
||||||
|
<span v-else>无</span>
|
||||||
|
</div>
|
||||||
|
<div class="ai-meta-row" v-if="genMeta.found !== undefined">
|
||||||
|
<span class="ai-meta-label">查到结果:</span>
|
||||||
|
<el-tag v-if="genMeta.found" size="small" type="success">已查到工商信息</el-tag>
|
||||||
|
<el-tag v-else size="small" type="warning">未查到(请手动补全)</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="详细数据">
|
||||||
|
<div class="ai-data-box">
|
||||||
|
<pre>{{ formattedAiData }}</pre>
|
||||||
|
</div>
|
||||||
|
<div class="form-tip">此数据为AI返回的原始响应,仅作查看参考,已自动填充可编辑字段到上方表单</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button v-if="step === 2" @click="step = 1">返回修改名称</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="step === 2"
|
||||||
|
type="primary"
|
||||||
|
:loading="saving"
|
||||||
|
@click="handleSave"
|
||||||
|
>确认保存</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, watch } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { Cpu } from '@element-plus/icons-vue'
|
||||||
|
import { smartGenerateCompany } from '@/api/ai'
|
||||||
|
import { createCrmSupplier } from '@/api/crm'
|
||||||
|
import {
|
||||||
|
SUPPLIER_TYPE_OPTIONS,
|
||||||
|
STATUS_OPTIONS,
|
||||||
|
levelOptions,
|
||||||
|
} from '../../dict'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
visible: { type: Boolean, default: false },
|
||||||
|
type: { type: String, default: 'supplier' } // 当前仅支持 supplier
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:visible', 'success'])
|
||||||
|
|
||||||
|
const step = ref(1)
|
||||||
|
const companyName = ref('')
|
||||||
|
const generating = ref(false)
|
||||||
|
const saving = ref(false)
|
||||||
|
const formRef = ref()
|
||||||
|
|
||||||
|
const defaultForm = () => ({
|
||||||
|
supplier_name: '',
|
||||||
|
supplier_type: '1',
|
||||||
|
supplier_level: '3',
|
||||||
|
industry: '',
|
||||||
|
enterprise_type: '有限责任公司',
|
||||||
|
taxpayer_qualification: '一般纳税人',
|
||||||
|
status: '1',
|
||||||
|
registered_capital: '',
|
||||||
|
paid_capital: '',
|
||||||
|
establish_date: '',
|
||||||
|
administrative_division: '',
|
||||||
|
business_scope: '',
|
||||||
|
address: '',
|
||||||
|
contact_person: '',
|
||||||
|
contact_phone: '',
|
||||||
|
contact_email: '',
|
||||||
|
invoice_title: '',
|
||||||
|
tax_number: '',
|
||||||
|
bank_name: '',
|
||||||
|
bank_account: '',
|
||||||
|
registered_address: '',
|
||||||
|
registered_phone: '',
|
||||||
|
remark: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const form = reactive(defaultForm())
|
||||||
|
|
||||||
|
// 本次查询使用的模型与 MCP 工具(后端返回)
|
||||||
|
const genMeta = ref({ model: '', tools: [], found: undefined })
|
||||||
|
|
||||||
|
// 原始AI响应数据(JSON格式字符串)
|
||||||
|
const rawAiData = ref('')
|
||||||
|
|
||||||
|
// formattedAiData 直接显示 rawAiData 的内容
|
||||||
|
const formattedAiData = ref('')
|
||||||
|
watch(rawAiData, (newVal) => {
|
||||||
|
formattedAiData.value = newVal
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
async function handleGenerate() {
|
||||||
|
if (!companyName.value.trim()) {
|
||||||
|
ElMessage.warning('请输入公司名称')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
generating.value = true
|
||||||
|
try {
|
||||||
|
const res = await smartGenerateCompany({
|
||||||
|
company_name: companyName.value.trim(),
|
||||||
|
type: props.type
|
||||||
|
})
|
||||||
|
if (res.code === 200) {
|
||||||
|
// 记录本次查询使用的模型与 MCP 工具,便于前端透明展示
|
||||||
|
genMeta.value = {
|
||||||
|
model: res.data.model || '',
|
||||||
|
tools: res.data.tools || [],
|
||||||
|
found: res.data.found
|
||||||
|
}
|
||||||
|
if (res.data.parsed && res.data.data) {
|
||||||
|
const data = res.data.data
|
||||||
|
form.supplier_name = data.supplier_name || data.name || companyName.value.trim()
|
||||||
|
form.supplier_type = data.supplier_type || '1'
|
||||||
|
form.supplier_level = data.supplier_level || '3'
|
||||||
|
form.status = data.status || '1'
|
||||||
|
form.industry = data.industry || ''
|
||||||
|
form.enterprise_type = data.enterprise_type || ''
|
||||||
|
form.taxpayer_qualification = data.taxpayer_qualification || ''
|
||||||
|
form.registered_capital = data.registered_capital || ''
|
||||||
|
form.paid_capital = data.paid_capital || ''
|
||||||
|
form.establish_date = data.establish_date || ''
|
||||||
|
form.administrative_division = data.administrative_division || ''
|
||||||
|
form.business_scope = data.business_scope || ''
|
||||||
|
form.address = data.address || ''
|
||||||
|
form.contact_person = data.contact_person || ''
|
||||||
|
form.contact_phone = data.contact_phone || ''
|
||||||
|
form.contact_email = data.contact_email || ''
|
||||||
|
form.invoice_title = data.invoice_title || form.supplier_name
|
||||||
|
form.tax_number = data.tax_number || ''
|
||||||
|
form.bank_name = data.bank_name || ''
|
||||||
|
form.bank_account = data.bank_account || ''
|
||||||
|
form.registered_address = data.registered_address || ''
|
||||||
|
form.registered_phone = data.registered_phone || ''
|
||||||
|
form.remark = data.remark || ''
|
||||||
|
// 保存原始AI响应数据(使用后端返回的原始数据)
|
||||||
|
if (res.data.raw) {
|
||||||
|
rawAiData.value = res.data.raw
|
||||||
|
} else {
|
||||||
|
rawAiData.value = JSON.stringify(data, null, 2)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 解析失败,显示AI原始返回内容
|
||||||
|
let rawContent = res.data.raw || '无原始数据'
|
||||||
|
let errorMsg = res.data.error || '未知错误'
|
||||||
|
rawAiData.value = rawContent
|
||||||
|
if (rawContent.length > 1500) {
|
||||||
|
rawContent = rawContent.substring(0, 1500) + '...'
|
||||||
|
}
|
||||||
|
ElMessage.error({
|
||||||
|
message: `AI生成失败:${errorMsg}\n\n原始响应:\n${rawContent}`,
|
||||||
|
showClose: true,
|
||||||
|
duration: 0
|
||||||
|
})
|
||||||
|
form.supplier_name = companyName.value.trim()
|
||||||
|
}
|
||||||
|
step.value = 2
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error(e.message || '生成失败')
|
||||||
|
} finally {
|
||||||
|
generating.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSave() {
|
||||||
|
if (!form.supplier_name.trim()) {
|
||||||
|
ElMessage.warning('供应商名称不能为空')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
supplier_name: form.supplier_name,
|
||||||
|
supplier_type: form.supplier_type,
|
||||||
|
supplier_level: form.supplier_level,
|
||||||
|
status: form.status,
|
||||||
|
industry: form.industry,
|
||||||
|
enterprise_type: form.enterprise_type,
|
||||||
|
taxpayer_qualification: form.taxpayer_qualification,
|
||||||
|
registered_capital: form.registered_capital,
|
||||||
|
paid_capital: form.paid_capital,
|
||||||
|
establish_date: form.establish_date,
|
||||||
|
administrative_division: form.administrative_division,
|
||||||
|
business_scope: form.business_scope,
|
||||||
|
address: form.address,
|
||||||
|
contact_person: form.contact_person,
|
||||||
|
contact_phone: form.contact_phone,
|
||||||
|
contact_email: form.contact_email,
|
||||||
|
invoice_title: form.invoice_title,
|
||||||
|
tax_number: form.tax_number,
|
||||||
|
bank_name: form.bank_name,
|
||||||
|
bank_account: form.bank_account,
|
||||||
|
registered_address: form.registered_address,
|
||||||
|
registered_phone: form.registered_phone,
|
||||||
|
remark: form.remark,
|
||||||
|
is_draft: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
await createCrmSupplier(payload)
|
||||||
|
|
||||||
|
ElMessage.success('保存成功')
|
||||||
|
emit('success')
|
||||||
|
emit('update:visible', false)
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error(e.message || '保存失败')
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClosed() {
|
||||||
|
step.value = 1
|
||||||
|
companyName.value = ''
|
||||||
|
Object.assign(form, defaultForm())
|
||||||
|
rawAiData.value = ''
|
||||||
|
formattedAiData.value = ''
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.step-input {
|
||||||
|
padding: 20px 0;
|
||||||
|
|
||||||
|
.step-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-confirm {
|
||||||
|
.confirm-header {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.smart-form {
|
||||||
|
max-height: 60vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-data-box {
|
||||||
|
background: var(--el-fill-color-lighter);
|
||||||
|
border: 1px solid var(--el-border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 12px;
|
||||||
|
font-family: 'Consolas', 'Monaco', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-meta {
|
||||||
|
background: var(--el-fill-color-lighter);
|
||||||
|
border: 1px solid var(--el-border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
|
||||||
|
.ai-meta-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-meta-label {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-tool-tag {
|
||||||
|
margin-right: 6px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<el-button :icon="Refresh" @click="fetchList">刷新</el-button>
|
<el-button :icon="Refresh" @click="fetchList">刷新</el-button>
|
||||||
<el-button type="primary" :icon="Plus" @click="openCreate">新增供应商</el-button>
|
<el-button type="primary" :icon="Plus" @click="openCreate">新增供应商</el-button>
|
||||||
|
<el-button type="success" :icon="Cpu" @click="smartAddVisible = true">智能添加</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -155,11 +156,18 @@
|
|||||||
{{ statusText(detailData.status) }}
|
{{ statusText(detailData.status) }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="合作开始">{{ formatDate(detailData.register_time) }}</el-descriptions-item>
|
<el-descriptions-item label="注册资本">{{ detailData.registered_capital ? detailData.registered_capital + ' 万元' : '-' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="合作到期">{{ formatDate(detailData.expire_time) }}</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">
|
<el-descriptions-item label="供应商地址" :span="2">
|
||||||
{{ detailData.address || '-' }}
|
{{ detailData.address || '-' }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="经营范围" :span="2">
|
||||||
|
{{ detailData.business_scope || '-' }}
|
||||||
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="创建时间">{{ formatDateTime(detailData.create_time) }}</el-descriptions-item>
|
<el-descriptions-item label="创建时间">{{ formatDateTime(detailData.create_time) }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="更新时间">{{ formatDateTime(detailData.update_time) }}</el-descriptions-item>
|
<el-descriptions-item label="更新时间">{{ formatDateTime(detailData.update_time) }}</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
@@ -234,6 +242,8 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="mobile" label="手机号" width="130" />
|
<el-table-column prop="mobile" label="手机号" width="130" />
|
||||||
<el-table-column prop="email" label="邮箱" min-width="180" show-overflow-tooltip />
|
<el-table-column prop="email" label="邮箱" min-width="180" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="wechat" label="微信" width="120" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="qq" label="QQ" width="110" show-overflow-tooltip />
|
||||||
<el-table-column prop="department" label="部门" width="120" 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 prop="position" label="职位" width="130" show-overflow-tooltip />
|
||||||
<el-table-column label="操作" width="120" align="center" fixed="right">
|
<el-table-column label="操作" width="120" align="center" fixed="right">
|
||||||
@@ -257,13 +267,19 @@
|
|||||||
:default-related="contactRelated"
|
:default-related="contactRelated"
|
||||||
@success="loadContacts"
|
@success="loadContacts"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<SmartAddCompany
|
||||||
|
v-model:visible="smartAddVisible"
|
||||||
|
type="supplier"
|
||||||
|
@success="fetchList"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, computed, onMounted } from "vue";
|
import { ref, reactive, computed, onMounted } from "vue";
|
||||||
import { ElMessage, ElMessageBox } from "element-plus";
|
import { ElMessage, ElMessageBox } from "element-plus";
|
||||||
import { Plus, Search, Refresh } from "@element-plus/icons-vue";
|
import { Plus, Search, Refresh, Cpu } from "@element-plus/icons-vue";
|
||||||
import {
|
import {
|
||||||
getCrmSupplierList,
|
getCrmSupplierList,
|
||||||
deleteCrmSupplier,
|
deleteCrmSupplier,
|
||||||
@@ -272,6 +288,7 @@ import {
|
|||||||
import { listContacts, deleteContact } from "@/api/contact";
|
import { listContacts, deleteContact } from "@/api/contact";
|
||||||
import SupplierEdit from "./components/edit.vue";
|
import SupplierEdit from "./components/edit.vue";
|
||||||
import ContactEdit from "../contact/components/edit.vue";
|
import ContactEdit from "../contact/components/edit.vue";
|
||||||
|
import SmartAddCompany from "./components/smartAddCompany.vue";
|
||||||
import {
|
import {
|
||||||
SUPPLIER_TYPE_OPTIONS,
|
SUPPLIER_TYPE_OPTIONS,
|
||||||
STATUS_OPTIONS,
|
STATUS_OPTIONS,
|
||||||
@@ -299,6 +316,7 @@ const contactList = ref([]);
|
|||||||
const currentRow = ref(null);
|
const currentRow = ref(null);
|
||||||
const contactEditVisible = ref(false);
|
const contactEditVisible = ref(false);
|
||||||
const contactEditData = ref(null);
|
const contactEditData = ref(null);
|
||||||
|
const smartAddVisible = ref(false);
|
||||||
|
|
||||||
const filters = reactive({
|
const filters = reactive({
|
||||||
keyword: "",
|
keyword: "",
|
||||||
@@ -530,4 +548,20 @@ async function handleContactDelete(row) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.detail-content {
|
||||||
|
padding: 4px 8px;
|
||||||
|
|
||||||
|
:deep(.el-descriptions__label) {
|
||||||
|
width: 130px;
|
||||||
|
min-width: 130px;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
font-weight: 500;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-descriptions__content) {
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -225,6 +225,32 @@
|
|||||||
|
|
||||||
<!-- 显示原始AI响应数据 -->
|
<!-- 显示原始AI响应数据 -->
|
||||||
<el-divider content-position="left">AI响应数据</el-divider>
|
<el-divider content-position="left">AI响应数据</el-divider>
|
||||||
|
<el-form-item label="查询信息" v-if="genMeta.model || (genMeta.tools && genMeta.tools.length)">
|
||||||
|
<div class="ai-meta">
|
||||||
|
<div class="ai-meta-row">
|
||||||
|
<span class="ai-meta-label">使用模型:</span>
|
||||||
|
<span>{{ genMeta.model || '—' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="ai-meta-row">
|
||||||
|
<span class="ai-meta-label">调用工具:</span>
|
||||||
|
<template v-if="genMeta.tools && genMeta.tools.length">
|
||||||
|
<el-tag
|
||||||
|
v-for="t in genMeta.tools"
|
||||||
|
:key="t.server_id + '-' + t.tool_name"
|
||||||
|
size="small"
|
||||||
|
type="info"
|
||||||
|
class="ai-tool-tag"
|
||||||
|
>{{ t.server_name }} / {{ t.tool_name }}</el-tag>
|
||||||
|
</template>
|
||||||
|
<span v-else>无</span>
|
||||||
|
</div>
|
||||||
|
<div class="ai-meta-row" v-if="genMeta.found !== undefined">
|
||||||
|
<span class="ai-meta-label">查到结果:</span>
|
||||||
|
<el-tag v-if="genMeta.found" size="small" type="success">已查到工商信息</el-tag>
|
||||||
|
<el-tag v-else size="small" type="warning">未查到(请手动补全)</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="详细数据">
|
<el-form-item label="详细数据">
|
||||||
<div class="ai-data-box">
|
<div class="ai-data-box">
|
||||||
<pre>{{ formattedAiData }}</pre>
|
<pre>{{ formattedAiData }}</pre>
|
||||||
@@ -326,6 +352,9 @@ const defaultForm = () => ({
|
|||||||
|
|
||||||
const form = reactive(defaultForm())
|
const form = reactive(defaultForm())
|
||||||
|
|
||||||
|
// 本次查询使用的模型与 MCP 工具(后端返回)
|
||||||
|
const genMeta = ref({ model: '', tools: [], found: undefined })
|
||||||
|
|
||||||
// 原始AI响应数据(JSON格式字符串)
|
// 原始AI响应数据(JSON格式字符串)
|
||||||
const rawAiData = ref('')
|
const rawAiData = ref('')
|
||||||
|
|
||||||
@@ -348,6 +377,12 @@ async function handleGenerate() {
|
|||||||
type: props.type
|
type: props.type
|
||||||
})
|
})
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
|
// 记录本次查询使用的模型与 MCP 工具,便于前端透明展示
|
||||||
|
genMeta.value = {
|
||||||
|
model: res.data.model || '',
|
||||||
|
tools: res.data.tools || [],
|
||||||
|
found: res.data.found
|
||||||
|
}
|
||||||
if (res.data.parsed && res.data.data) {
|
if (res.data.parsed && res.data.data) {
|
||||||
const data = res.data.data
|
const data = res.data.data
|
||||||
const nameField = props.type === 'supplier' ? 'supplier_name' : 'customer_name'
|
const nameField = props.type === 'supplier' ? 'supplier_name' : 'customer_name'
|
||||||
@@ -506,5 +541,37 @@ function handleClosed() {
|
|||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ai-meta {
|
||||||
|
background: var(--el-fill-color-lighter);
|
||||||
|
border: 1px solid var(--el-border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
|
||||||
|
.ai-meta-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-meta-label {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-tool-tag {
|
||||||
|
margin-right: 6px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -179,11 +179,12 @@ type mcpToolSummary struct {
|
|||||||
|
|
||||||
const maxToolRounds = 6
|
const maxToolRounds = 6
|
||||||
|
|
||||||
// aiUsage 一次 AI 调用的 token 用量
|
// aiUsage 一次 AI 调用的 token 用量与真实模型
|
||||||
type aiUsage struct {
|
type aiUsage struct {
|
||||||
PromptTokens int `json:"prompt_tokens"`
|
Model string `json:"model,omitempty"` // 接口实际返回的使用模型(如网关把 auto 解析后的真实模型)
|
||||||
CompletionTokens int `json:"completion_tokens"`
|
PromptTokens int `json:"prompt_tokens"`
|
||||||
TotalTokens int `json:"total_tokens"`
|
CompletionTokens int `json:"completion_tokens"`
|
||||||
|
TotalTokens int `json:"total_tokens"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// add 累加用量
|
// add 累加用量
|
||||||
@@ -201,6 +202,21 @@ func (u aiUsage) total() int {
|
|||||||
return u.PromptTokens + u.CompletionTokens
|
return u.PromptTokens + u.CompletionTokens
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// usedMcpTool 单次调用中实际使用的 MCP 工具(含所属服务器)
|
||||||
|
type usedMcpTool struct {
|
||||||
|
ServerID uint64 `json:"server_id"`
|
||||||
|
ServerName string `json:"server_name"`
|
||||||
|
ToolName string `json:"tool_name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// aiToolUsage 一次 AI 工具循环调用的元信息:用了什么模型、调用了哪些 MCP 工具
|
||||||
|
type aiToolUsage struct {
|
||||||
|
Model string `json:"model"` // 实际使用的模型
|
||||||
|
Rounds int `json:"rounds"` // 工具循环轮数
|
||||||
|
Tools []usedMcpTool `json:"tools"` // 实际被调用的 MCP 工具(去重)
|
||||||
|
ToolCount int `json:"tool_count"` // 工具调用种类数
|
||||||
|
}
|
||||||
|
|
||||||
// sanitizeToolName 工具名规范化为 OpenAI 允许的字符集
|
// sanitizeToolName 工具名规范化为 OpenAI 允许的字符集
|
||||||
func sanitizeToolName(s string) string {
|
func sanitizeToolName(s string) string {
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
@@ -346,24 +362,56 @@ func buildMcpAssistantMessage(text string, pending []pendingToolCall) openaiMess
|
|||||||
return msg
|
return msg
|
||||||
}
|
}
|
||||||
|
|
||||||
// runToolLoop 运行带已启用 MCP 工具的完整对话循环(非流式),返回最终文本。
|
// runToolLoop 运行带已启用 MCP 工具的完整对话循环(非流式),返回最终文本与调用元信息。
|
||||||
// 供聊天非流式接口与智能生成等模块复用。
|
// 供聊天非流式接口与智能生成等模块复用。
|
||||||
func runToolLoop(claims *jwtutil.Claims, provider models.BackendAiProvider, model string, systemPrompt string, messages []openaiMessage) (string, error) {
|
func runToolLoop(claims *jwtutil.Claims, provider models.BackendAiProvider, model string, systemPrompt string, messages []openaiMessage) (string, *aiToolUsage, error) {
|
||||||
// 智能生成等内部调用:默认注入全部已启用的 MCP 工具
|
// 智能生成等内部调用:默认注入全部已启用的 MCP 工具
|
||||||
llmTools, refMap, _, _ := collectMcpTools(claims, true, nil)
|
llmTools, refMap, _, _ := collectMcpTools(claims, true, nil)
|
||||||
|
usage := &aiToolUsage{Model: model}
|
||||||
|
usedSet := make(map[string]bool)
|
||||||
|
addUsed := func(key string) {
|
||||||
|
ref, ok := refMap[key]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id := fmt.Sprintf("%d:%s", ref.ServerID, ref.ToolName)
|
||||||
|
if usedSet[id] {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
usedSet[id] = true
|
||||||
|
usage.Tools = append(usage.Tools, usedMcpTool{
|
||||||
|
ServerID: ref.ServerID,
|
||||||
|
ServerName: ref.ServerName,
|
||||||
|
ToolName: ref.ToolName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
current := messages
|
current := messages
|
||||||
rounds := 0
|
rounds := 0
|
||||||
for {
|
for {
|
||||||
rounds++
|
rounds++
|
||||||
text, pending, _, err := callAITools(provider, model, systemPrompt, current, llmTools)
|
text, pending, roundUsage, err := callAITools(provider, model, systemPrompt, current, llmTools)
|
||||||
|
// 用接口实际返回的真实模型覆盖占位符(如 auto),若网关未回传则保留请求模型
|
||||||
|
if roundUsage.Model != "" {
|
||||||
|
usage.Model = roundUsage.Model
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
usage.Rounds = rounds
|
||||||
|
usage.ToolCount = len(usage.Tools)
|
||||||
|
return "", usage, err
|
||||||
|
}
|
||||||
|
for _, pc := range pending {
|
||||||
|
addUsed(pc.Name)
|
||||||
}
|
}
|
||||||
if len(pending) == 0 {
|
if len(pending) == 0 {
|
||||||
return text, nil
|
usage.Rounds = rounds
|
||||||
|
usage.ToolCount = len(usage.Tools)
|
||||||
|
return text, usage, nil
|
||||||
}
|
}
|
||||||
if rounds >= maxToolRounds {
|
if rounds >= maxToolRounds {
|
||||||
return text, nil
|
usage.Rounds = rounds
|
||||||
|
usage.ToolCount = len(usage.Tools)
|
||||||
|
return text, usage, nil
|
||||||
}
|
}
|
||||||
current = append(current, buildMcpAssistantMessage(text, pending))
|
current = append(current, buildMcpAssistantMessage(text, pending))
|
||||||
toolMsgs := executePendingTools(claims, pending, refMap, nil, nil)
|
toolMsgs := executePendingTools(claims, pending, refMap, nil, nil)
|
||||||
@@ -923,6 +971,7 @@ func callAITools(provider models.BackendAiProvider, model string, systemPrompt s
|
|||||||
return "", nil, aiUsage{}, fmt.Errorf("API返回错误状态 %d: %s", resp.StatusCode, string(bodyBytes))
|
return "", nil, aiUsage{}, fmt.Errorf("API返回错误状态 %d: %s", resp.StatusCode, string(bodyBytes))
|
||||||
}
|
}
|
||||||
var result struct {
|
var result struct {
|
||||||
|
Model string `json:"model"`
|
||||||
Choices []struct {
|
Choices []struct {
|
||||||
Message struct {
|
Message struct {
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
@@ -955,6 +1004,10 @@ func callAITools(provider models.BackendAiProvider, model string, systemPrompt s
|
|||||||
TotalTokens: result.Usage.TotalTokens,
|
TotalTokens: result.Usage.TotalTokens,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 接口实际返回的使用模型(网关可能将 auto 解析为具体模型)
|
||||||
|
if result.Model != "" {
|
||||||
|
usage.Model = result.Model
|
||||||
|
}
|
||||||
content := result.Choices[0].Message.Content
|
content := result.Choices[0].Message.Content
|
||||||
var pending []pendingToolCall
|
var pending []pendingToolCall
|
||||||
for _, tc := range result.Choices[0].Message.ToolCalls {
|
for _, tc := range result.Choices[0].Message.ToolCalls {
|
||||||
@@ -1029,6 +1082,8 @@ func callAITools(provider models.BackendAiProvider, model string, systemPrompt s
|
|||||||
TotalTokens: result.Usage.InputTokens + result.Usage.OutputTokens,
|
TotalTokens: result.Usage.InputTokens + result.Usage.OutputTokens,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Anthropic 响应不回传模型名,记录请求使用的模型
|
||||||
|
usage.Model = model
|
||||||
var text strings.Builder
|
var text strings.Builder
|
||||||
var pending []pendingToolCall
|
var pending []pendingToolCall
|
||||||
for _, block := range result.Content {
|
for _, block := range result.Content {
|
||||||
@@ -1129,6 +1184,7 @@ func callOpenAIStreamTools(provider models.BackendAiProvider, model string, syst
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
var chunk struct {
|
var chunk struct {
|
||||||
|
Model string `json:"model"`
|
||||||
Choices []struct {
|
Choices []struct {
|
||||||
Delta struct {
|
Delta struct {
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
@@ -1152,11 +1208,12 @@ func callOpenAIStreamTools(provider models.BackendAiProvider, model string, syst
|
|||||||
return "", nil, aiUsage{}, fmt.Errorf(chunk.Error.Message)
|
return "", nil, aiUsage{}, fmt.Errorf(chunk.Error.Message)
|
||||||
}
|
}
|
||||||
if chunk.Usage != nil {
|
if chunk.Usage != nil {
|
||||||
usage = aiUsage{
|
usage.PromptTokens = chunk.Usage.PromptTokens
|
||||||
PromptTokens: chunk.Usage.PromptTokens,
|
usage.CompletionTokens = chunk.Usage.CompletionTokens
|
||||||
CompletionTokens: chunk.Usage.CompletionTokens,
|
usage.TotalTokens = chunk.Usage.TotalTokens
|
||||||
TotalTokens: chunk.Usage.TotalTokens,
|
}
|
||||||
}
|
if chunk.Model != "" {
|
||||||
|
usage.Model = chunk.Model
|
||||||
}
|
}
|
||||||
if len(chunk.Choices) == 0 {
|
if len(chunk.Choices) == 0 {
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -240,7 +240,7 @@ func (c *BackendAiSmartGenerateController) GenerateCompany() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 调用AI(非流式,含已启用 MCP 工具:AI 会先调用天眼查等工具获取真实数据再生成)
|
// 调用AI(非流式,含已启用 MCP 工具:AI 会先调用天眼查等工具获取真实数据再生成)
|
||||||
reply, err := runToolLoop(claims, provider, useModel, "", messages)
|
reply, usage, err := runToolLoop(claims, provider, useModel, "", messages)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.sgJsonErr(500, 500, "AI生成失败: "+err.Error())
|
c.sgJsonErr(500, 500, "AI生成失败: "+err.Error())
|
||||||
return
|
return
|
||||||
@@ -269,12 +269,16 @@ func (c *BackendAiSmartGenerateController) GenerateCompany() {
|
|||||||
"error": "AI返回格式异常,无法解析JSON",
|
"error": "AI返回格式异常,无法解析JSON",
|
||||||
"name": p.CompanyName,
|
"name": p.CompanyName,
|
||||||
"type": p.Type,
|
"type": p.Type,
|
||||||
|
"model": usage.Model,
|
||||||
|
"tools": usage.Tools,
|
||||||
|
"tool_count": usage.ToolCount,
|
||||||
|
"rounds": usage.Rounds,
|
||||||
"generate_time": time.Now().Format("2006-01-02 15:04:05"),
|
"generate_time": time.Now().Format("2006-01-02 15:04:05"),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// KV兜底成功,继续走后面的结果组装
|
// KV兜底成功,继续走后面的结果组装
|
||||||
c.sgOk(smartGenResult(reply, result, p.CompanyName, p.Type, nameField))
|
c.sgOk(smartGenResult(reply, result, p.CompanyName, p.Type, nameField, usage))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,17 +289,31 @@ func (c *BackendAiSmartGenerateController) GenerateCompany() {
|
|||||||
"error": fmt.Sprintf("解析JSON失败: %v", err),
|
"error": fmt.Sprintf("解析JSON失败: %v", err),
|
||||||
"name": p.CompanyName,
|
"name": p.CompanyName,
|
||||||
"type": p.Type,
|
"type": p.Type,
|
||||||
|
"model": usage.Model,
|
||||||
|
"tools": usage.Tools,
|
||||||
|
"tool_count": usage.ToolCount,
|
||||||
|
"rounds": usage.Rounds,
|
||||||
"generate_time": time.Now().Format("2006-01-02 15:04:05"),
|
"generate_time": time.Now().Format("2006-01-02 15:04:05"),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
c.sgOk(smartGenResult(reply, result, p.CompanyName, p.Type, nameField))
|
c.sgOk(smartGenResult(reply, result, p.CompanyName, p.Type, nameField, usage))
|
||||||
|
}
|
||||||
|
|
||||||
|
// aiGenNotFound 判断 AI 回复是否明确表示未查到企业信息(MCP 工具查询无结果)
|
||||||
|
func aiGenNotFound(raw string) bool {
|
||||||
|
for _, kw := range []string{"未找到", "未匹配", "查无", "未查询到", "未检索到", "no matching", "not found"} {
|
||||||
|
if strings.Contains(raw, kw) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// smartGenResult 组装智能生成成功响应(含完整AI回复 raw),并对表单受限字段做规整
|
// smartGenResult 组装智能生成成功响应(含完整AI回复 raw),并对表单受限字段做规整
|
||||||
func smartGenResult(reply string, result map[string]interface{}, companyName, genType, nameField string) map[string]interface{} {
|
func smartGenResult(reply string, result map[string]interface{}, companyName, genType, nameField string, usage *aiToolUsage) map[string]interface{} {
|
||||||
// 确保公司名称字段存在
|
// 确保公司名称字段存在
|
||||||
if _, ok := result[nameField]; !ok || result[nameField] == "" {
|
if _, ok := result[nameField]; !ok || result[nameField] == "" {
|
||||||
result[nameField] = companyName
|
result[nameField] = companyName
|
||||||
@@ -317,10 +335,15 @@ func smartGenResult(reply string, result map[string]interface{}, companyName, ge
|
|||||||
crossFillPhone(result, "contact_phone", "registered_phone")
|
crossFillPhone(result, "contact_phone", "registered_phone")
|
||||||
return map[string]interface{}{
|
return map[string]interface{}{
|
||||||
"parsed": true,
|
"parsed": true,
|
||||||
|
"found": !aiGenNotFound(reply), // MCP 是否查到企业信息
|
||||||
"data": result,
|
"data": result,
|
||||||
"raw": reply, // AI完整回复(含企业画像markdown,供前端"AI响应数据"展示)
|
"raw": reply, // AI完整回复(含企业画像markdown,供前端"AI响应数据"展示)
|
||||||
"name": companyName,
|
"name": companyName,
|
||||||
"type": genType,
|
"type": genType,
|
||||||
|
"model": usage.Model, // 实际使用的模型
|
||||||
|
"tools": usage.Tools, // 实际调用的 MCP 工具(server + tool)
|
||||||
|
"tool_count": usage.ToolCount,
|
||||||
|
"rounds": usage.Rounds,
|
||||||
"generate_time": time.Now().Format("2006-01-02 15:04:05"),
|
"generate_time": time.Now().Format("2006-01-02 15:04:05"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,495 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"server/models"
|
||||||
|
"server/pkg/jwtutil"
|
||||||
|
|
||||||
|
"github.com/beego/beego/v2/client/orm"
|
||||||
|
beego "github.com/beego/beego/v2/server/web"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BackendCrmContactController CRM 联系人管理(客户 / 供应商的对接人)
|
||||||
|
//
|
||||||
|
// 数据复用已有的「公司联系人」表 models.ErpCompanyContact:
|
||||||
|
// - related_type(1=客户,2=供应商) <-> company_type(customer/supplier)
|
||||||
|
// - related_id <-> company_id
|
||||||
|
// - contact_name <-> name
|
||||||
|
// - mobile(手机号) <-> mobiles(JSON数组,取第一个)
|
||||||
|
// - phone(座机) <-> phone
|
||||||
|
// - wechat / qq / dingtalk / home_address / status <-> 同名字段
|
||||||
|
//
|
||||||
|
// 这样 CRM 联系人与 ERP 公司联系人是同一份数据,不会分裂。
|
||||||
|
type BackendCrmContactController struct {
|
||||||
|
beego.Controller
|
||||||
|
}
|
||||||
|
|
||||||
|
// crmContactDTO 返回给前端的联系人结构
|
||||||
|
type crmContactDTO struct {
|
||||||
|
ID uint64 `json:"id"`
|
||||||
|
RelatedType int `json:"related_type"`
|
||||||
|
RelatedID uint64 `json:"related_id"`
|
||||||
|
RelatedName string `json:"related_name"`
|
||||||
|
ContactName string `json:"contact_name"`
|
||||||
|
Gender int8 `json:"gender"`
|
||||||
|
Mobile string `json:"mobile"` // 手机号(mobiles 数组第一个)
|
||||||
|
Mobiles string `json:"mobiles"` // 手机号原始 JSON 数组
|
||||||
|
Phone string `json:"phone"` // 座机
|
||||||
|
Email string `json:"email"`
|
||||||
|
Wechat string `json:"wechat"` // 微信
|
||||||
|
QQ string `json:"qq"` // QQ
|
||||||
|
Dingtalk string `json:"dingtalk"` // 钉钉
|
||||||
|
Department string `json:"department"`
|
||||||
|
Position string `json:"position"`
|
||||||
|
HomeAddress string `json:"home_address"` // 家庭住址
|
||||||
|
IsPrimary int8 `json:"is_primary"`
|
||||||
|
Remark string `json:"remark"`
|
||||||
|
Status int8 `json:"status"` // 1-在职 0-离职
|
||||||
|
CreateTime time.Time `json:"create_time"`
|
||||||
|
UpdateTime time.Time `json:"update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildMobiles 将单个手机号转为 mobiles 的 JSON 数组字符串
|
||||||
|
func buildMobiles(mobile string) string {
|
||||||
|
mobile = strings.TrimSpace(mobile)
|
||||||
|
if mobile == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
arr := []string{mobile}
|
||||||
|
b, err := json.Marshal(arr)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// firstMobile 从 mobiles(JSON 数组或普通字符串)中取出第一个手机号
|
||||||
|
func firstMobile(mobiles string) string {
|
||||||
|
mobiles = strings.TrimSpace(mobiles)
|
||||||
|
if mobiles == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
var arr []string
|
||||||
|
if err := json.Unmarshal([]byte(mobiles), &arr); err == nil {
|
||||||
|
for _, m := range arr {
|
||||||
|
if strings.TrimSpace(m) != "" {
|
||||||
|
return strings.TrimSpace(m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
// 非 JSON,按原样返回
|
||||||
|
return mobiles
|
||||||
|
}
|
||||||
|
|
||||||
|
func crmContactCompanyType(relatedType int) string {
|
||||||
|
if relatedType == 2 {
|
||||||
|
return "supplier"
|
||||||
|
}
|
||||||
|
return "customer"
|
||||||
|
}
|
||||||
|
|
||||||
|
func crmContactRelatedType(companyType string) int {
|
||||||
|
if companyType == "supplier" {
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// crmContactAtoi 将查询参数安全转为 int,非法值返回 0
|
||||||
|
func crmContactAtoi(s string) int {
|
||||||
|
n, _ := strconv.Atoi(strings.TrimSpace(s))
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BackendCrmContactController) contactClaims() (*jwtutil.Claims, error) {
|
||||||
|
auth := c.Ctx.Request.Header.Get("Authorization")
|
||||||
|
if auth == "" {
|
||||||
|
return nil, fmt.Errorf("未登录")
|
||||||
|
}
|
||||||
|
parts := strings.SplitN(auth, " ", 2)
|
||||||
|
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||||
|
return nil, fmt.Errorf("认证信息格式错误")
|
||||||
|
}
|
||||||
|
claims, err := jwtutil.ParseToken(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("无效的token")
|
||||||
|
}
|
||||||
|
if claims.UserType != "backend" {
|
||||||
|
return nil, fmt.Errorf("无权访问")
|
||||||
|
}
|
||||||
|
return claims, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BackendCrmContactController) crmContactJsonErr(httpStatus, bizCode int, msg string) {
|
||||||
|
c.Ctx.Output.SetStatus(httpStatus)
|
||||||
|
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
|
||||||
|
_ = c.ServeJSON()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BackendCrmContactController) crmContactOk(data interface{}) {
|
||||||
|
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data}
|
||||||
|
_ = c.ServeJSON()
|
||||||
|
}
|
||||||
|
|
||||||
|
// toCrmContactDTO 模型转 DTO,names 为关联对象名称映射(key: "customer:123")
|
||||||
|
func toCrmContactDTO(row models.ErpCompanyContact, names map[string]string) crmContactDTO {
|
||||||
|
relatedType := crmContactRelatedType(row.CompanyType)
|
||||||
|
key := fmt.Sprintf("%s:%d", row.CompanyType, row.CompanyID)
|
||||||
|
return crmContactDTO{
|
||||||
|
ID: row.ID,
|
||||||
|
RelatedType: relatedType,
|
||||||
|
RelatedID: row.CompanyID,
|
||||||
|
RelatedName: names[key],
|
||||||
|
ContactName: row.Name,
|
||||||
|
Gender: row.Gender,
|
||||||
|
Mobile: firstMobile(row.Mobiles),
|
||||||
|
Mobiles: row.Mobiles,
|
||||||
|
Phone: row.Phone,
|
||||||
|
Email: row.Email,
|
||||||
|
Wechat: row.Wechat,
|
||||||
|
QQ: row.QQ,
|
||||||
|
Dingtalk: row.Dingtalk,
|
||||||
|
Department: row.Department,
|
||||||
|
Position: row.Position,
|
||||||
|
HomeAddress: row.HomeAddress,
|
||||||
|
IsPrimary: row.IsPrimary,
|
||||||
|
Remark: row.Remark,
|
||||||
|
Status: row.Status,
|
||||||
|
CreateTime: row.CreateTime,
|
||||||
|
UpdateTime: row.UpdateTime,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadRelatedNames 批量查询关联的客户/供应商名称,避免逐条查询
|
||||||
|
func loadRelatedNames(tenantID string, rows []models.ErpCompanyContact) map[string]string {
|
||||||
|
names := map[string]string{}
|
||||||
|
if len(rows) == 0 || models.Orm == nil {
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
customerIDs := make([]uint64, 0)
|
||||||
|
supplierIDs := make([]uint64, 0)
|
||||||
|
for _, r := range rows {
|
||||||
|
if r.CompanyType == "supplier" {
|
||||||
|
supplierIDs = append(supplierIDs, r.CompanyID)
|
||||||
|
} else {
|
||||||
|
customerIDs = append(customerIDs, r.CompanyID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(customerIDs) > 0 {
|
||||||
|
var list []models.TenantCrmCustomer
|
||||||
|
if _, err := models.Orm.QueryTable(new(models.TenantCrmCustomer)).
|
||||||
|
Filter("tenant_id", tenantID).
|
||||||
|
Filter("id__in", customerIDs).
|
||||||
|
All(&list); err == nil {
|
||||||
|
for _, r := range list {
|
||||||
|
names[fmt.Sprintf("customer:%d", r.ID)] = r.CustomerName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(supplierIDs) > 0 {
|
||||||
|
var list []models.TenantCrmSupplier
|
||||||
|
if _, err := models.Orm.QueryTable(new(models.TenantCrmSupplier)).
|
||||||
|
Filter("tenant_id", tenantID).
|
||||||
|
Filter("id__in", supplierIDs).
|
||||||
|
All(&list); err == nil {
|
||||||
|
for _, r := range list {
|
||||||
|
names[fmt.Sprintf("supplier:%d", r.ID)] = r.SupplierName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
// clearOtherPrimary 将同一公司下的其他联系人取消主联系人,保证只有一个主联系人
|
||||||
|
func clearOtherPrimary(tenantID, companyType string, companyID, excludeID uint64) {
|
||||||
|
if models.Orm == nil || companyID == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
qs := models.Orm.QueryTable(new(models.ErpCompanyContact)).
|
||||||
|
Filter("tenant_id", tenantID).
|
||||||
|
Filter("company_type", companyType).
|
||||||
|
Filter("company_id", companyID).
|
||||||
|
Filter("delete_time__isnull", true).
|
||||||
|
Exclude("id", excludeID)
|
||||||
|
_, _ = qs.Update(map[string]interface{}{"is_primary": 0})
|
||||||
|
}
|
||||||
|
|
||||||
|
// List GET /backend/crm/contact/list?related_type=1&related_id=7&keyword=&is_primary=
|
||||||
|
func (c *BackendCrmContactController) List() {
|
||||||
|
claims, err := c.contactClaims()
|
||||||
|
if err != nil {
|
||||||
|
c.crmContactJsonErr(401, 401, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
page, _ := c.GetInt("page", 1)
|
||||||
|
pageSize, _ := c.GetInt("pageSize", 20)
|
||||||
|
keyword := strings.TrimSpace(c.GetString("keyword"))
|
||||||
|
relatedTypeStr := strings.TrimSpace(c.GetString("related_type"))
|
||||||
|
relatedIDStr := strings.TrimSpace(c.GetString("related_id"))
|
||||||
|
primaryStr := strings.TrimSpace(c.GetString("is_primary"))
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
if pageSize < 1 || pageSize > 100 {
|
||||||
|
pageSize = 20
|
||||||
|
}
|
||||||
|
|
||||||
|
tenantID := fmt.Sprintf("%d", claims.TenantId)
|
||||||
|
cond := orm.NewCondition().
|
||||||
|
And("tenant_id", tenantID).
|
||||||
|
And("delete_time__isnull", true)
|
||||||
|
|
||||||
|
if relatedTypeStr != "" {
|
||||||
|
cond = cond.And("company_type", crmContactCompanyType(crmContactAtoi(relatedTypeStr)))
|
||||||
|
}
|
||||||
|
if relatedIDStr != "" && relatedIDStr != "0" {
|
||||||
|
cond = cond.And("company_id", relatedIDStr)
|
||||||
|
}
|
||||||
|
if primaryStr != "" {
|
||||||
|
cond = cond.And("is_primary", crmContactAtoi(primaryStr))
|
||||||
|
}
|
||||||
|
if keyword != "" {
|
||||||
|
kw := orm.NewCondition().
|
||||||
|
Or("name__contains", keyword).
|
||||||
|
Or("phone__contains", keyword).
|
||||||
|
Or("email__contains", keyword)
|
||||||
|
cond = cond.AndCond(kw)
|
||||||
|
}
|
||||||
|
|
||||||
|
qs := models.Orm.QueryTable(new(models.ErpCompanyContact)).SetCond(cond)
|
||||||
|
total, _ := qs.Count()
|
||||||
|
var rows []models.ErpCompanyContact
|
||||||
|
if total > 0 {
|
||||||
|
_, _ = qs.OrderBy("-is_primary", "-update_time").
|
||||||
|
Offset((page - 1) * pageSize).
|
||||||
|
Limit(pageSize).
|
||||||
|
All(&rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
names := loadRelatedNames(tenantID, rows)
|
||||||
|
list := make([]crmContactDTO, 0, len(rows))
|
||||||
|
for _, r := range rows {
|
||||||
|
list = append(list, toCrmContactDTO(r, names))
|
||||||
|
}
|
||||||
|
|
||||||
|
c.crmContactOk(map[string]interface{}{
|
||||||
|
"list": list,
|
||||||
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
"pageSize": pageSize,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add POST /backend/crm/contact/add
|
||||||
|
func (c *BackendCrmContactController) Add() {
|
||||||
|
claims, err := c.contactClaims()
|
||||||
|
if err != nil {
|
||||||
|
c.crmContactJsonErr(401, 401, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var p struct {
|
||||||
|
RelatedType int `json:"related_type"`
|
||||||
|
RelatedID uint64 `json:"related_id"`
|
||||||
|
ContactName string `json:"contact_name"`
|
||||||
|
Gender int8 `json:"gender"`
|
||||||
|
Mobile string `json:"mobile"`
|
||||||
|
Phone string `json:"phone"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Wechat string `json:"wechat"`
|
||||||
|
QQ string `json:"qq"`
|
||||||
|
Dingtalk string `json:"dingtalk"`
|
||||||
|
Department string `json:"department"`
|
||||||
|
Position string `json:"position"`
|
||||||
|
HomeAddress string `json:"home_address"`
|
||||||
|
IsPrimary int8 `json:"is_primary"`
|
||||||
|
Status int8 `json:"status"`
|
||||||
|
Remark string `json:"remark"`
|
||||||
|
}
|
||||||
|
raw, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||||
|
if err := json.Unmarshal(raw, &p); err != nil {
|
||||||
|
c.crmContactJsonErr(400, 400, "参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(p.ContactName) == "" {
|
||||||
|
c.crmContactJsonErr(400, 400, "联系人姓名不能为空")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if p.RelatedID == 0 {
|
||||||
|
c.crmContactJsonErr(400, 400, "请选择关联对象")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
companyType := crmContactCompanyType(p.RelatedType)
|
||||||
|
now := time.Now()
|
||||||
|
contact := models.ErpCompanyContact{
|
||||||
|
TenantID: fmt.Sprintf("%d", claims.TenantId),
|
||||||
|
CompanyType: companyType,
|
||||||
|
CompanyID: p.RelatedID,
|
||||||
|
Name: strings.TrimSpace(p.ContactName),
|
||||||
|
Gender: p.Gender,
|
||||||
|
Phone: strings.TrimSpace(p.Phone), // 座机
|
||||||
|
Mobiles: buildMobiles(p.Mobile), // 手机号(JSON数组)
|
||||||
|
Email: strings.TrimSpace(p.Email),
|
||||||
|
Wechat: strings.TrimSpace(p.Wechat),
|
||||||
|
QQ: strings.TrimSpace(p.QQ),
|
||||||
|
Dingtalk: strings.TrimSpace(p.Dingtalk),
|
||||||
|
Department: strings.TrimSpace(p.Department),
|
||||||
|
Position: strings.TrimSpace(p.Position),
|
||||||
|
HomeAddress: strings.TrimSpace(p.HomeAddress),
|
||||||
|
IsPrimary: p.IsPrimary,
|
||||||
|
Status: 1,
|
||||||
|
Remark: p.Remark,
|
||||||
|
CreateTime: now,
|
||||||
|
UpdateTime: now,
|
||||||
|
}
|
||||||
|
// 新增联系人默认在职
|
||||||
|
if p.Status == 0 {
|
||||||
|
contact.Status = 1
|
||||||
|
} else {
|
||||||
|
contact.Status = p.Status
|
||||||
|
}
|
||||||
|
id, err := models.Orm.Insert(&contact)
|
||||||
|
if err != nil {
|
||||||
|
c.crmContactJsonErr(500, 500, "新增失败: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if p.IsPrimary == 1 {
|
||||||
|
clearOtherPrimary(fmt.Sprintf("%d", claims.TenantId), companyType, p.RelatedID, uint64(id))
|
||||||
|
}
|
||||||
|
c.crmContactOk(map[string]interface{}{"id": id})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edit POST /backend/crm/contact/edit
|
||||||
|
func (c *BackendCrmContactController) Edit() {
|
||||||
|
claims, err := c.contactClaims()
|
||||||
|
if err != nil {
|
||||||
|
c.crmContactJsonErr(401, 401, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var p struct {
|
||||||
|
ID uint64 `json:"id"`
|
||||||
|
RelatedType int `json:"related_type"`
|
||||||
|
RelatedID uint64 `json:"related_id"`
|
||||||
|
ContactName string `json:"contact_name"`
|
||||||
|
Gender int8 `json:"gender"`
|
||||||
|
Mobile string `json:"mobile"`
|
||||||
|
Phone string `json:"phone"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Wechat string `json:"wechat"`
|
||||||
|
QQ string `json:"qq"`
|
||||||
|
Dingtalk string `json:"dingtalk"`
|
||||||
|
Department string `json:"department"`
|
||||||
|
Position string `json:"position"`
|
||||||
|
HomeAddress string `json:"home_address"`
|
||||||
|
IsPrimary int8 `json:"is_primary"`
|
||||||
|
Status int8 `json:"status"`
|
||||||
|
Remark string `json:"remark"`
|
||||||
|
}
|
||||||
|
raw, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||||
|
if err := json.Unmarshal(raw, &p); err != nil {
|
||||||
|
c.crmContactJsonErr(400, 400, "参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if p.ID == 0 {
|
||||||
|
c.crmContactJsonErr(400, 400, "缺少ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(p.ContactName) == "" {
|
||||||
|
c.crmContactJsonErr(400, 400, "联系人姓名不能为空")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tenantID := fmt.Sprintf("%d", claims.TenantId)
|
||||||
|
var contact models.ErpCompanyContact
|
||||||
|
if err := models.Orm.QueryTable(new(models.ErpCompanyContact)).
|
||||||
|
Filter("id", p.ID).
|
||||||
|
Filter("tenant_id", tenantID).
|
||||||
|
Filter("delete_time__isnull", true).
|
||||||
|
One(&contact); err != nil {
|
||||||
|
c.crmContactJsonErr(404, 404, "联系人未找到")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 允许调整关联对象;未传则保持原值
|
||||||
|
companyType := contact.CompanyType
|
||||||
|
companyID := contact.CompanyID
|
||||||
|
if p.RelatedID > 0 {
|
||||||
|
companyType = crmContactCompanyType(p.RelatedType)
|
||||||
|
companyID = p.RelatedID
|
||||||
|
}
|
||||||
|
|
||||||
|
contact.CompanyType = companyType
|
||||||
|
contact.CompanyID = companyID
|
||||||
|
contact.Name = strings.TrimSpace(p.ContactName)
|
||||||
|
contact.Gender = p.Gender
|
||||||
|
contact.Phone = strings.TrimSpace(p.Phone) // 座机
|
||||||
|
contact.Mobiles = buildMobiles(p.Mobile) // 手机号(JSON数组)
|
||||||
|
contact.Email = strings.TrimSpace(p.Email)
|
||||||
|
contact.Wechat = strings.TrimSpace(p.Wechat)
|
||||||
|
contact.QQ = strings.TrimSpace(p.QQ)
|
||||||
|
contact.Dingtalk = strings.TrimSpace(p.Dingtalk)
|
||||||
|
contact.Department = strings.TrimSpace(p.Department)
|
||||||
|
contact.Position = strings.TrimSpace(p.Position)
|
||||||
|
contact.HomeAddress = strings.TrimSpace(p.HomeAddress)
|
||||||
|
contact.IsPrimary = p.IsPrimary
|
||||||
|
contact.Status = p.Status
|
||||||
|
contact.Remark = p.Remark
|
||||||
|
contact.UpdateTime = time.Now()
|
||||||
|
|
||||||
|
if _, err := models.Orm.Update(&contact); err != nil {
|
||||||
|
c.crmContactJsonErr(500, 500, "更新失败: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if p.IsPrimary == 1 {
|
||||||
|
clearOtherPrimary(tenantID, companyType, companyID, contact.ID)
|
||||||
|
}
|
||||||
|
c.crmContactOk(map[string]interface{}{"id": contact.ID})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete POST /backend/crm/contact/delete 软删除
|
||||||
|
func (c *BackendCrmContactController) Delete() {
|
||||||
|
claims, err := c.contactClaims()
|
||||||
|
if err != nil {
|
||||||
|
c.crmContactJsonErr(401, 401, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var p struct {
|
||||||
|
ID uint64 `json:"id"`
|
||||||
|
}
|
||||||
|
raw, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||||
|
if err := json.Unmarshal(raw, &p); err != nil {
|
||||||
|
c.crmContactJsonErr(400, 400, "参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if p.ID == 0 {
|
||||||
|
c.crmContactJsonErr(400, 400, "缺少ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
num, err := models.Orm.QueryTable(new(models.ErpCompanyContact)).
|
||||||
|
Filter("id", p.ID).
|
||||||
|
Filter("tenant_id", fmt.Sprintf("%d", claims.TenantId)).
|
||||||
|
Filter("delete_time__isnull", true).
|
||||||
|
Update(map[string]interface{}{
|
||||||
|
"delete_time": now,
|
||||||
|
"update_time": now,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
c.crmContactJsonErr(500, 500, "删除失败: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if num == 0 {
|
||||||
|
c.crmContactJsonErr(404, 404, "联系人未找到")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.crmContactOk(nil)
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"server/models"
|
||||||
|
"server/pkg/jwtutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CRM 客户 / 供应商的删除权限规则:
|
||||||
|
// 1. 平台管理员(UserType=platform)可删除;
|
||||||
|
// 2. 租户管理员(绑定「租户管理员」角色,或未分配角色/角色未配置权限等既有"全权限"情形)可删除;
|
||||||
|
// 3. 记录的创建人可删除自己创建的记录。
|
||||||
|
// 其余用户一律禁止删除。删除本身为软删除(置 delete_time)。
|
||||||
|
|
||||||
|
// canDeleteCrmRecord 判断当前用户是否有权删除指定记录。
|
||||||
|
// createUserID 为记录的创建人用户ID(历史数据可能为空)。
|
||||||
|
func canDeleteCrmRecord(claims *jwtutil.Claims, createUserID string) bool {
|
||||||
|
if claims == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// 平台管理员
|
||||||
|
if claims.UserType == "platform" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// 创建人本人
|
||||||
|
uid := fmt.Sprintf("%d", claims.UserID)
|
||||||
|
if createUserID != "" && createUserID == uid {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// 租户管理员 / 具备全权限的租户用户
|
||||||
|
return isTenantAdmin(claims.UserID, claims.TenantId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// isTenantAdmin 判断用户在指定租户内是否具备管理员(全权限)身份。
|
||||||
|
// 判定与既有权限约定保持一致:
|
||||||
|
// - 未分配角色(group_id=0)视为全权限;
|
||||||
|
// - 角色名为「租户管理员」视为管理员;
|
||||||
|
// - 角色未配置 rights(空/空数组/null)视为全权限。
|
||||||
|
func isTenantAdmin(userID int, tenantID int) bool {
|
||||||
|
if tenantID <= 0 || models.Orm == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
var tu models.SystemTenantUser
|
||||||
|
if err := models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||||
|
Filter("uid", userID).
|
||||||
|
Filter("tid", tenantID).
|
||||||
|
Filter("delete_time__isnull", true).
|
||||||
|
One(&tu); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if tu.GroupID == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
var role models.AdminRole
|
||||||
|
if err := models.Orm.QueryTable(new(models.AdminRole)).
|
||||||
|
Filter("id", tu.GroupID).
|
||||||
|
Filter("cid", 2).
|
||||||
|
Filter("tenant_id", tenantID).
|
||||||
|
One(&role); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if role.Name == "租户管理员" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
rights := ""
|
||||||
|
if role.Rights != nil {
|
||||||
|
rights = strings.TrimSpace(*role.Rights)
|
||||||
|
}
|
||||||
|
return rights == "" || rights == "[]" || rights == "null"
|
||||||
|
}
|
||||||
@@ -0,0 +1,268 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"server/models"
|
||||||
|
"server/pkg/jwtutil"
|
||||||
|
|
||||||
|
"github.com/beego/beego/v2/client/orm"
|
||||||
|
beego "github.com/beego/beego/v2/server/web"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BackendCrmPoolController 客户公海控制器
|
||||||
|
// 公海语义:in_pool=1 的客户处于公海中(租户共享,无个人负责人);
|
||||||
|
// 移入公海会置 in_pool=1 并释放负责人,领取/分配/移出会置 in_pool=0 并绑定负责人。
|
||||||
|
type BackendCrmPoolController struct {
|
||||||
|
beego.Controller
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BackendCrmPoolController) poolClaims() (*jwtutil.Claims, error) {
|
||||||
|
auth := c.Ctx.Request.Header.Get("Authorization")
|
||||||
|
if auth == "" {
|
||||||
|
return nil, fmt.Errorf("未登录")
|
||||||
|
}
|
||||||
|
parts := strings.SplitN(auth, " ", 2)
|
||||||
|
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||||
|
return nil, fmt.Errorf("认证信息格式错误")
|
||||||
|
}
|
||||||
|
claims, err := jwtutil.ParseToken(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("无效的token")
|
||||||
|
}
|
||||||
|
if claims.UserType != "backend" {
|
||||||
|
return nil, fmt.Errorf("无权访问")
|
||||||
|
}
|
||||||
|
return claims, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BackendCrmPoolController) poolJsonErr(httpStatus, bizCode int, msg string) {
|
||||||
|
c.Ctx.Output.SetStatus(httpStatus)
|
||||||
|
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
|
||||||
|
_ = c.ServeJSON()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BackendCrmPoolController) poolOk(data interface{}) {
|
||||||
|
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data}
|
||||||
|
_ = c.ServeJSON()
|
||||||
|
}
|
||||||
|
|
||||||
|
// List GET /backend/crm/pool/list 公海列表(无负责人、租户内共享)
|
||||||
|
func (c *BackendCrmPoolController) List() {
|
||||||
|
claims, err := c.poolClaims()
|
||||||
|
if err != nil {
|
||||||
|
c.poolJsonErr(401, 401, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
page, _ := c.GetInt("page", 1)
|
||||||
|
pageSize, _ := c.GetInt("pageSize", 20)
|
||||||
|
keyword := strings.TrimSpace(c.GetString("keyword"))
|
||||||
|
customerType := strings.TrimSpace(c.GetString("customer_type"))
|
||||||
|
customerLevel := strings.TrimSpace(c.GetString("customer_level"))
|
||||||
|
startTime := strings.TrimSpace(c.GetString("start_time"))
|
||||||
|
endTime := strings.TrimSpace(c.GetString("end_time"))
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
if pageSize < 1 || pageSize > 100 {
|
||||||
|
pageSize = 20
|
||||||
|
}
|
||||||
|
|
||||||
|
cond := orm.NewCondition().
|
||||||
|
And("tenant_id", fmt.Sprintf("%d", claims.TenantId)).
|
||||||
|
And("delete_time__isnull", true).
|
||||||
|
And("in_pool", 1)
|
||||||
|
if keyword != "" {
|
||||||
|
kw := orm.NewCondition().
|
||||||
|
Or("customer_name__contains", keyword).
|
||||||
|
Or("contact_person__contains", keyword).
|
||||||
|
Or("contact_phone__contains", keyword)
|
||||||
|
cond = cond.AndCond(kw)
|
||||||
|
}
|
||||||
|
if customerType != "" {
|
||||||
|
cond = cond.And("customer_type", customerType)
|
||||||
|
}
|
||||||
|
if customerLevel != "" {
|
||||||
|
cond = cond.And("customer_level", customerLevel)
|
||||||
|
}
|
||||||
|
if startTime != "" {
|
||||||
|
cond = cond.And("pool_in_time__gte", startTime)
|
||||||
|
}
|
||||||
|
if endTime != "" {
|
||||||
|
cond = cond.And("pool_in_time__lte", endTime+" 23:59:59")
|
||||||
|
}
|
||||||
|
|
||||||
|
qs := models.Orm.QueryTable(new(models.TenantCrmCustomer)).SetCond(cond)
|
||||||
|
total, _ := qs.Count()
|
||||||
|
var list []models.TenantCrmCustomer
|
||||||
|
if total > 0 {
|
||||||
|
_, _ = qs.OrderBy("-pool_in_time").Offset((page - 1) * pageSize).Limit(pageSize).All(&list)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.poolOk(map[string]interface{}{
|
||||||
|
"list": list,
|
||||||
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
"pageSize": pageSize,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move POST /backend/crm/pool/move 移入公海:释放负责人,记录原负责人与原因
|
||||||
|
func (c *BackendCrmPoolController) Move() {
|
||||||
|
claims, err := c.poolClaims()
|
||||||
|
if err != nil {
|
||||||
|
c.poolJsonErr(401, 401, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var p struct {
|
||||||
|
IDs []uint64 `json:"ids"`
|
||||||
|
Reason string `json:"reason"`
|
||||||
|
}
|
||||||
|
raw, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||||
|
if err := json.Unmarshal(raw, &p); err != nil {
|
||||||
|
c.poolJsonErr(400, 400, "参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(p.IDs) == 0 {
|
||||||
|
c.poolJsonErr(400, 400, "请选择要移入公海客户")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
reason := strings.TrimSpace(p.Reason)
|
||||||
|
if reason == "" {
|
||||||
|
c.poolJsonErr(400, 400, "请填写移入原因")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
updated := 0
|
||||||
|
for _, id := range p.IDs {
|
||||||
|
var cust models.TenantCrmCustomer
|
||||||
|
err := models.Orm.QueryTable(new(models.TenantCrmCustomer)).
|
||||||
|
Filter("id", id).
|
||||||
|
Filter("tenant_id", fmt.Sprintf("%d", claims.TenantId)).
|
||||||
|
Filter("delete_time__isnull", true).
|
||||||
|
One(&cust)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// 记录原负责人(移入公海前的负责人),再清空负责人并置入公海
|
||||||
|
cust.LastOwnerName = cust.OwnerUserName
|
||||||
|
cust.OwnerUserID = ""
|
||||||
|
cust.OwnerUserName = ""
|
||||||
|
cust.InPool = 1
|
||||||
|
cust.PoolReason = reason
|
||||||
|
cust.PoolInTime = &now
|
||||||
|
cust.UpdateTime = now
|
||||||
|
if _, err := models.Orm.Update(&cust,
|
||||||
|
"last_owner_name", "owner_user_id", "owner_user_name", "in_pool",
|
||||||
|
"pool_reason", "pool_in_time", "update_time"); err == nil {
|
||||||
|
updated++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.poolOk(map[string]interface{}{"updated": updated})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Claim POST /backend/crm/pool/claim 领取:将公海客户归属为当前登录用户
|
||||||
|
func (c *BackendCrmPoolController) Claim() {
|
||||||
|
claims, err := c.poolClaims()
|
||||||
|
if err != nil {
|
||||||
|
c.poolJsonErr(401, 401, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var p struct {
|
||||||
|
IDs []uint64 `json:"ids"`
|
||||||
|
}
|
||||||
|
raw, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||||
|
if err := json.Unmarshal(raw, &p); err != nil {
|
||||||
|
c.poolJsonErr(400, 400, "参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(p.IDs) == 0 {
|
||||||
|
c.poolJsonErr(400, 400, "请选择客户")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
uid := fmt.Sprintf("%d", claims.UserID)
|
||||||
|
updated := 0
|
||||||
|
for _, id := range p.IDs {
|
||||||
|
var cust models.TenantCrmCustomer
|
||||||
|
err := models.Orm.QueryTable(new(models.TenantCrmCustomer)).
|
||||||
|
Filter("id", id).
|
||||||
|
Filter("tenant_id", fmt.Sprintf("%d", claims.TenantId)).
|
||||||
|
Filter("delete_time__isnull", true).
|
||||||
|
One(&cust)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cust.OwnerUserID = uid
|
||||||
|
cust.OwnerUserName = claims.Username
|
||||||
|
cust.InPool = 0
|
||||||
|
cust.PoolInTime = nil
|
||||||
|
cust.PoolReason = ""
|
||||||
|
cust.UpdateTime = now
|
||||||
|
if _, err := models.Orm.Update(&cust,
|
||||||
|
"owner_user_id", "owner_user_name", "in_pool", "pool_in_time", "pool_reason", "update_time"); err == nil {
|
||||||
|
updated++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.poolOk(map[string]interface{}{"updated": updated})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign POST /backend/crm/pool/assign 分配:将公海客户分配给指定负责人
|
||||||
|
func (c *BackendCrmPoolController) Assign() {
|
||||||
|
claims, err := c.poolClaims()
|
||||||
|
if err != nil {
|
||||||
|
c.poolJsonErr(401, 401, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var p struct {
|
||||||
|
IDs []uint64 `json:"ids"`
|
||||||
|
OwnerUserID interface{} `json:"owner_user_id"`
|
||||||
|
OwnerUserName string `json:"owner_user_name"`
|
||||||
|
}
|
||||||
|
raw, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||||
|
if err := json.Unmarshal(raw, &p); err != nil {
|
||||||
|
c.poolJsonErr(400, 400, "参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(p.IDs) == 0 {
|
||||||
|
c.poolJsonErr(400, 400, "请选择客户")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ownerID := strings.TrimSpace(fmt.Sprintf("%v", p.OwnerUserID))
|
||||||
|
if ownerID == "" {
|
||||||
|
c.poolJsonErr(400, 400, "请指定负责人")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
updated := 0
|
||||||
|
for _, id := range p.IDs {
|
||||||
|
var cust models.TenantCrmCustomer
|
||||||
|
err := models.Orm.QueryTable(new(models.TenantCrmCustomer)).
|
||||||
|
Filter("id", id).
|
||||||
|
Filter("tenant_id", fmt.Sprintf("%d", claims.TenantId)).
|
||||||
|
Filter("delete_time__isnull", true).
|
||||||
|
One(&cust)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cust.OwnerUserID = ownerID
|
||||||
|
cust.OwnerUserName = strings.TrimSpace(p.OwnerUserName)
|
||||||
|
cust.InPool = 0
|
||||||
|
cust.PoolInTime = nil
|
||||||
|
cust.PoolReason = ""
|
||||||
|
cust.UpdateTime = now
|
||||||
|
if _, err := models.Orm.Update(&cust,
|
||||||
|
"owner_user_id", "owner_user_name", "in_pool", "pool_in_time", "pool_reason", "update_time"); err == nil {
|
||||||
|
updated++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.poolOk(map[string]interface{}{"updated": updated})
|
||||||
|
}
|
||||||
@@ -101,9 +101,11 @@ func (c *BackendErpCustomerController) List() {
|
|||||||
pageSize = 20
|
pageSize = 20
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 已移入公海(in_pool=1)的客户不在客户列表中展示,公海请到 /backend/crm/pool/list 查看
|
||||||
qs := models.Orm.QueryTable(new(models.TenantCrmCustomer)).
|
qs := models.Orm.QueryTable(new(models.TenantCrmCustomer)).
|
||||||
Filter("tenant_id", fmt.Sprintf("%d", claims.TenantId)).
|
Filter("tenant_id", fmt.Sprintf("%d", claims.TenantId)).
|
||||||
Filter("delete_time__isnull", true)
|
Filter("delete_time__isnull", true).
|
||||||
|
Filter("in_pool", 0)
|
||||||
|
|
||||||
if keyword != "" {
|
if keyword != "" {
|
||||||
qs = qs.Filter("customer_name__contains", keyword)
|
qs = qs.Filter("customer_name__contains", keyword)
|
||||||
@@ -224,6 +226,9 @@ func (c *BackendErpCustomerController) Create() {
|
|||||||
BankAccount: strings.TrimSpace(p.BankAccount),
|
BankAccount: strings.TrimSpace(p.BankAccount),
|
||||||
RegisteredAddress: strings.TrimSpace(p.RegisteredAddress),
|
RegisteredAddress: strings.TrimSpace(p.RegisteredAddress),
|
||||||
RegisteredPhone: strings.TrimSpace(p.RegisteredPhone),
|
RegisteredPhone: strings.TrimSpace(p.RegisteredPhone),
|
||||||
|
OwnerUserID: fmt.Sprintf("%d", claims.UserID),
|
||||||
|
OwnerUserName: claims.Username,
|
||||||
|
CreateUserID: fmt.Sprintf("%d", claims.UserID),
|
||||||
CreateTime: time.Now(),
|
CreateTime: time.Now(),
|
||||||
UpdateTime: time.Now(),
|
UpdateTime: time.Now(),
|
||||||
}
|
}
|
||||||
@@ -366,6 +371,22 @@ func (c *BackendErpCustomerController) Delete() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 删除权限:仅平台管理员、租户管理员或创建人本人可删除
|
||||||
|
var cust models.TenantCrmCustomer
|
||||||
|
if err := models.Orm.QueryTable(new(models.TenantCrmCustomer)).
|
||||||
|
Filter("id", id).
|
||||||
|
Filter("tenant_id", fmt.Sprintf("%d", claims.TenantId)).
|
||||||
|
Filter("delete_time__isnull", true).
|
||||||
|
One(&cust); err != nil {
|
||||||
|
c.customerJsonErr(404, 404, "客户未找到")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !canDeleteCrmRecord(claims, cust.CreateUserID) {
|
||||||
|
c.customerJsonErr(403, 403, "只有创建人、租户管理员或平台管理员可以删除该客户")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 软删除:仅置 delete_time,不物理删除
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
_, err = models.Orm.QueryTable(new(models.TenantCrmCustomer)).
|
_, err = models.Orm.QueryTable(new(models.TenantCrmCustomer)).
|
||||||
Filter("id", id).
|
Filter("id", id).
|
||||||
|
|||||||
@@ -224,6 +224,7 @@ func (c *BackendErpSupplierController) Create() {
|
|||||||
BankAccount: strings.TrimSpace(p.BankAccount),
|
BankAccount: strings.TrimSpace(p.BankAccount),
|
||||||
RegisteredAddress: strings.TrimSpace(p.RegisteredAddress),
|
RegisteredAddress: strings.TrimSpace(p.RegisteredAddress),
|
||||||
RegisteredPhone: strings.TrimSpace(p.RegisteredPhone),
|
RegisteredPhone: strings.TrimSpace(p.RegisteredPhone),
|
||||||
|
CreateUserID: fmt.Sprintf("%d", claims.UserID),
|
||||||
CreateTime: time.Now(),
|
CreateTime: time.Now(),
|
||||||
UpdateTime: time.Now(),
|
UpdateTime: time.Now(),
|
||||||
}
|
}
|
||||||
@@ -366,6 +367,22 @@ func (c *BackendErpSupplierController) Delete() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 删除权限:仅平台管理员、租户管理员或创建人本人可删除
|
||||||
|
var supplier models.TenantCrmSupplier
|
||||||
|
if err := models.Orm.QueryTable(new(models.TenantCrmSupplier)).
|
||||||
|
Filter("id", id).
|
||||||
|
Filter("tenant_id", fmt.Sprintf("%d", claims.TenantId)).
|
||||||
|
Filter("delete_time__isnull", true).
|
||||||
|
One(&supplier); err != nil {
|
||||||
|
c.supplierJsonErr(404, 404, "供应商未找到")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !canDeleteCrmRecord(claims, supplier.CreateUserID) {
|
||||||
|
c.supplierJsonErr(403, 403, "只有创建人、租户管理员或平台管理员可以删除该供应商")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 软删除:仅置 delete_time,不物理删除
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
_, err = models.Orm.QueryTable(new(models.TenantCrmSupplier)).
|
_, err = models.Orm.QueryTable(new(models.TenantCrmSupplier)).
|
||||||
Filter("id", id).
|
Filter("id", id).
|
||||||
|
|||||||
@@ -599,6 +599,13 @@ CREATE TABLE `yz_tenant_crm_customer` (
|
|||||||
`bank_account` varchar(50) DEFAULT '' COMMENT '开户行账号',
|
`bank_account` varchar(50) DEFAULT '' COMMENT '开户行账号',
|
||||||
`registered_address` varchar(255) DEFAULT '' COMMENT '注册地址',
|
`registered_address` varchar(255) DEFAULT '' COMMENT '注册地址',
|
||||||
`registered_phone` varchar(50) DEFAULT '' COMMENT '注册电话',
|
`registered_phone` varchar(50) DEFAULT '' COMMENT '注册电话',
|
||||||
|
`owner_user_id` varchar(64) NOT NULL DEFAULT '' COMMENT '负责人ID,空表示在公海',
|
||||||
|
`owner_user_name` varchar(128) NOT NULL DEFAULT '' COMMENT '负责人姓名',
|
||||||
|
`last_owner_name` varchar(128) NOT NULL DEFAULT '' COMMENT '移入公海前的原负责人',
|
||||||
|
`pool_in_time` datetime DEFAULT NULL COMMENT '进入公海时间',
|
||||||
|
`pool_reason` varchar(200) NOT NULL DEFAULT '' COMMENT '移入公海原因',
|
||||||
|
`in_pool` tinyint NOT NULL DEFAULT 0 COMMENT '是否在公海:0-否 1-是',
|
||||||
|
`create_user_id` varchar(64) NOT NULL DEFAULT '' COMMENT '创建人用户ID,用于删除权限校验',
|
||||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
`delete_time` datetime DEFAULT NULL COMMENT '删除时间',
|
`delete_time` datetime DEFAULT NULL COMMENT '删除时间',
|
||||||
@@ -639,6 +646,7 @@ CREATE TABLE `yz_tenant_crm_supplier` (
|
|||||||
`bank_account` varchar(50) DEFAULT '' COMMENT '开户行账号',
|
`bank_account` varchar(50) DEFAULT '' COMMENT '开户行账号',
|
||||||
`registered_address` varchar(255) DEFAULT '' COMMENT '注册地址',
|
`registered_address` varchar(255) DEFAULT '' COMMENT '注册地址',
|
||||||
`registered_phone` varchar(50) DEFAULT '' COMMENT '注册电话',
|
`registered_phone` varchar(50) DEFAULT '' COMMENT '注册电话',
|
||||||
|
`create_user_id` varchar(64) NOT NULL DEFAULT '' COMMENT '创建人用户ID,用于删除权限校验',
|
||||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
`delete_time` datetime DEFAULT NULL COMMENT '删除时间',
|
`delete_time` datetime DEFAULT NULL COMMENT '删除时间',
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ type ErpCompanyContact struct {
|
|||||||
CompanyID uint64 `orm:"column(company_id)" json:"company_id"`
|
CompanyID uint64 `orm:"column(company_id)" json:"company_id"`
|
||||||
Name string `orm:"column(name);size(50)" json:"name"`
|
Name string `orm:"column(name);size(50)" json:"name"`
|
||||||
Gender int8 `orm:"column(gender);default(0)" json:"gender"` // 0-未知 1-男 2-女
|
Gender int8 `orm:"column(gender);default(0)" json:"gender"` // 0-未知 1-男 2-女
|
||||||
Phone string `orm:"column(phone);size(20)" json:"phone"`
|
Phone string `orm:"column(phone);size(20)" json:"phone"` // 座机(与 ERP 联系人语义一致)
|
||||||
Mobiles string `orm:"column(mobiles);type(text);null" json:"mobiles"` // JSON数组,最多5个
|
Mobiles string `orm:"column(mobiles);type(text);null" json:"mobiles"` // 手机号,JSON数组,最多5个
|
||||||
Wechat string `orm:"column(wechat);size(50)" json:"wechat"`
|
Wechat string `orm:"column(wechat);size(50)" json:"wechat"`
|
||||||
QQ string `orm:"column(qq);size(50)" json:"qq"`
|
QQ string `orm:"column(qq);size(50)" json:"qq"`
|
||||||
Dingtalk string `orm:"column(dingtalk);size(50)" json:"dingtalk"`
|
Dingtalk string `orm:"column(dingtalk);size(50)" json:"dingtalk"`
|
||||||
|
|||||||
@@ -132,4 +132,50 @@ func Init(_ string) {
|
|||||||
EnsureAdminRoleTenantColumn()
|
EnsureAdminRoleTenantColumn()
|
||||||
EnsureAdminRoleCustomColumn()
|
EnsureAdminRoleCustomColumn()
|
||||||
EnsureTenantUserOrgColumn()
|
EnsureTenantUserOrgColumn()
|
||||||
|
EnsureCrmCustomerPoolColumns()
|
||||||
|
EnsureCrmCreateUserColumn()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// EnsureCrmCreateUserColumn 补齐客户/供应商的创建人字段(用于删除权限校验)。
|
||||||
|
// 同样兼容历史表名差异,表不存在或列已存在时忽略错误。
|
||||||
|
func EnsureCrmCreateUserColumn() {
|
||||||
|
tables := []string{
|
||||||
|
new(TenantCrmCustomer).TableName(), "yz_tenant_crm_customer",
|
||||||
|
new(TenantCrmSupplier).TableName(), "yz_tenant_crm_supplier",
|
||||||
|
}
|
||||||
|
for _, table := range tables {
|
||||||
|
sql := "ALTER TABLE " + table +
|
||||||
|
" ADD COLUMN create_user_id varchar(64) NOT NULL DEFAULT '' COMMENT '创建人用户ID,用于删除权限校验'"
|
||||||
|
if _, err := Orm.Raw(sql).Exec(); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsureCrmCustomerPoolColumns 补齐客户公海相关字段。
|
||||||
|
// 兼容历史表名差异:模型 TableName 为 yz_backend_erp_customer,初始化 SQL 中为 yz_tenant_crm_customer,
|
||||||
|
// 对两个候选表名都执行 ALTER(表不存在或列已存在时忽略错误)。
|
||||||
|
func EnsureCrmCustomerPoolColumns() {
|
||||||
|
candidates := []string{
|
||||||
|
new(TenantCrmCustomer).TableName(),
|
||||||
|
"yz_tenant_crm_customer",
|
||||||
|
}
|
||||||
|
cols := []string{
|
||||||
|
"in_pool tinyint NOT NULL DEFAULT 0 COMMENT '是否在公海:0-否 1-是'",
|
||||||
|
"owner_user_id varchar(64) NOT NULL DEFAULT '' COMMENT '负责人ID,公海中为空'",
|
||||||
|
"owner_user_name varchar(128) NOT NULL DEFAULT '' COMMENT '负责人姓名'",
|
||||||
|
"last_owner_name varchar(128) NOT NULL DEFAULT '' COMMENT '移入公海前的原负责人'",
|
||||||
|
"pool_in_time datetime DEFAULT NULL COMMENT '进入公海时间'",
|
||||||
|
"pool_reason varchar(200) NOT NULL DEFAULT '' COMMENT '移入公海原因'",
|
||||||
|
}
|
||||||
|
for _, table := range candidates {
|
||||||
|
for _, col := range cols {
|
||||||
|
sql := fmt.Sprintf("ALTER TABLE %s ADD COLUMN %s", table, col)
|
||||||
|
if _, err := Orm.Raw(sql).Exec(); err != nil {
|
||||||
|
// 列已存在或表不存在时忽略
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,14 @@ type TenantCrmCustomer struct {
|
|||||||
BankAccount string `orm:"column(bank_account);size(50)" json:"bank_account"`
|
BankAccount string `orm:"column(bank_account);size(50)" json:"bank_account"`
|
||||||
RegisteredAddress string `orm:"column(registered_address);size(255)" json:"registered_address"`
|
RegisteredAddress string `orm:"column(registered_address);size(255)" json:"registered_address"`
|
||||||
RegisteredPhone string `orm:"column(registered_phone);size(50)" json:"registered_phone"`
|
RegisteredPhone string `orm:"column(registered_phone);size(50)" json:"registered_phone"`
|
||||||
|
// 公海相关:in_pool=1 表示客户在公海中(租户共享,无个人负责人)
|
||||||
|
InPool int8 `orm:"column(in_pool);default(0)" json:"in_pool"` // 是否在公海:0-否 1-是
|
||||||
|
OwnerUserID string `orm:"column(owner_user_id);size(64);null" json:"owner_user_id"` // 当前负责人ID,公海中为空
|
||||||
|
OwnerUserName string `orm:"column(owner_user_name);size(128);null" json:"owner_user_name"` // 当前负责人姓名
|
||||||
|
LastOwnerName string `orm:"column(last_owner_name);size(128);null" json:"last_owner_name"` // 移入公海前的原负责人
|
||||||
|
PoolInTime *time.Time `orm:"column(pool_in_time);type(datetime);null" json:"pool_in_time"` // 进入公海时间
|
||||||
|
PoolReason string `orm:"column(pool_reason);size(200);null" json:"pool_reason"` // 移入公海原因
|
||||||
|
CreateUserID string `orm:"column(create_user_id);size(64);null" json:"create_user_id"` // 创建人用户ID,用于删除权限校验
|
||||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||||
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"`
|
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"`
|
||||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ type TenantCrmSupplier struct {
|
|||||||
BankAccount string `orm:"column(bank_account);size(50)" json:"bank_account"`
|
BankAccount string `orm:"column(bank_account);size(50)" json:"bank_account"`
|
||||||
RegisteredAddress string `orm:"column(registered_address);size(255)" json:"registered_address"`
|
RegisteredAddress string `orm:"column(registered_address);size(255)" json:"registered_address"`
|
||||||
RegisteredPhone string `orm:"column(registered_phone);size(50)" json:"registered_phone"`
|
RegisteredPhone string `orm:"column(registered_phone);size(50)" json:"registered_phone"`
|
||||||
|
CreateUserID string `orm:"column(create_user_id);size(64);null" json:"create_user_id"` // 创建人用户ID,用于删除权限校验
|
||||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||||
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"`
|
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"`
|
||||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
||||||
|
|||||||
@@ -372,6 +372,18 @@ func registerOrganizationRoutes(module string) {
|
|||||||
beego.Router("/backend/erp/customer/:id", &controllers.BackendErpCustomerController{}, "get:Detail;put:Update;delete:Delete")
|
beego.Router("/backend/erp/customer/:id", &controllers.BackendErpCustomerController{}, "get:Detail;put:Update;delete:Delete")
|
||||||
beego.Router("/backend/erp/customer/:id/status", &controllers.BackendErpCustomerController{}, "post:ToggleStatus")
|
beego.Router("/backend/erp/customer/:id/status", &controllers.BackendErpCustomerController{}, "post:ToggleStatus")
|
||||||
|
|
||||||
|
// CRM联系人(客户/供应商对接人)
|
||||||
|
beego.Router("/backend/crm/contact/list", &controllers.BackendCrmContactController{}, "get:List")
|
||||||
|
beego.Router("/backend/crm/contact/add", &controllers.BackendCrmContactController{}, "post:Add")
|
||||||
|
beego.Router("/backend/crm/contact/edit", &controllers.BackendCrmContactController{}, "post:Edit")
|
||||||
|
beego.Router("/backend/crm/contact/delete", &controllers.BackendCrmContactController{}, "post:Delete")
|
||||||
|
|
||||||
|
// CRM客户公海
|
||||||
|
beego.Router("/backend/crm/pool/list", &controllers.BackendCrmPoolController{}, "get:List")
|
||||||
|
beego.Router("/backend/crm/pool/move", &controllers.BackendCrmPoolController{}, "post:Move")
|
||||||
|
beego.Router("/backend/crm/pool/claim", &controllers.BackendCrmPoolController{}, "post:Claim")
|
||||||
|
beego.Router("/backend/crm/pool/assign", &controllers.BackendCrmPoolController{}, "post:Assign")
|
||||||
|
|
||||||
// ERP供应商管理
|
// ERP供应商管理
|
||||||
beego.Router("/backend/erp/supplier/list", &controllers.BackendErpSupplierController{}, "get:List")
|
beego.Router("/backend/erp/supplier/list", &controllers.BackendErpSupplierController{}, "get:List")
|
||||||
beego.Router("/backend/erp/supplier", &controllers.BackendErpSupplierController{}, "post:Create")
|
beego.Router("/backend/erp/supplier", &controllers.BackendErpSupplierController{}, "post:Create")
|
||||||
|
|||||||
Reference in New Issue
Block a user