Files
klp-oa/klp-ui/src/views/crm/customer/index.vue
砂糖 5ac2e78a33 feat(crm): 新增JSON表格输入组件并优化客户管理页面
refactor(OrderDetail): 允许orderId为undefined并添加空值检查
feat(JSONTableInput): 新增支持JSON与表格双向绑定的通用组件
refactor(customer): 重构客户详情页面,优化表单交互和字段映射
style(KLPList): 简化列表组件样式并修复字段显示逻辑
2025-12-16 11:47:53 +08:00

406 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="app-container">
<el-row :gutter="20">
<el-col :span="5" style="border-right: 1px solid #e4e7ed;">
<div style="font-weight: 900;">客户列表</div>
<div style="display: flex; align-items: center; gap: 5px; margin-top: 10px;">
<!-- 主搜索和添加 -->
<el-input style="flex: 1;" prefix-icon="el-icon-search" placeholder="输入客户编码搜索"
v-model="queryParams.customerCode" @change="getCustomerList" clearable></el-input>
<el-button icon="el-icon-search" @click="toggleQuery"></el-button>
<el-button type="primary" icon="el-icon-plus" style="margin-left: 0;" @click="handleAdd"></el-button>
</div>
<div v-show="showQuery" style="display: flex; align-items: center; gap: 5px; margin-top: 10px;">
<!-- 查询区通过上方的查询按钮控制显示隐藏 -->
<!-- 客户行业和客户等级的下拉选 -->
<el-select style="width: 100px;" v-model="queryParams.industry" placeholder="客户行业" clearable
@change="getCustomerList">
<el-option v-for="item in dict.type.customer_industry" :key="item.value" :label="item.label"
:value="item.value" />
</el-select>
<el-select style="width: 100px;" v-model="queryParams.customerLevel" placeholder="客户等级" clearable
@change="getCustomerList">
<el-option v-for="item in dict.type.customer_level" :key="item.value" :label="item.label"
:value="item.value" />
</el-select>
</div>
<div>
<!-- 列表区域 -->
<KLPList :listData="customerList" listKey="customerId" :loading="customerLoading" field1="customerCode"
field4="companyName" @item-click="handleItemClick">
<template slot="actions" slot-scope="{ item }">
<el-button type="danger" size="mini" @click="handleDelete(item)" icon="el-icon-delete"></el-button>
</template>
</KLPList>
</div>
</el-col>
<el-col :span="19">
<el-tabs v-model="activeTab" type="border-card" v-if="currentCustomer && currentCustomer.customerId">
<el-tab-pane label="客户详情" name="detail">
<!-- 客户详情区域 -->
<el-descriptions :column="2" border>
<el-descriptions-item label="客户编号">
{{ currentCustomer.customerCode || '-' }}
</el-descriptions-item>
<el-descriptions-item label="公司">
{{ currentCustomer.companyName || '-' }}
</el-descriptions-item>
<el-descriptions-item label="联系人">
{{ currentCustomer.contactPerson || '-' }}
</el-descriptions-item>
<el-descriptions-item label="客户联系方式">{{ currentCustomer.contactWay || '-' }}</el-descriptions-item>
<el-descriptions-item label="客户行业">
<dict-tag :value="currentCustomer.industry" :options="dict.type.customer_industry"></dict-tag>
</el-descriptions-item>
<el-descriptions-item label="客户等级">
<dict-tag :value="currentCustomer.customerLevel" :options="dict.type.customer_level"></dict-tag>
</el-descriptions-item>
<el-descriptions-item label="客户地址" v-hasPermi="['crm:customer:address']">{{ currentCustomer.address || '-'
}}</el-descriptions-item>
</el-descriptions>
</el-tab-pane>
<el-tab-pane label="信息编辑" name="edit">
<!-- 客户联系人区域 -->
<el-form label-position="top" :model="currentCustomer" :disabled="updateLoading">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="客户编号" prop="customerCode">
<el-input v-model="currentCustomer.customerCode" placeholder="请输入客户编号"
@input="handleDetailChange" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="公司" prop="company">
<el-input v-model="currentCustomer.companyName" placeholder="请输入公司名称" @input="handleDetailChange" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="联系人" prop="contactPerson">
<!-- 修复字段映射错误contact contactPerson -->
<el-input v-model="currentCustomer.contactPerson" placeholder="请输入联系人"
@input="handleDetailChange" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="客户联系方式" prop="contactWay">
<el-input v-model="currentCustomer.contactWay" placeholder="请输入客户联系方式"
@input="handleDetailChange" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="客户行业" prop="industry">
<el-select v-model="currentCustomer.industry" placeholder="请选择客户行业" clearable
@change="handleDetailChange">
<el-option v-for="item in dict.type.customer_industry" :key="item.value" :label="item.label"
:value="item.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="客户等级" prop="level">
<el-select v-model="currentCustomer.customerLevel" placeholder="请选择客户等级" clearable
@change="handleDetailChange">
<el-option v-for="item in dict.type.customer_level" :key="item.value" :label="item.label"
:value="item.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="24" v-hasPermi="['crm:customer:address']">
<el-form-item label="客户地址" prop="address">
<el-input type="textarea" v-model="currentCustomer.address" placeholder="请输入客户地址"
@input="handleDetailChange" />
</el-form-item>
</el-col>
<el-col :span="24" v-hasPermi="['crm:customer:bank']">
<el-form-item label="银行信息" prop="bankInfo">
<JSONTableInput @change="handleDetailChange" v-model="currentCustomer.bankInfo"
:columns="[{ prop: 'bankName', label: '银行名称' }, { prop: 'bankAccount', label: '银行账号' }]" />
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-tab-pane>
<el-tab-pane label="历史订单" name="transaction">
<!-- 客户交易记录区域 -->
<div>
<el-descriptions :column="2" border>
<el-descriptions-item label="订单总数">{{ currentCustomer.totalCount || 0 }}</el-descriptions-item>
<el-descriptions-item label="已成交订单数">{{ currentCustomer.dealCount || 0 }}</el-descriptions-item>
<el-descriptions-item label="待成交订单数">{{ currentCustomer.waitCount || 0 }}</el-descriptions-item>
<el-descriptions-item label="取消订单数">{{ currentCustomer.cancelCount || 0 }}</el-descriptions-item>
</el-descriptions>
<el-table :data="[]" border style="margin-top: 10px;" placeholder="暂无订单数据"></el-table>
</div>
</el-tab-pane>
</el-tabs>
<el-empty v-else style="margin-top: 20px;" description="选择客户查看详情"></el-empty>
</el-col>
</el-row>
<!-- 添加或修改客户信息对话框 -->
<el-dialog title="录入客户" :visible.sync="open" width="500px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-form-item label="客户编码" prop="customerCode">
<el-input v-model="form.customerCode" placeholder="请输入客户编码" />
</el-form-item>
<el-form-item label="公司名称" prop="companyName">
<el-input v-model="form.companyName" placeholder="请输入公司名称" />
</el-form-item>
<el-form-item label="联系人" prop="contactPerson">
<el-input v-model="form.contactPerson" placeholder="请输入联系人" />
</el-form-item>
<el-form-item label="联系方式" prop="contactWay">
<el-input v-model="form.contactWay" placeholder="请输入联系方式" />
</el-form-item>
<el-form-item label="所属行业" prop="industry">
<el-select v-model="form.industry" placeholder="请选择所属行业" clearable>
<el-option v-for="item in dict.type.customer_industry" :key="item.value" :label="item.label"
:value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="客户等级" prop="customerLevel">
<el-select v-model="form.customerLevel" placeholder="请选择客户等级" clearable>
<el-option v-for="item in dict.type.customer_level" :key="item.value" :label="item.label"
:value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="客户地址" prop="address">
<el-input v-model="form.address" placeholder="请输入客户地址" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" placeholder="请输入备注" />
</el-form-item>
<el-form-item label="银行信息" prop="transactionRecords">
<JSONTableInput v-model="form.bankInfo"
:columns="[{ prop: 'bankName', label: '银行名称' }, { prop: 'bankAccount', label: '银行账号' }]" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import KLPList from '@/components/KLPUI/KLPList/index.vue'
import JSONTableInput from '../components/JSONTableInput.vue'
import { listCustomer, addCustomer, updateCustomer, delCustomer } from '@/api/crm/customer'
export default {
name: 'CustomerPage',
components: {
KLPList,
JSONTableInput
},
dicts: ['customer_industry', 'customer_level'],
data() {
return {
customerList: [],
showQuery: false,
queryParams: {
industry: '',
customerLevel: '',
customerCode: '',
pageNum: 1,
pageSize: 10
},
total: 0,
activeTab: 'detail',
customerLoading: false,
currentCustomer: {},
open: false,
form: {},
buttonLoading: false,
updateLoading: false, // 编辑请求加载状态
debounceTimer: null, // 防抖定时器
rules: {
customerCode: [{ required: true, message: '请输入客户编码', trigger: 'blur' }],
companyName: [{ required: true, message: '请输入公司名称', trigger: 'blur' }],
contactPerson: [{ required: true, message: '请输入联系人', trigger: 'blur' }],
contactWay: [{ required: true, message: '请输入联系方式', trigger: 'blur' }],
industry: [{ required: true, message: '请选择所属行业', trigger: 'change' }],
customerLevel: [{ required: true, message: '请选择客户等级', trigger: 'change' }],
address: [{ required: true, message: '请输入客户地址', trigger: 'blur' }],
}
}
},
computed: {
currentCustomerId() {
return this.currentCustomer.customerId || undefined
}
},
mounted() {
this.getCustomerList();
},
beforeDestroy() {
// 销毁时清除防抖定时器,避免内存泄漏
clearTimeout(this.debounceTimer);
},
methods: {
toggleQuery() {
this.showQuery = !this.showQuery
},
/**
* 防抖函数(通用)
* @param {Function} fn - 执行函数
* @param {Number} delay - 延迟时间(ms)
* @returns {Function} 防抖后的函数
*/
debounce(fn, delay) {
return (...args) => {
// 清除上一次定时器
if (this.debounceTimer) clearTimeout(this.debounceTimer);
// 重新设置定时器
this.debounceTimer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
},
/** 表单编辑变更 - 防抖提交 */
handleDetailChange: function () {
// 绑定防抖函数延迟2秒仅最后一次变更后执行
this.debounce(async () => {
// 无客户ID或加载中不执行
if (!this.currentCustomerId || this.updateLoading) return;
try {
this.updateLoading = true;
// 深拷贝避免请求过程中数据被修改
const params = { ...this.currentCustomer };
const res = await updateCustomer(params);
this.$message({
type: 'success',
message: '客户信息更新成功'
});
// 同步列表数据(可选)
this.syncCustomerList(params);
} catch (error) {
this.$message({
type: 'error',
message: '更新失败:' + (error.msg || '服务器异常')
});
} finally {
this.updateLoading = false;
}
}, 1000)();
},
/** 同步列表数据(避免列表和详情数据不一致) */
syncCustomerList(updatedCustomer) {
const index = this.customerList.findIndex(item => item.customerId === updatedCustomer.customerId);
if (index > -1) {
this.$set(this.customerList, index, { ...this.customerList[index], ...updatedCustomer });
}
},
getCustomerList() {
this.customerLoading = true;
listCustomer(this.queryParams).then(response => {
this.customerList = response.rows || [];
this.total = response.total || 0; // 补充总数
this.customerLoading = false;
}).catch(() => {
this.customerLoading = false;
this.$message.error('获取客户列表失败');
});
},
handleItemClick(item) {
// 深拷贝避免原数据被直接修改
this.currentCustomer = { ...item };
this.activeTab = 'detail';
},
// 表单重置
reset() {
this.form = {
customerId: undefined,
customerCode: undefined,
companyName: undefined,
contactPerson: undefined,
contactWay: undefined,
industry: undefined,
customerLevel: undefined,
address: undefined,
bankInfo: undefined,
remark: undefined,
createBy: undefined,
createTime: undefined,
updateBy: undefined,
updateTime: undefined,
delFlag: undefined
};
if (this.$refs.form) this.$refs.form.resetFields(); // 修复重置表单
},
/** 新增按钮操作 */
handleAdd() {
this.reset();
this.open = true;
},
submitForm() {
this.$refs.form.validate(async (valid) => {
if (valid) {
this.buttonLoading = true;
try {
await addCustomer(this.form);
this.$message({
message: '客户录入成功',
type: 'success'
});
this.open = false;
this.getCustomerList();
} catch (error) {
this.$message.error('录入失败:' + (error.msg || '服务器异常'));
} finally {
this.buttonLoading = false;
}
}
});
},
/** 取消按钮操作 */
cancel() {
this.reset();
this.open = false;
},
/** 处理删除(补充实现) */
handleDelete(item) {
this.$confirm('确定删除该客户吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
// 补充删除接口调用逻辑
await delCustomer(item.customerId);
this.$message.success('删除成功');
this.getCustomerList();
}).catch(() => {
this.$message.info('已取消删除');
});
}
},
}
</script>
<style scoped>
.app-container {
padding: 16px;
height: 100%;
box-sizing: border-box;
}
.dialog-footer {
text-align: center;
}
</style>