feat: 新增客户管理全模块功能
实现客户信息、客户部门、客户账号三个子模块的完整CRUD,包含: 1. 数据库实体、DTO、控制器、服务层代码 2. RBAC权限映射与菜单种子数据扩展 3. 前端路由与API封装 4. 数据权限隔离与鉴权逻辑 5. 配套工具脚本与测试验证清单
This commit is contained in:
1 parent
026ec34471
commit
1c80a24d1d
31 files changed
+26321
-653
No files matched your search
@@ -0,0 +1,92 @@
|
||||
import request from '@/utils/request';
|
||||
|
||||
export interface CustomerInfo {
|
||||
id: number;
|
||||
customerNo: string;
|
||||
customerName: string;
|
||||
province?: string;
|
||||
city?: string;
|
||||
district?: string;
|
||||
address?: string;
|
||||
contactPerson?: string;
|
||||
contactPhone?: string;
|
||||
postalCode?: string;
|
||||
fax?: string;
|
||||
customerLevel?: string;
|
||||
status: number;
|
||||
salesmanId?: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface CustomerQuery {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
keyword?: string;
|
||||
status?: number;
|
||||
salesmanId?: number;
|
||||
}
|
||||
|
||||
export interface CustomerListResult {
|
||||
list: CustomerInfo[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
export interface CustomerCreateParams {
|
||||
customerName: string;
|
||||
province?: string;
|
||||
city?: string;
|
||||
district?: string;
|
||||
address?: string;
|
||||
contactPerson?: string;
|
||||
contactPhone?: string;
|
||||
postalCode?: string;
|
||||
fax?: string;
|
||||
customerLevel?: string;
|
||||
status?: number;
|
||||
salesmanId?: number;
|
||||
}
|
||||
|
||||
export interface CustomerUpdateParams {
|
||||
customerName?: string;
|
||||
province?: string;
|
||||
city?: string;
|
||||
district?: string;
|
||||
address?: string;
|
||||
contactPerson?: string;
|
||||
contactPhone?: string;
|
||||
postalCode?: string;
|
||||
fax?: string;
|
||||
customerLevel?: string;
|
||||
status?: number;
|
||||
salesmanId?: number;
|
||||
}
|
||||
|
||||
export interface CustomerStatusParams {
|
||||
status: number;
|
||||
}
|
||||
|
||||
export const customerApi = {
|
||||
getCustomerList: (params: CustomerQuery) =>
|
||||
request.get<CustomerListResult>('/customers', { params }),
|
||||
|
||||
getCustomerDetail: (id: number) =>
|
||||
request.get<CustomerInfo>(`/customers/${id}`),
|
||||
|
||||
getCustomerOptions: () =>
|
||||
request.get<CustomerInfo[]>('/customers/options/all'),
|
||||
|
||||
createCustomer: (data: CustomerCreateParams) =>
|
||||
request.post('/customers', data),
|
||||
|
||||
updateCustomer: (id: number, data: CustomerUpdateParams) =>
|
||||
request.put(`/customers/${id}`, data),
|
||||
|
||||
deleteCustomer: (id: number) =>
|
||||
request.delete(`/customers/${id}`),
|
||||
|
||||
updateCustomerStatus: (id: number, status: number) =>
|
||||
request.put(`/customers/${id}/status`, { status }),
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
import request from '@/utils/request';
|
||||
|
||||
export interface CustomerAccountInfo {
|
||||
id: number;
|
||||
customerId: number;
|
||||
phone: string;
|
||||
status: number;
|
||||
lastLoginAt?: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface AccountQuery {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
keyword?: string;
|
||||
customerId?: number;
|
||||
status?: number;
|
||||
}
|
||||
|
||||
export interface AccountListResult {
|
||||
list: CustomerAccountInfo[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
export interface AccountCreateParams {
|
||||
customerId: number;
|
||||
phone: string;
|
||||
status?: number;
|
||||
}
|
||||
|
||||
export interface AccountUpdateParams {
|
||||
phone?: string;
|
||||
status?: number;
|
||||
resetPassword?: boolean;
|
||||
}
|
||||
|
||||
export interface AccountStatusParams {
|
||||
status: number;
|
||||
}
|
||||
|
||||
export const customerAccountApi = {
|
||||
getAccountList: (params: AccountQuery) =>
|
||||
request.get<AccountListResult>('/customer-accounts', { params }),
|
||||
|
||||
getAccountDetail: (id: number) =>
|
||||
request.get<CustomerAccountInfo>(`/customer-accounts/${id}`),
|
||||
|
||||
createAccount: (data: AccountCreateParams) =>
|
||||
request.post('/customer-accounts', data),
|
||||
|
||||
updateAccount: (id: number, data: AccountUpdateParams) =>
|
||||
request.put(`/customer-accounts/${id}`, data),
|
||||
|
||||
deleteAccount: (id: number) =>
|
||||
request.delete(`/customer-accounts/${id}`),
|
||||
|
||||
updateAccountStatus: (id: number, status: number) =>
|
||||
request.put(`/customer-accounts/${id}/status`, { status }),
|
||||
|
||||
resetAccountPassword: (id: number) =>
|
||||
request.post(`/customer-accounts/${id}/reset-password`),
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
import request from '@/utils/request';
|
||||
|
||||
export interface CustomerDepartmentInfo {
|
||||
id: number;
|
||||
customerId: number;
|
||||
deptName: string;
|
||||
contactPerson?: string;
|
||||
contactPhone?: string;
|
||||
functionDesc?: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface DepartmentQuery {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
keyword?: string;
|
||||
customerId?: number;
|
||||
}
|
||||
|
||||
export interface DepartmentListResult {
|
||||
list: CustomerDepartmentInfo[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
export interface DepartmentCreateParams {
|
||||
customerId: number;
|
||||
deptName: string;
|
||||
contactPerson?: string;
|
||||
contactPhone?: string;
|
||||
functionDesc?: string;
|
||||
}
|
||||
|
||||
export interface DepartmentUpdateParams {
|
||||
customerId?: number;
|
||||
deptName?: string;
|
||||
contactPerson?: string;
|
||||
contactPhone?: string;
|
||||
functionDesc?: string;
|
||||
}
|
||||
|
||||
export const customerDepartmentApi = {
|
||||
getDepartmentList: (params: DepartmentQuery) =>
|
||||
request.get<DepartmentListResult>('/customer-departments', { params }),
|
||||
|
||||
getDepartmentDetail: (id: number) =>
|
||||
request.get<CustomerDepartmentInfo>(`/customer-departments/${id}`),
|
||||
|
||||
createDepartment: (data: DepartmentCreateParams) =>
|
||||
request.post('/customer-departments', data),
|
||||
|
||||
updateDepartment: (id: number, data: DepartmentUpdateParams) =>
|
||||
request.put(`/customer-departments/${id}`, data),
|
||||
|
||||
deleteDepartment: (id: number) =>
|
||||
request.delete(`/customer-departments/${id}`),
|
||||
};
|
||||
File diff suppressed because it is too large.
Load diff
@@ -30,12 +30,24 @@ const routes: RouteRecordRaw[] = [
|
||||
title: "仪表盘"
|
||||
}
|
||||
},
|
||||
// 业务模块(未开发,先使用占位页;菜单 component 落地后替换为真实组件)
|
||||
// 业务模块
|
||||
{
|
||||
path: "customer",
|
||||
name: "Customer",
|
||||
component: () => import("@/views/Placeholder.vue"),
|
||||
meta: { requiresAuth: true, title: "客户管理", permission: "customer:list" }
|
||||
path: "customer/info",
|
||||
name: "CustomerInfo",
|
||||
component: () => import("@/views/customer/info/index.vue"),
|
||||
meta: { requiresAuth: true, title: "客户信息", permission: "customer:list" }
|
||||
},
|
||||
{
|
||||
path: "customer/department",
|
||||
name: "CustomerDept",
|
||||
component: () => import("@/views/customer/department/index.vue"),
|
||||
meta: { requiresAuth: true, title: "客户部门", permission: "customer:department" }
|
||||
},
|
||||
{
|
||||
path: "customer/account",
|
||||
name: "CustomerAccount",
|
||||
component: () => import("@/views/customer/account/index.vue"),
|
||||
meta: { requiresAuth: true, title: "客户账号", permission: "customer:account" }
|
||||
},
|
||||
{
|
||||
path: "project",
|
||||
|
||||
@@ -0,0 +1,459 @@
|
||||
<template>
|
||||
<div class="customer-account">
|
||||
<a-card title="客户账号管理">
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="handleAdd">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新增账号
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<a-alert
|
||||
type="info"
|
||||
message="初始密码规则:默认密码为手机号后 6 位,如手机号不足 6 位则为 123456;编辑时可勾选重置为默认密码"
|
||||
style="margin-bottom: 16px"
|
||||
/>
|
||||
|
||||
<div class="search-bar">
|
||||
<a-input
|
||||
v-model:value="searchForm.keyword"
|
||||
placeholder="登录手机号"
|
||||
allow-clear
|
||||
style="width: 220px"
|
||||
@pressEnter="handleSearch"
|
||||
>
|
||||
<template #prefix><SearchOutlined /></template>
|
||||
</a-input>
|
||||
<a-select
|
||||
v-model:value="searchForm.customerId"
|
||||
placeholder="所属客户"
|
||||
allow-clear
|
||||
style="width: 220px"
|
||||
:options="customerOptions"
|
||||
/>
|
||||
<a-select v-model:value="searchForm.status" placeholder="状态" allow-clear style="width: 120px">
|
||||
<a-select-option :value="1">启用</a-select-option>
|
||||
<a-select-option :value="0">禁用</a-select-option>
|
||||
</a-select>
|
||||
<a-button type="primary" @click="handleSearch">
|
||||
<template #icon><SearchOutlined /></template>
|
||||
查询
|
||||
</a-button>
|
||||
<a-button @click="handleReset">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
重置
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<div class="table-wrapper" ref="wrapperRef">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="dataSource"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
row-key="id"
|
||||
:scroll="{ x: 'max-content', y: tableHeight }"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'customerId'">
|
||||
{{ customerNameMap[record.customerId] || '-' }}
|
||||
</template>
|
||||
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-switch
|
||||
:checked="record.status === 1"
|
||||
checked-children="启用"
|
||||
un-checked-children="禁用"
|
||||
@change="(checked: boolean) => handleStatusChange(record.id, checked ? 1 : 0)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-if="column.key === 'createdAt'">
|
||||
{{ formatDateTime(record.createdAt) }}
|
||||
</template>
|
||||
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleEdit(record)">
|
||||
<template #icon><EditOutlined /></template>
|
||||
编辑
|
||||
</a-button>
|
||||
<a-popconfirm title="确定要重置该账号的密码吗?" ok-text="确定" cancel-text="取消" @confirm="handleResetPassword(record.id)">
|
||||
<a-button type="link" size="small">
|
||||
<template #icon><KeyOutlined /></template>
|
||||
重置密码
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
<a-popconfirm title="确定要删除该账号吗?" ok-text="确定" cancel-text="取消" @confirm="handleDelete(record.id)">
|
||||
<a-button type="link" size="small" danger>
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template #emptyText>
|
||||
<a-empty description="暂无账号数据" />
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
:title="modalTitle"
|
||||
:confirm-loading="modalLoading"
|
||||
width="600px"
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 16 }"
|
||||
>
|
||||
<a-form-item label="所属客户" name="customerId">
|
||||
<a-select
|
||||
v-model:value="formData.customerId"
|
||||
placeholder="请选择所属客户"
|
||||
:options="customerOptions"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="登录电话" name="phone">
|
||||
<a-input v-model:value="formData.phone" placeholder="请输入登录电话" maxlength="20" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-radio-group v-model:value="formData.status">
|
||||
<a-radio :value="1">启用</a-radio>
|
||||
<a-radio :value="0">禁用</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item v-if="isEdit">
|
||||
<a-checkbox v-model:checked="formData.resetPassword">重置为默认密码(手机号后 6 位)</a-checkbox>
|
||||
<div style="font-size: 12px; color: #999; margin-top: 4px; padding-left: 24px">
|
||||
勾选后保存即重置,下次登录请用新密码
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import {
|
||||
SearchOutlined,
|
||||
ReloadOutlined,
|
||||
PlusOutlined,
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
KeyOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
import { customerApi, type CustomerInfo } from '@/api/customer';
|
||||
import { customerAccountApi, type CustomerAccountInfo, type AccountQuery } from '@/api/customerAccount';
|
||||
import { useTableScroll } from '@/composables/useTableScroll';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const { wrapperRef, tableHeight } = useTableScroll();
|
||||
|
||||
const loading = ref(false);
|
||||
const dataSource = ref<CustomerAccountInfo[]>([]);
|
||||
const total = ref(0);
|
||||
const searchForm = reactive<AccountQuery>({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
keyword: '',
|
||||
customerId: undefined as any,
|
||||
status: undefined as any,
|
||||
});
|
||||
const modalVisible = ref(false);
|
||||
const modalTitle = ref('');
|
||||
const modalLoading = ref(false);
|
||||
const formRef = ref();
|
||||
const isEdit = ref(false);
|
||||
const editId = ref<number | null>(null);
|
||||
const customerOptions = ref<{ label: string; value: number }[]>([]);
|
||||
const customerNameMap = reactive<Record<number, string>>({});
|
||||
|
||||
const formData = reactive({
|
||||
customerId: undefined as number | undefined,
|
||||
phone: '',
|
||||
status: 1,
|
||||
resetPassword: false,
|
||||
});
|
||||
|
||||
const formRules = {
|
||||
customerId: [{ required: true, message: '请选择所属客户', trigger: 'change' }],
|
||||
phone: [
|
||||
{ required: true, message: '请输入登录电话', trigger: 'blur' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' },
|
||||
],
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{ title: '所属客户', dataIndex: 'customerId', key: 'customerId', ellipsis: true, width: 180 },
|
||||
{ title: '登录电话', dataIndex: 'phone', key: 'phone', ellipsis: true, width: 140 },
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
align: 'center' as const,
|
||||
width: 100,
|
||||
},
|
||||
{ title: '创建时间', dataIndex: 'createdAt', key: 'createdAt', ellipsis: true, width: 180 },
|
||||
{ title: '操作', key: 'action', width: 260, fixed: 'right' as const },
|
||||
];
|
||||
|
||||
const pagination = computed(() => ({
|
||||
current: searchForm.page,
|
||||
pageSize: searchForm.pageSize,
|
||||
total: total.value,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (t: number) => `共 ${t} 条数据`,
|
||||
}));
|
||||
|
||||
onMounted(() => {
|
||||
fetchList();
|
||||
fetchCustomerOptions();
|
||||
});
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res: any = await customerAccountApi.getAccountList(searchForm);
|
||||
if (res.code === 200) {
|
||||
dataSource.value = res.data.list;
|
||||
total.value = res.data.total;
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('获取账号列表失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchCustomerOptions() {
|
||||
try {
|
||||
const res: any = await customerApi.getCustomerOptions();
|
||||
if (res.code === 200) {
|
||||
const list: CustomerInfo[] = res.data;
|
||||
customerOptions.value = list.map((item) => ({
|
||||
label: `${item.customerNo} ${item.customerName}`,
|
||||
value: item.id,
|
||||
}));
|
||||
list.forEach((item) => {
|
||||
customerNameMap[item.id] = `${item.customerNo} ${item.customerName}`;
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取客户选项失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
searchForm.page = 1;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.keyword = '';
|
||||
searchForm.customerId = undefined as any;
|
||||
searchForm.status = undefined as any;
|
||||
searchForm.page = 1;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleTableChange(pag: any) {
|
||||
searchForm.page = pag.current;
|
||||
searchForm.pageSize = pag.pageSize;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
isEdit.value = false;
|
||||
editId.value = null;
|
||||
modalTitle.value = '新增账号';
|
||||
resetForm();
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function handleEdit(record: CustomerAccountInfo) {
|
||||
isEdit.value = true;
|
||||
editId.value = record.id;
|
||||
modalTitle.value = '编辑账号';
|
||||
resetForm();
|
||||
|
||||
formData.customerId = record.customerId;
|
||||
formData.phone = record.phone;
|
||||
formData.status = record.status;
|
||||
formData.resetPassword = false;
|
||||
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
formData.customerId = undefined;
|
||||
formData.phone = '';
|
||||
formData.status = 1;
|
||||
formData.resetPassword = false;
|
||||
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
modalLoading.value = true;
|
||||
try {
|
||||
let res: any;
|
||||
if (isEdit.value && editId.value) {
|
||||
const updateData: any = {
|
||||
phone: formData.phone,
|
||||
status: formData.status,
|
||||
};
|
||||
if (formData.resetPassword) {
|
||||
updateData.resetPassword = true;
|
||||
}
|
||||
res = await customerAccountApi.updateAccount(editId.value, updateData);
|
||||
} else {
|
||||
res = await customerAccountApi.createAccount({
|
||||
customerId: formData.customerId as number,
|
||||
phone: formData.phone,
|
||||
status: formData.status,
|
||||
});
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
message.success(isEdit.value ? '更新成功' : '创建成功');
|
||||
modalVisible.value = false;
|
||||
searchForm.page = 1;
|
||||
fetchList();
|
||||
} else {
|
||||
message.error(res.message || '操作失败');
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('操作失败');
|
||||
} finally {
|
||||
modalLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
modalVisible.value = false;
|
||||
resetForm();
|
||||
}
|
||||
|
||||
async function handleDelete(id: number) {
|
||||
try {
|
||||
const res: any = await customerAccountApi.deleteAccount(id);
|
||||
if (res.code === 200) {
|
||||
message.success('删除成功');
|
||||
searchForm.page = 1;
|
||||
fetchList();
|
||||
} else {
|
||||
message.error(res.message || '删除失败');
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('删除失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleStatusChange(id: number, status: number) {
|
||||
try {
|
||||
const res: any = await customerAccountApi.updateAccountStatus(id, status);
|
||||
if (res.code === 200) {
|
||||
message.success(status === 1 ? '已启用' : '已禁用');
|
||||
fetchList();
|
||||
} else {
|
||||
message.error(res.message || '状态更新失败');
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('状态更新失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleResetPassword(id: number) {
|
||||
try {
|
||||
const res: any = await customerAccountApi.resetAccountPassword(id);
|
||||
if (res.code === 200) {
|
||||
message.success('密码已重置为手机号后6位');
|
||||
} else {
|
||||
message.error(res.message || '密码重置失败');
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('密码重置失败');
|
||||
}
|
||||
}
|
||||
|
||||
function formatDateTime(dateStr: string) {
|
||||
if (!dateStr) return '-';
|
||||
return dayjs(dateStr).format('YYYY-MM-DD HH:mm:ss');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.customer-account {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:deep(.ant-card) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
:deep(.ant-card-body) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
flex-shrink: 0;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:deep(.ant-table) {
|
||||
font-size: 14px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.ant-btn-link) {
|
||||
padding: 0 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,401 @@
|
||||
<template>
|
||||
<div class="customer-department">
|
||||
<a-card title="部门管理">
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="handleAdd">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新增部门
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<div class="search-bar">
|
||||
<a-input
|
||||
v-model:value="searchForm.keyword"
|
||||
placeholder="部门名称/联系人/电话"
|
||||
allow-clear
|
||||
style="width: 220px"
|
||||
@pressEnter="handleSearch"
|
||||
>
|
||||
<template #prefix><SearchOutlined /></template>
|
||||
</a-input>
|
||||
<a-select
|
||||
v-model:value="searchForm.customerId"
|
||||
placeholder="所属客户"
|
||||
allow-clear
|
||||
style="width: 220px"
|
||||
:options="customerOptions"
|
||||
/>
|
||||
<a-button type="primary" @click="handleSearch">
|
||||
<template #icon><SearchOutlined /></template>
|
||||
查询
|
||||
</a-button>
|
||||
<a-button @click="handleReset">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
重置
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<div class="table-wrapper" ref="wrapperRef">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="dataSource"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
row-key="id"
|
||||
:scroll="{ x: 'max-content', y: tableHeight }"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'customerId'">
|
||||
{{ customerNameMap[record.customerId] || '-' }}
|
||||
</template>
|
||||
|
||||
<template v-if="column.key === 'createdAt'">
|
||||
{{ formatDateTime(record.createdAt) }}
|
||||
</template>
|
||||
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleEdit(record)">
|
||||
<template #icon><EditOutlined /></template>
|
||||
编辑
|
||||
</a-button>
|
||||
<a-popconfirm title="确定要删除该部门吗?" ok-text="确定" cancel-text="取消" @confirm="handleDelete(record.id)">
|
||||
<a-button type="link" size="small" danger>
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template #emptyText>
|
||||
<a-empty description="暂无部门数据" />
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
:title="modalTitle"
|
||||
:confirm-loading="modalLoading"
|
||||
width="600px"
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 16 }"
|
||||
>
|
||||
<a-form-item label="所属客户" name="customerId">
|
||||
<a-select
|
||||
v-model:value="formData.customerId"
|
||||
placeholder="请选择所属客户"
|
||||
:options="customerOptions"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="部门名称" name="deptName">
|
||||
<a-input v-model:value="formData.deptName" placeholder="请输入部门名称" maxlength="64" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="联系人" name="contactPerson">
|
||||
<a-input v-model:value="formData.contactPerson" placeholder="请输入联系人" maxlength="32" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="联系电话" name="contactPhone">
|
||||
<a-input v-model:value="formData.contactPhone" placeholder="请输入联系电话" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="职能描述" name="functionDesc">
|
||||
<a-textarea v-model:value="formData.functionDesc" placeholder="请输入职能描述" :rows="3" maxlength="256" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import {
|
||||
SearchOutlined,
|
||||
ReloadOutlined,
|
||||
PlusOutlined,
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
import { customerApi, type CustomerInfo } from '@/api/customer';
|
||||
import { customerDepartmentApi, type CustomerDepartmentInfo, type DepartmentQuery } from '@/api/customerDepartment';
|
||||
import { useTableScroll } from '@/composables/useTableScroll';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const { wrapperRef, tableHeight } = useTableScroll();
|
||||
|
||||
const loading = ref(false);
|
||||
const dataSource = ref<CustomerDepartmentInfo[]>([]);
|
||||
const total = ref(0);
|
||||
const searchForm = reactive<DepartmentQuery>({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
keyword: '',
|
||||
customerId: undefined as any,
|
||||
});
|
||||
const modalVisible = ref(false);
|
||||
const modalTitle = ref('');
|
||||
const modalLoading = ref(false);
|
||||
const formRef = ref();
|
||||
const isEdit = ref(false);
|
||||
const editId = ref<number | null>(null);
|
||||
const customerOptions = ref<{ label: string; value: number }[]>([]);
|
||||
const customerNameMap = reactive<Record<number, string>>({});
|
||||
|
||||
const formData = reactive({
|
||||
customerId: undefined as number | undefined,
|
||||
deptName: '',
|
||||
contactPerson: '',
|
||||
contactPhone: '',
|
||||
functionDesc: '',
|
||||
});
|
||||
|
||||
const formRules = {
|
||||
customerId: [{ required: true, message: '请选择所属客户', trigger: 'change' }],
|
||||
deptName: [{ required: true, message: '请输入部门名称', trigger: 'blur' }],
|
||||
contactPhone: [{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' }],
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{ title: '所属客户', dataIndex: 'customerId', key: 'customerId', ellipsis: true, width: 180 },
|
||||
{ title: '部门名称', dataIndex: 'deptName', key: 'deptName', ellipsis: true },
|
||||
{ title: '联系人', dataIndex: 'contactPerson', key: 'contactPerson', ellipsis: true, width: 120 },
|
||||
{ title: '联系电话', dataIndex: 'contactPhone', key: 'contactPhone', ellipsis: true, width: 140 },
|
||||
{ title: '职能描述', dataIndex: 'functionDesc', key: 'functionDesc', ellipsis: true },
|
||||
{ title: '创建时间', dataIndex: 'createdAt', key: 'createdAt', ellipsis: true, width: 180 },
|
||||
{ title: '操作', key: 'action', width: 160, fixed: 'right' as const },
|
||||
];
|
||||
|
||||
const pagination = computed(() => ({
|
||||
current: searchForm.page,
|
||||
pageSize: searchForm.pageSize,
|
||||
total: total.value,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (t: number) => `共 ${t} 条数据`,
|
||||
}));
|
||||
|
||||
onMounted(() => {
|
||||
fetchList();
|
||||
fetchCustomerOptions();
|
||||
});
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res: any = await customerDepartmentApi.getDepartmentList(searchForm);
|
||||
if (res.code === 200) {
|
||||
dataSource.value = res.data.list;
|
||||
total.value = res.data.total;
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('获取部门列表失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchCustomerOptions() {
|
||||
try {
|
||||
const res: any = await customerApi.getCustomerOptions();
|
||||
if (res.code === 200) {
|
||||
const list: CustomerInfo[] = res.data;
|
||||
customerOptions.value = list.map((item) => ({
|
||||
label: `${item.customerNo} ${item.customerName}`,
|
||||
value: item.id,
|
||||
}));
|
||||
list.forEach((item) => {
|
||||
customerNameMap[item.id] = `${item.customerNo} ${item.customerName}`;
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取客户选项失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
searchForm.page = 1;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.keyword = '';
|
||||
searchForm.customerId = undefined as any;
|
||||
searchForm.page = 1;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleTableChange(pag: any) {
|
||||
searchForm.page = pag.current;
|
||||
searchForm.pageSize = pag.pageSize;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
isEdit.value = false;
|
||||
editId.value = null;
|
||||
modalTitle.value = '新增部门';
|
||||
resetForm();
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function handleEdit(record: CustomerDepartmentInfo) {
|
||||
isEdit.value = true;
|
||||
editId.value = record.id;
|
||||
modalTitle.value = '编辑部门';
|
||||
resetForm();
|
||||
|
||||
formData.customerId = record.customerId;
|
||||
formData.deptName = record.deptName;
|
||||
formData.contactPerson = record.contactPerson || '';
|
||||
formData.contactPhone = record.contactPhone || '';
|
||||
formData.functionDesc = record.functionDesc || '';
|
||||
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
formData.customerId = undefined;
|
||||
formData.deptName = '';
|
||||
formData.contactPerson = '';
|
||||
formData.contactPhone = '';
|
||||
formData.functionDesc = '';
|
||||
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
modalLoading.value = true;
|
||||
try {
|
||||
let res: any;
|
||||
if (isEdit.value && editId.value) {
|
||||
res = await customerDepartmentApi.updateDepartment(editId.value, {
|
||||
customerId: formData.customerId,
|
||||
deptName: formData.deptName,
|
||||
contactPerson: formData.contactPerson,
|
||||
contactPhone: formData.contactPhone,
|
||||
functionDesc: formData.functionDesc,
|
||||
});
|
||||
} else {
|
||||
res = await customerDepartmentApi.createDepartment({
|
||||
customerId: formData.customerId as number,
|
||||
deptName: formData.deptName,
|
||||
contactPerson: formData.contactPerson,
|
||||
contactPhone: formData.contactPhone,
|
||||
functionDesc: formData.functionDesc,
|
||||
});
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
message.success(isEdit.value ? '更新成功' : '创建成功');
|
||||
modalVisible.value = false;
|
||||
searchForm.page = 1;
|
||||
fetchList();
|
||||
} else {
|
||||
message.error(res.message || '操作失败');
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('操作失败');
|
||||
} finally {
|
||||
modalLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
modalVisible.value = false;
|
||||
resetForm();
|
||||
}
|
||||
|
||||
async function handleDelete(id: number) {
|
||||
try {
|
||||
const res: any = await customerDepartmentApi.deleteDepartment(id);
|
||||
if (res.code === 200) {
|
||||
message.success('删除成功');
|
||||
searchForm.page = 1;
|
||||
fetchList();
|
||||
} else {
|
||||
message.error(res.message || '删除失败');
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('删除失败');
|
||||
}
|
||||
}
|
||||
|
||||
function formatDateTime(dateStr: string) {
|
||||
if (!dateStr) return '-';
|
||||
return dayjs(dateStr).format('YYYY-MM-DD HH:mm:ss');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.customer-department {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:deep(.ant-card) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
:deep(.ant-card-body) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
flex-shrink: 0;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:deep(.ant-table) {
|
||||
font-size: 14px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.ant-btn-link) {
|
||||
padding: 0 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,496 @@
|
||||
<template>
|
||||
<div class="customer-management">
|
||||
<a-card title="客户信息管理">
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="handleAdd">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新增客户
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<div class="search-bar">
|
||||
<a-input
|
||||
v-model:value="searchForm.keyword"
|
||||
placeholder="客户名称/联系人/电话"
|
||||
allow-clear
|
||||
style="width: 240px"
|
||||
@pressEnter="handleSearch"
|
||||
>
|
||||
<template #prefix><SearchOutlined /></template>
|
||||
</a-input>
|
||||
<a-select v-model:value="searchForm.status" placeholder="客户状态" allow-clear style="width: 140px">
|
||||
<a-select-option :value="1">意向</a-select-option>
|
||||
<a-select-option :value="2">签约</a-select-option>
|
||||
<a-select-option :value="3">运维中</a-select-option>
|
||||
<a-select-option :value="4">终止</a-select-option>
|
||||
</a-select>
|
||||
<a-select
|
||||
v-if="isAdmin"
|
||||
v-model:value="searchForm.salesmanId"
|
||||
placeholder="负责业务员"
|
||||
allow-clear
|
||||
style="width: 160px"
|
||||
:options="salesmanOptions"
|
||||
/>
|
||||
<a-button type="primary" @click="handleSearch">
|
||||
<template #icon><SearchOutlined /></template>
|
||||
查询
|
||||
</a-button>
|
||||
<a-button @click="handleReset">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
重置
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<div class="table-wrapper" ref="wrapperRef">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="dataSource"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
row-key="id"
|
||||
:scroll="{ x: 'max-content', y: tableHeight }"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'region'">
|
||||
{{ [record.province, record.city, record.district].filter(Boolean).join(' / ') }}
|
||||
</template>
|
||||
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="getStatusColor(record.status)">
|
||||
{{ getStatusText(record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<template v-if="column.key === 'createdAt'">
|
||||
{{ formatDateTime(record.createdAt) }}
|
||||
</template>
|
||||
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleEdit(record)">
|
||||
<template #icon><EditOutlined /></template>
|
||||
编辑
|
||||
</a-button>
|
||||
<a-popconfirm title="确定要删除该客户吗?" ok-text="确定" cancel-text="取消" @confirm="handleDelete(record.id)">
|
||||
<a-button type="link" size="small" danger>
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template #emptyText>
|
||||
<a-empty description="暂无客户数据" />
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
:title="modalTitle"
|
||||
:confirm-loading="modalLoading"
|
||||
width="680px"
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 16 }"
|
||||
>
|
||||
<a-form-item label="客户名称" name="customerName">
|
||||
<a-input v-model:value="formData.customerName" placeholder="请输入客户名称" maxlength="128" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="省市区" name="region">
|
||||
<a-cascader
|
||||
v-model:value="formData.region"
|
||||
:options="regionOptions"
|
||||
placeholder="请选择省市区"
|
||||
change-on-select
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="详细地址" name="address">
|
||||
<a-input v-model:value="formData.address" placeholder="请输入详细地址" maxlength="256" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="联系人" name="contactPerson">
|
||||
<a-input v-model:value="formData.contactPerson" placeholder="请输入联系人" maxlength="32" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="联系电话" name="contactPhone">
|
||||
<a-input v-model:value="formData.contactPhone" placeholder="请输入联系电话" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="邮编" name="postalCode">
|
||||
<a-input v-model:value="formData.postalCode" placeholder="请输入邮编" maxlength="10" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="传真" name="fax">
|
||||
<a-input v-model:value="formData.fax" placeholder="请输入传真" maxlength="32" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="客户级别" name="customerLevel">
|
||||
<a-input v-model:value="formData.customerLevel" placeholder="请输入客户级别" maxlength="32" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-radio-group v-model:value="formData.status">
|
||||
<a-radio :value="1">意向</a-radio>
|
||||
<a-radio :value="2">签约</a-radio>
|
||||
<a-radio :value="3">运维中</a-radio>
|
||||
<a-radio :value="4">终止</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item v-if="isAdmin" label="负责业务员" name="salesmanId">
|
||||
<a-select
|
||||
v-model:value="formData.salesmanId"
|
||||
placeholder="请选择负责业务员"
|
||||
:options="salesmanOptions"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import {
|
||||
SearchOutlined,
|
||||
ReloadOutlined,
|
||||
PlusOutlined,
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
import { customerApi, type CustomerInfo, type CustomerQuery } from '@/api/customer';
|
||||
import { userApi } from '@/api/user';
|
||||
import { useTableScroll } from '@/composables/useTableScroll';
|
||||
import { usePermissionStore } from '@/stores/permission';
|
||||
import { regionOptions } from '@/assets/data/region';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const { wrapperRef, tableHeight } = useTableScroll();
|
||||
const permissionStore = usePermissionStore();
|
||||
|
||||
const loading = ref(false);
|
||||
const dataSource = ref<CustomerInfo[]>([]);
|
||||
const total = ref(0);
|
||||
const searchForm = reactive<CustomerQuery>({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
keyword: '',
|
||||
status: undefined as any,
|
||||
salesmanId: undefined as any,
|
||||
});
|
||||
const modalVisible = ref(false);
|
||||
const modalTitle = ref('');
|
||||
const modalLoading = ref(false);
|
||||
const formRef = ref();
|
||||
const isEdit = ref(false);
|
||||
const editId = ref<number | null>(null);
|
||||
const salesmanOptions = ref<{ label: string; value: number }[]>([]);
|
||||
|
||||
const isAdmin = computed(() => permissionStore.hasRole('ADMIN'));
|
||||
|
||||
const formData = reactive({
|
||||
customerName: '',
|
||||
region: [] as string[],
|
||||
address: '',
|
||||
contactPerson: '',
|
||||
contactPhone: '',
|
||||
postalCode: '',
|
||||
fax: '',
|
||||
customerLevel: '',
|
||||
status: 1,
|
||||
salesmanId: undefined as number | undefined,
|
||||
});
|
||||
|
||||
const formRules = {
|
||||
customerName: [{ required: true, message: '请输入客户名称', trigger: 'blur' }],
|
||||
region: [{ required: true, message: '请选择省市区', trigger: 'change', type: 'array' as const }],
|
||||
contactPhone: [{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' }],
|
||||
status: [{ required: true, message: '请选择状态', trigger: 'change' }],
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{ title: '客户编号', dataIndex: 'customerNo', key: 'customerNo', ellipsis: true, width: 140 },
|
||||
{ title: '客户名称', dataIndex: 'customerName', key: 'customerName', ellipsis: true },
|
||||
{ title: '省市区', key: 'region' },
|
||||
{ title: '详细地址', dataIndex: 'address', key: 'address', ellipsis: true },
|
||||
{ title: '联系人', dataIndex: 'contactPerson', key: 'contactPerson' },
|
||||
{ title: '联系电话', dataIndex: 'contactPhone', key: 'contactPhone', ellipsis: true },
|
||||
{ title: '客户级别', dataIndex: 'customerLevel', key: 'customerLevel', ellipsis: true },
|
||||
{ title: '状态', key: 'status', align: 'center' as const, width: 100 },
|
||||
{ title: '创建时间', dataIndex: 'createdAt', key: 'createdAt', ellipsis: true },
|
||||
{ title: '操作', key: 'action', width: 160, fixed: 'right' as const },
|
||||
];
|
||||
|
||||
const pagination = computed(() => ({
|
||||
current: searchForm.page,
|
||||
pageSize: searchForm.pageSize,
|
||||
total: total.value,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (t: number) => `共 ${t} 条数据`,
|
||||
}));
|
||||
|
||||
onMounted(() => {
|
||||
fetchList();
|
||||
if (isAdmin.value) {
|
||||
fetchSalesmanList();
|
||||
}
|
||||
});
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res: any = await customerApi.getCustomerList(searchForm);
|
||||
if (res.code === 200) {
|
||||
dataSource.value = res.data.list;
|
||||
total.value = res.data.total;
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('获取客户列表失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchSalesmanList() {
|
||||
try {
|
||||
const res: any = await userApi.getUserList({ page: 1, pageSize: 999 });
|
||||
if (res.code === 200) {
|
||||
salesmanOptions.value = res.data.list.map((item: any) => ({
|
||||
label: item.realName,
|
||||
value: item.id,
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取业务员列表失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
searchForm.page = 1;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.keyword = '';
|
||||
searchForm.status = undefined as any;
|
||||
searchForm.salesmanId = undefined as any;
|
||||
searchForm.page = 1;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleTableChange(pag: any) {
|
||||
searchForm.page = pag.current;
|
||||
searchForm.pageSize = pag.pageSize;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
isEdit.value = false;
|
||||
editId.value = null;
|
||||
modalTitle.value = '新增客户';
|
||||
resetForm();
|
||||
formData.status = 1;
|
||||
formData.region = [];
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function handleEdit(record: CustomerInfo) {
|
||||
isEdit.value = true;
|
||||
editId.value = record.id;
|
||||
modalTitle.value = '编辑客户';
|
||||
resetForm();
|
||||
|
||||
formData.customerName = record.customerName;
|
||||
formData.region = [record.province, record.city, record.district].filter(Boolean) as string[];
|
||||
formData.address = record.address || '';
|
||||
formData.contactPerson = record.contactPerson || '';
|
||||
formData.contactPhone = record.contactPhone || '';
|
||||
formData.postalCode = record.postalCode || '';
|
||||
formData.fax = record.fax || '';
|
||||
formData.customerLevel = record.customerLevel || '';
|
||||
formData.status = record.status;
|
||||
formData.salesmanId = record.salesmanId;
|
||||
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
formData.customerName = '';
|
||||
formData.region = [];
|
||||
formData.address = '';
|
||||
formData.contactPerson = '';
|
||||
formData.contactPhone = '';
|
||||
formData.postalCode = '';
|
||||
formData.fax = '';
|
||||
formData.customerLevel = '';
|
||||
formData.status = 1;
|
||||
formData.salesmanId = undefined;
|
||||
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
modalLoading.value = true;
|
||||
try {
|
||||
const submitData: any = {
|
||||
customerName: formData.customerName,
|
||||
province: formData.region[0] || undefined,
|
||||
city: formData.region[1] || undefined,
|
||||
district: formData.region[2] || undefined,
|
||||
address: formData.address,
|
||||
contactPerson: formData.contactPerson,
|
||||
contactPhone: formData.contactPhone,
|
||||
postalCode: formData.postalCode,
|
||||
fax: formData.fax,
|
||||
customerLevel: formData.customerLevel,
|
||||
status: formData.status,
|
||||
};
|
||||
|
||||
if (isAdmin.value && formData.salesmanId !== undefined) {
|
||||
submitData.salesmanId = formData.salesmanId;
|
||||
}
|
||||
|
||||
let res: any;
|
||||
if (isEdit.value && editId.value) {
|
||||
res = await customerApi.updateCustomer(editId.value, submitData);
|
||||
} else {
|
||||
res = await customerApi.createCustomer(submitData);
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
message.success(isEdit.value ? '更新成功' : '创建成功');
|
||||
modalVisible.value = false;
|
||||
searchForm.page = 1;
|
||||
fetchList();
|
||||
} else {
|
||||
message.error(res.message || '操作失败');
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('操作失败');
|
||||
} finally {
|
||||
modalLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
modalVisible.value = false;
|
||||
resetForm();
|
||||
}
|
||||
|
||||
async function handleDelete(id: number) {
|
||||
try {
|
||||
const res: any = await customerApi.deleteCustomer(id);
|
||||
if (res.code === 200) {
|
||||
message.success('删除成功');
|
||||
searchForm.page = 1;
|
||||
fetchList();
|
||||
} else {
|
||||
message.error(res.message || '删除失败');
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('删除失败');
|
||||
}
|
||||
}
|
||||
|
||||
function getStatusColor(status: number): string {
|
||||
const colorMap: Record<number, string> = {
|
||||
1: 'blue',
|
||||
2: 'green',
|
||||
3: 'orange',
|
||||
4: 'default',
|
||||
};
|
||||
return colorMap[status] || 'default';
|
||||
}
|
||||
|
||||
function getStatusText(status: number): string {
|
||||
const textMap: Record<number, string> = {
|
||||
1: '意向',
|
||||
2: '签约',
|
||||
3: '运维中',
|
||||
4: '终止',
|
||||
};
|
||||
return textMap[status] || '未知';
|
||||
}
|
||||
|
||||
function formatDateTime(dateStr: string) {
|
||||
if (!dateStr) return '-';
|
||||
return dayjs(dateStr).format('YYYY-MM-DD HH:mm:ss');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.customer-management {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:deep(.ant-card) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
:deep(.ant-card-body) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
flex-shrink: 0;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:deep(.ant-table) {
|
||||
font-size: 14px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.ant-btn-link) {
|
||||
padding: 0 4px;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user