feat: 新增客户拜访、建筑、房间及项目全流程业务模块
本次提交新增了客户拜访、客户建筑、客户房间的完整业务模块,同时补充了项目勘察、项目方案、合同管理、项目验收、回访记录的全项目流程相关功能,包括: 1. 新增对应业务的实体类、DTO、控制器、服务层代码 2. 完善RBAC权限映射配置 3. 添加前端API、路由及页面菜单配置 4. 更新数据库种子数据补充菜单和权限绑定
This commit is contained in:
1 parent
f61ade107b
commit
219091f864
53 files changed
+5990
-6
No files matched your search
@@ -0,0 +1,65 @@
|
||||
import request from '@/utils/request';
|
||||
|
||||
export interface AcceptanceInfo {
|
||||
id: number;
|
||||
acceptNo: string;
|
||||
customerId: number;
|
||||
acceptor?: string;
|
||||
acceptOpinion?: string;
|
||||
acceptDate?: string;
|
||||
installer?: string;
|
||||
acceptFileUrl?: string;
|
||||
contractId?: number;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface AcceptanceQuery {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
keyword?: string;
|
||||
customerId?: number;
|
||||
}
|
||||
|
||||
export interface AcceptanceListResult {
|
||||
list: AcceptanceInfo[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
export interface AcceptanceCreateParams {
|
||||
customerId: number;
|
||||
acceptor?: string;
|
||||
acceptOpinion?: string;
|
||||
acceptDate?: string;
|
||||
installer?: string;
|
||||
acceptFileUrl?: string;
|
||||
contractId?: number;
|
||||
}
|
||||
|
||||
export interface AcceptanceUpdateParams {
|
||||
customerId?: number;
|
||||
acceptor?: string;
|
||||
acceptOpinion?: string;
|
||||
acceptDate?: string;
|
||||
installer?: string;
|
||||
acceptFileUrl?: string;
|
||||
contractId?: number;
|
||||
}
|
||||
|
||||
export const acceptanceApi = {
|
||||
getList: (params: AcceptanceQuery) =>
|
||||
request.get<AcceptanceListResult>('/acceptances', { params }),
|
||||
|
||||
getDetail: (id: number) =>
|
||||
request.get<AcceptanceInfo>(`/acceptances/${id}`),
|
||||
|
||||
create: (data: AcceptanceCreateParams) =>
|
||||
request.post('/acceptances', data),
|
||||
|
||||
update: (id: number, data: AcceptanceUpdateParams) =>
|
||||
request.put(`/acceptances/${id}`, data),
|
||||
|
||||
delete: (id: number) =>
|
||||
request.delete(`/acceptances/${id}`),
|
||||
};
|
||||
@@ -0,0 +1,81 @@
|
||||
import request from '@/utils/request';
|
||||
|
||||
export interface ContractInfo {
|
||||
id: number;
|
||||
customerId: number;
|
||||
xtContractNo?: string;
|
||||
contractAmount?: number;
|
||||
payCycle?: string;
|
||||
salesmanId?: number;
|
||||
signDate?: string;
|
||||
contractDesc?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
planId?: number;
|
||||
contractFileUrl?: string;
|
||||
status?: number;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface ContractQuery {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
keyword?: string;
|
||||
customerId?: number;
|
||||
salesmanId?: number;
|
||||
status?: number;
|
||||
}
|
||||
|
||||
export interface ContractListResult {
|
||||
list: ContractInfo[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
export interface ContractCreateParams {
|
||||
customerId: number;
|
||||
xtContractNo?: string;
|
||||
contractAmount?: number;
|
||||
payCycle?: string;
|
||||
salesmanId?: number;
|
||||
signDate?: string;
|
||||
contractDesc?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
planId?: number;
|
||||
contractFileUrl?: string;
|
||||
status?: number;
|
||||
}
|
||||
|
||||
export interface ContractUpdateParams {
|
||||
customerId?: number;
|
||||
xtContractNo?: string;
|
||||
contractAmount?: number;
|
||||
payCycle?: string;
|
||||
salesmanId?: number;
|
||||
signDate?: string;
|
||||
contractDesc?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
planId?: number;
|
||||
contractFileUrl?: string;
|
||||
status?: number;
|
||||
}
|
||||
|
||||
export const contractApi = {
|
||||
getList: (params: ContractQuery) =>
|
||||
request.get<ContractListResult>('/contracts', { params }),
|
||||
|
||||
getDetail: (id: number) =>
|
||||
request.get<ContractInfo>(`/contracts/${id}`),
|
||||
|
||||
create: (data: ContractCreateParams) =>
|
||||
request.post('/contracts', data),
|
||||
|
||||
update: (id: number, data: ContractUpdateParams) =>
|
||||
request.put(`/contracts/${id}`, data),
|
||||
|
||||
delete: (id: number) =>
|
||||
request.delete(`/contracts/${id}`),
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
import request from '@/utils/request';
|
||||
|
||||
export interface CustomerBuildingInfo {
|
||||
id: number;
|
||||
customerId: number;
|
||||
buildingName: string;
|
||||
basicInfo?: string;
|
||||
purpose?: string;
|
||||
floorCount?: number;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface CustomerBuildingQuery {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
keyword?: string;
|
||||
customerId?: number;
|
||||
}
|
||||
|
||||
export interface CustomerBuildingListResult {
|
||||
list: CustomerBuildingInfo[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
export interface CustomerBuildingCreateParams {
|
||||
customerId: number;
|
||||
buildingName: string;
|
||||
basicInfo?: string;
|
||||
purpose?: string;
|
||||
floorCount?: number;
|
||||
}
|
||||
|
||||
export interface CustomerBuildingUpdateParams {
|
||||
customerId?: number;
|
||||
buildingName?: string;
|
||||
basicInfo?: string;
|
||||
purpose?: string;
|
||||
floorCount?: number;
|
||||
}
|
||||
|
||||
export const customerBuildingApi = {
|
||||
getList: (params: CustomerBuildingQuery) =>
|
||||
request.get<CustomerBuildingListResult>('/customer-buildings', { params }),
|
||||
|
||||
getDetail: (id: number) =>
|
||||
request.get<CustomerBuildingInfo>(`/customer-buildings/${id}`),
|
||||
|
||||
create: (data: CustomerBuildingCreateParams) =>
|
||||
request.post('/customer-buildings', data),
|
||||
|
||||
update: (id: number, data: CustomerBuildingUpdateParams) =>
|
||||
request.put(`/customer-buildings/${id}`, data),
|
||||
|
||||
delete: (id: number) =>
|
||||
request.delete(`/customer-buildings/${id}`),
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
import request from '@/utils/request';
|
||||
|
||||
export interface CustomerRoomInfo {
|
||||
id: number;
|
||||
roomCode: string;
|
||||
customerId: number;
|
||||
roomName: string;
|
||||
buildingId: number;
|
||||
floorNo?: string;
|
||||
roomPurpose?: string;
|
||||
deptId?: number;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface CustomerRoomQuery {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
keyword?: string;
|
||||
customerId?: number;
|
||||
buildingId?: number;
|
||||
}
|
||||
|
||||
export interface CustomerRoomListResult {
|
||||
list: CustomerRoomInfo[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
export interface CustomerRoomCreateParams {
|
||||
roomCode: string;
|
||||
customerId: number;
|
||||
roomName: string;
|
||||
buildingId: number;
|
||||
floorNo?: string;
|
||||
roomPurpose?: string;
|
||||
deptId?: number;
|
||||
}
|
||||
|
||||
export interface CustomerRoomUpdateParams {
|
||||
roomCode?: string;
|
||||
customerId?: number;
|
||||
roomName?: string;
|
||||
buildingId?: number;
|
||||
floorNo?: string;
|
||||
roomPurpose?: string;
|
||||
deptId?: number;
|
||||
}
|
||||
|
||||
export const customerRoomApi = {
|
||||
getList: (params: CustomerRoomQuery) =>
|
||||
request.get<CustomerRoomListResult>('/customer-rooms', { params }),
|
||||
|
||||
getDetail: (id: number) =>
|
||||
request.get<CustomerRoomInfo>(`/customer-rooms/${id}`),
|
||||
|
||||
create: (data: CustomerRoomCreateParams) =>
|
||||
request.post('/customer-rooms', data),
|
||||
|
||||
update: (id: number, data: CustomerRoomUpdateParams) =>
|
||||
request.put(`/customer-rooms/${id}`, data),
|
||||
|
||||
delete: (id: number) =>
|
||||
request.delete(`/customer-rooms/${id}`),
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
import request from '@/utils/request';
|
||||
|
||||
export interface CustomerVisitInfo {
|
||||
id: number;
|
||||
visitNo: string;
|
||||
customerId: number;
|
||||
visitPerson: string;
|
||||
visitPhone?: string;
|
||||
visitContent?: string;
|
||||
visitTime?: string;
|
||||
salesmanId?: number;
|
||||
willPlan?: number;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface CustomerVisitQuery {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
keyword?: string;
|
||||
customerId?: number;
|
||||
salesmanId?: number;
|
||||
}
|
||||
|
||||
export interface CustomerVisitListResult {
|
||||
list: CustomerVisitInfo[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
export interface CustomerVisitCreateParams {
|
||||
customerId: number;
|
||||
visitPerson: string;
|
||||
visitPhone?: string;
|
||||
visitContent?: string;
|
||||
visitTime?: string;
|
||||
salesmanId?: number;
|
||||
willPlan?: number;
|
||||
}
|
||||
|
||||
export interface CustomerVisitUpdateParams {
|
||||
customerId?: number;
|
||||
visitPerson?: string;
|
||||
visitPhone?: string;
|
||||
visitContent?: string;
|
||||
visitTime?: string;
|
||||
salesmanId?: number;
|
||||
willPlan?: number;
|
||||
}
|
||||
|
||||
export const customerVisitApi = {
|
||||
getList: (params: CustomerVisitQuery) =>
|
||||
request.get<CustomerVisitListResult>('/customer-visits', { params }),
|
||||
|
||||
getDetail: (id: number) =>
|
||||
request.get<CustomerVisitInfo>(`/customer-visits/${id}`),
|
||||
|
||||
create: (data: CustomerVisitCreateParams) =>
|
||||
request.post('/customer-visits', data),
|
||||
|
||||
update: (id: number, data: CustomerVisitUpdateParams) =>
|
||||
request.put(`/customer-visits/${id}`, data),
|
||||
|
||||
delete: (id: number) =>
|
||||
request.delete(`/customer-visits/${id}`),
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
import request from '@/utils/request';
|
||||
|
||||
export interface PlanDetail {
|
||||
id?: number;
|
||||
equipName?: string;
|
||||
equipId?: number;
|
||||
smartDeviceName?: string;
|
||||
smartDeviceId?: number;
|
||||
quantity?: number;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
export interface ProjectPlanInfo {
|
||||
id: number;
|
||||
planNo: string;
|
||||
customerId: number;
|
||||
registrar?: string;
|
||||
registerTime?: string;
|
||||
planDesc?: string;
|
||||
planFileUrl?: string;
|
||||
status?: number;
|
||||
createdAt: string;
|
||||
details?: PlanDetail[];
|
||||
}
|
||||
|
||||
export interface ProjectPlanQuery {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
keyword?: string;
|
||||
customerId?: number;
|
||||
status?: number;
|
||||
}
|
||||
|
||||
export interface ProjectPlanListResult {
|
||||
list: ProjectPlanInfo[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
export interface ProjectPlanCreateParams {
|
||||
customerId: number;
|
||||
registrar?: string;
|
||||
registerTime?: string;
|
||||
planDesc?: string;
|
||||
planFileUrl?: string;
|
||||
status?: number;
|
||||
details?: PlanDetail[];
|
||||
}
|
||||
|
||||
export interface ProjectPlanUpdateParams {
|
||||
customerId?: number;
|
||||
registrar?: string;
|
||||
registerTime?: string;
|
||||
planDesc?: string;
|
||||
planFileUrl?: string;
|
||||
status?: number;
|
||||
details?: PlanDetail[];
|
||||
}
|
||||
|
||||
export const projectPlanApi = {
|
||||
getList: (params: ProjectPlanQuery) =>
|
||||
request.get<ProjectPlanListResult>('/project-plans', { params }),
|
||||
|
||||
getDetail: (id: number) =>
|
||||
request.get<ProjectPlanInfo>(`/project-plans/${id}`),
|
||||
|
||||
create: (data: ProjectPlanCreateParams) =>
|
||||
request.post('/project-plans', data),
|
||||
|
||||
update: (id: number, data: ProjectPlanUpdateParams) =>
|
||||
request.put(`/project-plans/${id}`, data),
|
||||
|
||||
delete: (id: number) =>
|
||||
request.delete(`/project-plans/${id}`),
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
import request from '@/utils/request';
|
||||
|
||||
export interface SurveyOrderInfo {
|
||||
id: number;
|
||||
surveyNo: string;
|
||||
customerId: number;
|
||||
surveyor?: string;
|
||||
workerId?: number;
|
||||
surveyTime?: string;
|
||||
remark?: string;
|
||||
status?: number;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface SurveyOrderQuery {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
keyword?: string;
|
||||
customerId?: number;
|
||||
status?: number;
|
||||
}
|
||||
|
||||
export interface SurveyOrderListResult {
|
||||
list: SurveyOrderInfo[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
export interface SurveyOrderCreateParams {
|
||||
customerId: number;
|
||||
surveyor?: string;
|
||||
workerId?: number;
|
||||
surveyTime?: string;
|
||||
remark?: string;
|
||||
status?: number;
|
||||
}
|
||||
|
||||
export interface SurveyOrderUpdateParams {
|
||||
customerId?: number;
|
||||
surveyor?: string;
|
||||
workerId?: number;
|
||||
surveyTime?: string;
|
||||
remark?: string;
|
||||
status?: number;
|
||||
}
|
||||
|
||||
export const surveyOrderApi = {
|
||||
getList: (params: SurveyOrderQuery) =>
|
||||
request.get<SurveyOrderListResult>('/survey-orders', { params }),
|
||||
|
||||
getDetail: (id: number) =>
|
||||
request.get<SurveyOrderInfo>(`/survey-orders/${id}`),
|
||||
|
||||
create: (data: SurveyOrderCreateParams) =>
|
||||
request.post('/survey-orders', data),
|
||||
|
||||
update: (id: number, data: SurveyOrderUpdateParams) =>
|
||||
request.put(`/survey-orders/${id}`, data),
|
||||
|
||||
delete: (id: number) =>
|
||||
request.delete(`/survey-orders/${id}`),
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
import request from '@/utils/request';
|
||||
|
||||
export interface VisitRecordInfo {
|
||||
id: number;
|
||||
customerId: number;
|
||||
contactPerson?: string;
|
||||
contactPhone?: string;
|
||||
visitor?: string;
|
||||
visitTime?: string;
|
||||
customerOpinion?: string;
|
||||
photos?: any[];
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface VisitRecordQuery {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
keyword?: string;
|
||||
customerId?: number;
|
||||
}
|
||||
|
||||
export interface VisitRecordListResult {
|
||||
list: VisitRecordInfo[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
export interface VisitRecordCreateParams {
|
||||
customerId: number;
|
||||
contactPerson?: string;
|
||||
contactPhone?: string;
|
||||
visitor?: string;
|
||||
visitTime?: string;
|
||||
customerOpinion?: string;
|
||||
photos?: any[];
|
||||
}
|
||||
|
||||
export interface VisitRecordUpdateParams {
|
||||
customerId?: number;
|
||||
contactPerson?: string;
|
||||
contactPhone?: string;
|
||||
visitor?: string;
|
||||
visitTime?: string;
|
||||
customerOpinion?: string;
|
||||
photos?: any[];
|
||||
}
|
||||
|
||||
export const visitRecordApi = {
|
||||
getList: (params: VisitRecordQuery) =>
|
||||
request.get<VisitRecordListResult>('/visit-records', { params }),
|
||||
|
||||
getDetail: (id: number) =>
|
||||
request.get<VisitRecordInfo>(`/visit-records/${id}`),
|
||||
|
||||
create: (data: VisitRecordCreateParams) =>
|
||||
request.post('/visit-records', data),
|
||||
|
||||
update: (id: number, data: VisitRecordUpdateParams) =>
|
||||
request.put(`/visit-records/${id}`, data),
|
||||
|
||||
delete: (id: number) =>
|
||||
request.delete(`/visit-records/${id}`),
|
||||
};
|
||||
@@ -49,12 +49,60 @@ const routes: RouteRecordRaw[] = [
|
||||
component: () => import("@/views/customer/account/index.vue"),
|
||||
meta: { requiresAuth: true, title: "客户账号", permission: "customer:account" }
|
||||
},
|
||||
{
|
||||
path: "customer/visit",
|
||||
name: "CustomerVisit",
|
||||
component: () => import("@/views/customer/visit/index.vue"),
|
||||
meta: { requiresAuth: true, title: "客户拜访", permission: "customer:visit" }
|
||||
},
|
||||
{
|
||||
path: "customer/building",
|
||||
name: "CustomerBuilding",
|
||||
component: () => import("@/views/customer/building/index.vue"),
|
||||
meta: { requiresAuth: true, title: "客户建筑", permission: "customer:building" }
|
||||
},
|
||||
{
|
||||
path: "customer/room",
|
||||
name: "CustomerRoom",
|
||||
component: () => import("@/views/customer/room/index.vue"),
|
||||
meta: { requiresAuth: true, title: "客户房间", permission: "customer:room" }
|
||||
},
|
||||
{
|
||||
path: "project",
|
||||
name: "Project",
|
||||
component: () => import("@/views/Placeholder.vue"),
|
||||
redirect: "/project/survey",
|
||||
meta: { requiresAuth: true, title: "项目管理", permission: "project:list" }
|
||||
},
|
||||
{
|
||||
path: "project/survey",
|
||||
name: "ProjectSurvey",
|
||||
component: () => import("@/views/project/survey/index.vue"),
|
||||
meta: { requiresAuth: true, title: "项目勘察", permission: "project:survey" }
|
||||
},
|
||||
{
|
||||
path: "project/plan",
|
||||
name: "ProjectPlan",
|
||||
component: () => import("@/views/project/plan/index.vue"),
|
||||
meta: { requiresAuth: true, title: "项目方案", permission: "project:plan" }
|
||||
},
|
||||
{
|
||||
path: "project/contract",
|
||||
name: "ProjectContract",
|
||||
component: () => import("@/views/project/contract/index.vue"),
|
||||
meta: { requiresAuth: true, title: "合同管理", permission: "project:contract" }
|
||||
},
|
||||
{
|
||||
path: "project/acceptance",
|
||||
name: "ProjectAcceptance",
|
||||
component: () => import("@/views/project/acceptance/index.vue"),
|
||||
meta: { requiresAuth: true, title: "项目验收", permission: "project:acceptance" }
|
||||
},
|
||||
{
|
||||
path: "project/visit",
|
||||
name: "ProjectVisit",
|
||||
component: () => import("@/views/project/visit/index.vue"),
|
||||
meta: { requiresAuth: true, title: "回访记录", permission: "project:visit" }
|
||||
},
|
||||
{
|
||||
path: "dispatch",
|
||||
name: "Dispatch",
|
||||
|
||||
@@ -0,0 +1,368 @@
|
||||
<template>
|
||||
<div class="page-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: 220px"
|
||||
@pressEnter="handleSearch"
|
||||
>
|
||||
<template #prefix><SearchOutlined /></template>
|
||||
</a-input>
|
||||
<a-select
|
||||
v-model:value="searchForm.customerId"
|
||||
placeholder="所属客户"
|
||||
allow-clear
|
||||
style="width: 200px"
|
||||
:options="customerOptions"
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
/>
|
||||
<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 === 'customerName'">
|
||||
{{ getCustomerName(record.customerId) }}
|
||||
</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="620px"
|
||||
@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"
|
||||
:disabled="!isAdmin && isEdit"
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="建筑名称" name="buildingName">
|
||||
<a-input v-model:value="formData.buildingName" placeholder="请输入建筑名称" maxlength="128" />
|
||||
</a-form-item>
|
||||
<a-form-item label="建筑用途" name="purpose">
|
||||
<a-input v-model:value="formData.purpose" placeholder="请输入建筑用途" maxlength="128" />
|
||||
</a-form-item>
|
||||
<a-form-item label="楼层数" name="floorCount">
|
||||
<a-input-number v-model:value="formData.floorCount" :min="0" style="width: 100%" placeholder="请输入楼层数" />
|
||||
</a-form-item>
|
||||
<a-form-item label="基本信息" name="basicInfo">
|
||||
<a-textarea v-model:value="formData.basicInfo" :rows="3" placeholder="请输入建筑基本信息" maxlength="2000" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { SearchOutlined, ReloadOutlined, PlusOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons-vue';
|
||||
import { customerBuildingApi, type CustomerBuildingInfo, type CustomerBuildingQuery } from '@/api/customerBuilding';
|
||||
import { customerApi } from '@/api/customer';
|
||||
import { useTableScroll } from '@/composables/useTableScroll';
|
||||
import { usePermissionStore } from '@/stores/permission';
|
||||
|
||||
const { wrapperRef, tableHeight } = useTableScroll();
|
||||
const permissionStore = usePermissionStore();
|
||||
const isAdmin = computed(() => permissionStore.hasRole('ADMIN'));
|
||||
|
||||
const loading = ref(false);
|
||||
const dataSource = ref<CustomerBuildingInfo[]>([]);
|
||||
const total = ref(0);
|
||||
const searchForm = reactive<CustomerBuildingQuery>({
|
||||
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 formData = reactive({
|
||||
customerId: undefined as number | undefined,
|
||||
buildingName: '',
|
||||
purpose: '',
|
||||
floorCount: undefined as number | undefined,
|
||||
basicInfo: '',
|
||||
});
|
||||
|
||||
const formRules = {
|
||||
customerId: [{ required: true, message: '请选择所属客户', trigger: 'change' }],
|
||||
buildingName: [{ required: true, message: '请输入建筑名称', trigger: 'blur' }],
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{ title: '所属客户', key: 'customerName', ellipsis: true },
|
||||
{ title: '建筑名称', dataIndex: 'buildingName', key: 'buildingName', ellipsis: true },
|
||||
{ title: '建筑用途', dataIndex: 'purpose', key: 'purpose', ellipsis: true },
|
||||
{ title: '楼层数', dataIndex: 'floorCount', key: 'floorCount', align: 'center' as const, width: 90 },
|
||||
{ title: '基本信息', dataIndex: 'basicInfo', key: 'basicInfo', ellipsis: true },
|
||||
{ title: '操作', key: 'action', width: 140, 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 customerBuildingApi.getList(searchForm);
|
||||
if (res.code === 200) {
|
||||
dataSource.value = res.data.list;
|
||||
total.value = res.data.total;
|
||||
}
|
||||
} catch {
|
||||
message.error('获取建筑列表失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchCustomerOptions() {
|
||||
try {
|
||||
const res: any = await customerApi.getCustomerOptions();
|
||||
if (res.code === 200) {
|
||||
customerOptions.value = res.data.map((item: any) => ({ label: item.customerName, value: item.id }));
|
||||
}
|
||||
} catch {
|
||||
console.error('获取客户选项失败');
|
||||
}
|
||||
}
|
||||
|
||||
function getCustomerName(customerId: number): string {
|
||||
const found = customerOptions.value.find((c) => c.value === customerId);
|
||||
return found ? found.label : `#${customerId}`;
|
||||
}
|
||||
|
||||
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: CustomerBuildingInfo) {
|
||||
isEdit.value = true;
|
||||
editId.value = record.id;
|
||||
modalTitle.value = '编辑建筑';
|
||||
resetForm();
|
||||
formData.customerId = record.customerId;
|
||||
formData.buildingName = record.buildingName;
|
||||
formData.purpose = record.purpose;
|
||||
formData.floorCount = record.floorCount;
|
||||
formData.basicInfo = record.basicInfo;
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
formData.customerId = undefined;
|
||||
formData.buildingName = '';
|
||||
formData.purpose = '';
|
||||
formData.floorCount = undefined;
|
||||
formData.basicInfo = '';
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
modalLoading.value = true;
|
||||
try {
|
||||
const submitData: any = {
|
||||
customerId: formData.customerId,
|
||||
buildingName: formData.buildingName,
|
||||
purpose: formData.purpose,
|
||||
floorCount: formData.floorCount,
|
||||
basicInfo: formData.basicInfo,
|
||||
};
|
||||
|
||||
let res: any;
|
||||
if (isEdit.value && editId.value) {
|
||||
res = await customerBuildingApi.update(editId.value, submitData);
|
||||
} else {
|
||||
res = await customerBuildingApi.create(submitData);
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
message.success(isEdit.value ? '更新成功' : '创建成功');
|
||||
modalVisible.value = false;
|
||||
fetchList();
|
||||
} else {
|
||||
message.error(res.message || '操作失败');
|
||||
}
|
||||
} catch {
|
||||
message.error('操作失败');
|
||||
} finally {
|
||||
modalLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
modalVisible.value = false;
|
||||
resetForm();
|
||||
}
|
||||
|
||||
async function handleDelete(id: number) {
|
||||
try {
|
||||
const res: any = await customerBuildingApi.delete(id);
|
||||
if (res.code === 200) {
|
||||
message.success('删除成功');
|
||||
fetchList();
|
||||
} else {
|
||||
message.error(res.message || '删除失败');
|
||||
}
|
||||
} catch {
|
||||
message.error('删除失败');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-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>
|
||||
@@ -0,0 +1,432 @@
|
||||
<template>
|
||||
<div class="page-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: 220px"
|
||||
@pressEnter="handleSearch"
|
||||
>
|
||||
<template #prefix><SearchOutlined /></template>
|
||||
</a-input>
|
||||
<a-select
|
||||
v-model:value="searchForm.customerId"
|
||||
placeholder="所属客户"
|
||||
allow-clear
|
||||
style="width: 200px"
|
||||
:options="customerOptions"
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
@change="handleCustomerSearchChange"
|
||||
/>
|
||||
<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 === 'customerName'">
|
||||
{{ getCustomerName(record.customerId) }}
|
||||
</template>
|
||||
<template v-if="column.key === 'buildingName'">
|
||||
{{ getBuildingName(record.buildingId) }}
|
||||
</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="620px"
|
||||
@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"
|
||||
:disabled="!isAdmin && isEdit"
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
@change="handleCustomerFormChange"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="所属建筑" name="buildingId">
|
||||
<a-select
|
||||
v-model:value="formData.buildingId"
|
||||
placeholder="请选择建筑"
|
||||
:options="buildingOptions"
|
||||
:not-found-content="null"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="房间编码" name="roomCode">
|
||||
<a-input v-model:value="formData.roomCode" placeholder="请输入房间编码" maxlength="64" />
|
||||
</a-form-item>
|
||||
<a-form-item label="房间名称" name="roomName">
|
||||
<a-input v-model:value="formData.roomName" placeholder="请输入房间名称" maxlength="128" />
|
||||
</a-form-item>
|
||||
<a-form-item label="所在楼层" name="floorNo">
|
||||
<a-input v-model:value="formData.floorNo" placeholder="请输入所在楼层" maxlength="16" />
|
||||
</a-form-item>
|
||||
<a-form-item label="房间用途" name="roomPurpose">
|
||||
<a-input v-model:value="formData.roomPurpose" placeholder="请输入房间用途" maxlength="128" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { SearchOutlined, ReloadOutlined, PlusOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons-vue';
|
||||
import { customerRoomApi, type CustomerRoomInfo, type CustomerRoomQuery } from '@/api/customerRoom';
|
||||
import { customerBuildingApi } from '@/api/customerBuilding';
|
||||
import { customerApi } from '@/api/customer';
|
||||
import { useTableScroll } from '@/composables/useTableScroll';
|
||||
import { usePermissionStore } from '@/stores/permission';
|
||||
|
||||
const { wrapperRef, tableHeight } = useTableScroll();
|
||||
const permissionStore = usePermissionStore();
|
||||
const isAdmin = computed(() => permissionStore.hasRole('ADMIN'));
|
||||
|
||||
const loading = ref(false);
|
||||
const dataSource = ref<CustomerRoomInfo[]>([]);
|
||||
const total = ref(0);
|
||||
const searchForm = reactive<CustomerRoomQuery>({
|
||||
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 buildingOptions = ref<{ label: string; value: number }[]>([]);
|
||||
const allBuildings = ref<{ label: string; value: number; customerId: number }[]>([]);
|
||||
|
||||
const formData = reactive({
|
||||
customerId: undefined as number | undefined,
|
||||
buildingId: undefined as number | undefined,
|
||||
roomCode: '',
|
||||
roomName: '',
|
||||
floorNo: '',
|
||||
roomPurpose: '',
|
||||
});
|
||||
|
||||
const formRules = {
|
||||
customerId: [{ required: true, message: '请选择所属客户', trigger: 'change' }],
|
||||
buildingId: [{ required: true, message: '请选择所属建筑', trigger: 'change' }],
|
||||
roomCode: [{ required: true, message: '请输入房间编码', trigger: 'blur' }],
|
||||
roomName: [{ required: true, message: '请输入房间名称', trigger: 'blur' }],
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{ title: '房间编码', dataIndex: 'roomCode', key: 'roomCode', ellipsis: true, width: 130 },
|
||||
{ title: '房间名称', dataIndex: 'roomName', key: 'roomName', ellipsis: true },
|
||||
{ title: '所属客户', key: 'customerName', ellipsis: true },
|
||||
{ title: '所属建筑', key: 'buildingName', ellipsis: true },
|
||||
{ title: '所在楼层', dataIndex: 'floorNo', key: 'floorNo', align: 'center' as const, width: 90 },
|
||||
{ title: '房间用途', dataIndex: 'roomPurpose', key: 'roomPurpose', ellipsis: true },
|
||||
{ title: '操作', key: 'action', width: 140, 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();
|
||||
fetchAllBuildings();
|
||||
});
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res: any = await customerRoomApi.getList(searchForm);
|
||||
if (res.code === 200) {
|
||||
dataSource.value = res.data.list;
|
||||
total.value = res.data.total;
|
||||
}
|
||||
} catch {
|
||||
message.error('获取房间列表失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchCustomerOptions() {
|
||||
try {
|
||||
const res: any = await customerApi.getCustomerOptions();
|
||||
if (res.code === 200) {
|
||||
customerOptions.value = res.data.map((item: any) => ({ label: item.customerName, value: item.id }));
|
||||
}
|
||||
} catch {
|
||||
console.error('获取客户选项失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchAllBuildings() {
|
||||
try {
|
||||
const res: any = await customerBuildingApi.getList({ page: 1, pageSize: 999 });
|
||||
if (res.code === 200) {
|
||||
allBuildings.value = res.data.list.map((item: any) => ({
|
||||
label: item.buildingName,
|
||||
value: item.id,
|
||||
customerId: item.customerId,
|
||||
}));
|
||||
}
|
||||
} catch {
|
||||
console.error('获取建筑列表失败');
|
||||
}
|
||||
}
|
||||
|
||||
function getCustomerName(customerId: number): string {
|
||||
const found = customerOptions.value.find((c) => c.value === customerId);
|
||||
return found ? found.label : `#${customerId}`;
|
||||
}
|
||||
|
||||
function getBuildingName(buildingId: number): string {
|
||||
const found = allBuildings.value.find((b) => b.value === buildingId);
|
||||
return found ? found.label : `#${buildingId}`;
|
||||
}
|
||||
|
||||
function filterBuildings(customerId?: number) {
|
||||
if (customerId) {
|
||||
buildingOptions.value = allBuildings.value.filter((b) => b.customerId === customerId);
|
||||
} else {
|
||||
buildingOptions.value = allBuildings.value;
|
||||
}
|
||||
}
|
||||
|
||||
function handleCustomerFormChange(customerId?: number) {
|
||||
formData.buildingId = undefined;
|
||||
filterBuildings(customerId);
|
||||
}
|
||||
|
||||
function handleCustomerSearchChange(customerId?: number) {
|
||||
searchForm.customerId = customerId;
|
||||
handleSearch();
|
||||
}
|
||||
|
||||
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();
|
||||
filterBuildings(undefined);
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function handleEdit(record: CustomerRoomInfo) {
|
||||
isEdit.value = true;
|
||||
editId.value = record.id;
|
||||
modalTitle.value = '编辑房间';
|
||||
resetForm();
|
||||
formData.customerId = record.customerId;
|
||||
filterBuildings(record.customerId);
|
||||
formData.buildingId = record.buildingId;
|
||||
formData.roomCode = record.roomCode;
|
||||
formData.roomName = record.roomName;
|
||||
formData.floorNo = record.floorNo;
|
||||
formData.roomPurpose = record.roomPurpose;
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
formData.customerId = undefined;
|
||||
formData.buildingId = undefined;
|
||||
formData.roomCode = '';
|
||||
formData.roomName = '';
|
||||
formData.floorNo = '';
|
||||
formData.roomPurpose = '';
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
modalLoading.value = true;
|
||||
try {
|
||||
const submitData: any = {
|
||||
customerId: formData.customerId,
|
||||
buildingId: formData.buildingId,
|
||||
roomCode: formData.roomCode,
|
||||
roomName: formData.roomName,
|
||||
floorNo: formData.floorNo,
|
||||
roomPurpose: formData.roomPurpose,
|
||||
};
|
||||
|
||||
let res: any;
|
||||
if (isEdit.value && editId.value) {
|
||||
res = await customerRoomApi.update(editId.value, submitData);
|
||||
} else {
|
||||
res = await customerRoomApi.create(submitData);
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
message.success(isEdit.value ? '更新成功' : '创建成功');
|
||||
modalVisible.value = false;
|
||||
fetchList();
|
||||
} else {
|
||||
message.error(res.message || '操作失败');
|
||||
}
|
||||
} catch {
|
||||
message.error('操作失败');
|
||||
} finally {
|
||||
modalLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
modalVisible.value = false;
|
||||
resetForm();
|
||||
}
|
||||
|
||||
async function handleDelete(id: number) {
|
||||
try {
|
||||
const res: any = await customerRoomApi.delete(id);
|
||||
if (res.code === 200) {
|
||||
message.success('删除成功');
|
||||
fetchList();
|
||||
} else {
|
||||
message.error(res.message || '删除失败');
|
||||
}
|
||||
} catch {
|
||||
message.error('删除失败');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-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>
|
||||
@@ -0,0 +1,423 @@
|
||||
<template>
|
||||
<div class="page-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: 220px"
|
||||
@pressEnter="handleSearch"
|
||||
>
|
||||
<template #prefix><SearchOutlined /></template>
|
||||
</a-input>
|
||||
<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 === 'customerName'">
|
||||
{{ getCustomerName(record.customerId) }}
|
||||
</template>
|
||||
<template v-if="column.key === 'willPlan'">
|
||||
<a-tag :color="record.willPlan === 1 ? 'orange' : 'default'">
|
||||
{{ record.willPlan === 1 ? '有意向方案' : '暂无' }}
|
||||
</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="customerId">
|
||||
<a-select
|
||||
v-model:value="formData.customerId"
|
||||
placeholder="请选择客户"
|
||||
:options="customerOptions"
|
||||
:disabled="!isAdmin && isEdit"
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="拜访人" name="visitPerson">
|
||||
<a-input v-model:value="formData.visitPerson" placeholder="请输入拜访人" maxlength="64" />
|
||||
</a-form-item>
|
||||
<a-form-item label="联系电话" name="visitPhone">
|
||||
<a-input v-model:value="formData.visitPhone" placeholder="请输入联系电话" />
|
||||
</a-form-item>
|
||||
<a-form-item label="拜访时间" name="visitTime">
|
||||
<a-date-picker v-model:value="formData.visitTime" style="width: 100%" placeholder="请选择拜访时间" />
|
||||
</a-form-item>
|
||||
<a-form-item label="是否有意向方案" name="willPlan">
|
||||
<a-radio-group v-model:value="formData.willPlan">
|
||||
<a-radio :value="1">有</a-radio>
|
||||
<a-radio :value="0">无</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="拜访内容" name="visitContent">
|
||||
<a-textarea v-model:value="formData.visitContent" :rows="3" placeholder="请输入拜访内容" maxlength="2000" />
|
||||
</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, computed, onMounted } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { SearchOutlined, ReloadOutlined, PlusOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons-vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { customerVisitApi, type CustomerVisitInfo, type CustomerVisitQuery } from '@/api/customerVisit';
|
||||
import { customerApi } from '@/api/customer';
|
||||
import { userApi } from '@/api/user';
|
||||
import { useTableScroll } from '@/composables/useTableScroll';
|
||||
import { usePermissionStore } from '@/stores/permission';
|
||||
|
||||
const { wrapperRef, tableHeight } = useTableScroll();
|
||||
const permissionStore = usePermissionStore();
|
||||
const isAdmin = computed(() => permissionStore.hasRole('ADMIN'));
|
||||
|
||||
const loading = ref(false);
|
||||
const dataSource = ref<CustomerVisitInfo[]>([]);
|
||||
const total = ref(0);
|
||||
const searchForm = reactive<CustomerVisitQuery>({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
keyword: '',
|
||||
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 customerOptions = ref<{ label: string; value: number }[]>([]);
|
||||
const salesmanOptions = ref<{ label: string; value: number }[]>([]);
|
||||
|
||||
const formData = reactive({
|
||||
customerId: undefined as number | undefined,
|
||||
visitPerson: '',
|
||||
visitPhone: '',
|
||||
visitTime: undefined as any,
|
||||
visitContent: '',
|
||||
willPlan: 0,
|
||||
salesmanId: undefined as number | undefined,
|
||||
});
|
||||
|
||||
const formRules = {
|
||||
customerId: [{ required: true, message: '请选择所属客户', trigger: 'change' }],
|
||||
visitPerson: [{ required: true, message: '请输入拜访人', trigger: 'blur' }],
|
||||
visitPhone: [{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' }],
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{ title: '拜访单号', dataIndex: 'visitNo', key: 'visitNo', ellipsis: true, width: 160 },
|
||||
{ title: '所属客户', key: 'customerName', ellipsis: true },
|
||||
{ title: '拜访人', dataIndex: 'visitPerson', key: 'visitPerson', ellipsis: true },
|
||||
{ title: '联系电话', dataIndex: 'visitPhone', key: 'visitPhone', ellipsis: true },
|
||||
{ title: '拜访时间', dataIndex: 'visitTime', key: 'visitTime' },
|
||||
{ title: '拜访内容', dataIndex: 'visitContent', key: 'visitContent', ellipsis: true },
|
||||
{ title: '意向方案', key: 'willPlan', align: 'center' as const, width: 100 },
|
||||
{ title: '创建时间', dataIndex: 'createdAt', key: 'createdAt', ellipsis: true },
|
||||
{ title: '操作', key: 'action', width: 140, 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();
|
||||
fetchSalesmanList();
|
||||
});
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res: any = await customerVisitApi.getList(searchForm);
|
||||
if (res.code === 200) {
|
||||
dataSource.value = res.data.list;
|
||||
total.value = res.data.total;
|
||||
}
|
||||
} catch {
|
||||
message.error('获取拜访列表失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchCustomerOptions() {
|
||||
try {
|
||||
const res: any = await customerApi.getCustomerOptions();
|
||||
if (res.code === 200) {
|
||||
customerOptions.value = res.data.map((item: any) => ({ label: item.customerName, value: item.id }));
|
||||
}
|
||||
} catch {
|
||||
console.error('获取客户选项失败');
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
console.error('获取业务员列表失败');
|
||||
}
|
||||
}
|
||||
|
||||
function getCustomerName(customerId: number): string {
|
||||
const found = customerOptions.value.find((c) => c.value === customerId);
|
||||
return found ? found.label : `#${customerId}`;
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
searchForm.page = 1;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.keyword = '';
|
||||
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();
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function handleEdit(record: CustomerVisitInfo) {
|
||||
isEdit.value = true;
|
||||
editId.value = record.id;
|
||||
modalTitle.value = '编辑拜访';
|
||||
resetForm();
|
||||
formData.customerId = record.customerId;
|
||||
formData.visitPerson = record.visitPerson;
|
||||
formData.visitPhone = record.visitPhone;
|
||||
formData.visitTime = record.visitTime ? dayjs(record.visitTime) : undefined;
|
||||
formData.visitContent = record.visitContent;
|
||||
formData.willPlan = record.willPlan ?? 0;
|
||||
formData.salesmanId = record.salesmanId;
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
formData.customerId = undefined;
|
||||
formData.visitPerson = '';
|
||||
formData.visitPhone = '';
|
||||
formData.visitTime = undefined;
|
||||
formData.visitContent = '';
|
||||
formData.willPlan = 0;
|
||||
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 = {
|
||||
customerId: formData.customerId,
|
||||
visitPerson: formData.visitPerson,
|
||||
visitPhone: formData.visitPhone,
|
||||
visitTime: formData.visitTime ? formData.visitTime.format('YYYY-MM-DD') : undefined,
|
||||
visitContent: formData.visitContent,
|
||||
willPlan: formData.willPlan,
|
||||
};
|
||||
if (isAdmin.value && formData.salesmanId !== undefined) {
|
||||
submitData.salesmanId = formData.salesmanId;
|
||||
}
|
||||
|
||||
let res: any;
|
||||
if (isEdit.value && editId.value) {
|
||||
res = await customerVisitApi.update(editId.value, submitData);
|
||||
} else {
|
||||
res = await customerVisitApi.create(submitData);
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
message.success(isEdit.value ? '更新成功' : '创建成功');
|
||||
modalVisible.value = false;
|
||||
fetchList();
|
||||
} else {
|
||||
message.error(res.message || '操作失败');
|
||||
}
|
||||
} catch {
|
||||
message.error('操作失败');
|
||||
} finally {
|
||||
modalLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
modalVisible.value = false;
|
||||
resetForm();
|
||||
}
|
||||
|
||||
async function handleDelete(id: number) {
|
||||
try {
|
||||
const res: any = await customerVisitApi.delete(id);
|
||||
if (res.code === 200) {
|
||||
message.success('删除成功');
|
||||
fetchList();
|
||||
} else {
|
||||
message.error(res.message || '删除失败');
|
||||
}
|
||||
} catch {
|
||||
message.error('删除失败');
|
||||
}
|
||||
}
|
||||
|
||||
function formatDateTime(dateStr: string) {
|
||||
if (!dateStr) return '-';
|
||||
return dayjs(dateStr).format('YYYY-MM-DD HH:mm:ss');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-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>
|
||||
@@ -0,0 +1,353 @@
|
||||
<template>
|
||||
<div class="page-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: 220px"
|
||||
@pressEnter="handleSearch"
|
||||
>
|
||||
<template #prefix><SearchOutlined /></template>
|
||||
</a-input>
|
||||
<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 === 'customerName'">
|
||||
{{ getCustomerName(record.customerId) }}
|
||||
</template>
|
||||
<template v-if="column.key === 'acceptDate'">
|
||||
{{ record.acceptDate || '-' }}
|
||||
</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="620px"
|
||||
@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"
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="验收人" name="acceptor">
|
||||
<a-input v-model:value="formData.acceptor" placeholder="请输入验收人" maxlength="64" />
|
||||
</a-form-item>
|
||||
<a-form-item label="验收时间" name="acceptDate">
|
||||
<a-date-picker v-model:value="formData.acceptDate" style="width: 100%" placeholder="请选择验收时间" />
|
||||
</a-form-item>
|
||||
<a-form-item label="安装人" name="installer">
|
||||
<a-input v-model:value="formData.installer" placeholder="请输入安装人" maxlength="64" />
|
||||
</a-form-item>
|
||||
<a-form-item label="验收意见" name="acceptOpinion">
|
||||
<a-textarea v-model:value="formData.acceptOpinion" :rows="3" placeholder="请输入验收意见" maxlength="2000" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { SearchOutlined, ReloadOutlined, PlusOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons-vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { acceptanceApi, type AcceptanceInfo, type AcceptanceQuery } from '@/api/acceptance';
|
||||
import { customerApi } from '@/api/customer';
|
||||
import { useTableScroll } from '@/composables/useTableScroll';
|
||||
|
||||
const { wrapperRef, tableHeight } = useTableScroll();
|
||||
|
||||
const loading = ref(false);
|
||||
const dataSource = ref<AcceptanceInfo[]>([]);
|
||||
const total = ref(0);
|
||||
const searchForm = reactive<AcceptanceQuery>({ page: 1, pageSize: 10, keyword: '' });
|
||||
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 formData = reactive({
|
||||
customerId: undefined as number | undefined,
|
||||
acceptor: '',
|
||||
acceptDate: undefined as any,
|
||||
installer: '',
|
||||
acceptOpinion: '',
|
||||
});
|
||||
|
||||
const formRules = {
|
||||
customerId: [{ required: true, message: '请选择所属客户', trigger: 'change' }],
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{ title: '验收单号', dataIndex: 'acceptNo', key: 'acceptNo', ellipsis: true, width: 170 },
|
||||
{ title: '所属客户', key: 'customerName', ellipsis: true },
|
||||
{ title: '验收人', dataIndex: 'acceptor', key: 'acceptor', ellipsis: true },
|
||||
{ title: '验收时间', key: 'acceptDate', width: 110 },
|
||||
{ title: '安装人', dataIndex: 'installer', key: 'installer', ellipsis: true },
|
||||
{ title: '验收意见', dataIndex: 'acceptOpinion', key: 'acceptOpinion', ellipsis: true },
|
||||
{ title: '操作', key: 'action', width: 140, 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 acceptanceApi.getList(searchForm);
|
||||
if (res.code === 200) {
|
||||
dataSource.value = res.data.list;
|
||||
total.value = res.data.total;
|
||||
}
|
||||
} catch {
|
||||
message.error('获取验收列表失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchCustomerOptions() {
|
||||
try {
|
||||
const res: any = await customerApi.getCustomerOptions();
|
||||
if (res.code === 200) {
|
||||
customerOptions.value = res.data.map((item: any) => ({ label: item.customerName, value: item.id }));
|
||||
}
|
||||
} catch {
|
||||
console.error('获取客户选项失败');
|
||||
}
|
||||
}
|
||||
|
||||
function getCustomerName(customerId: number): string {
|
||||
const found = customerOptions.value.find((c) => c.value === customerId);
|
||||
return found ? found.label : `#${customerId}`;
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
searchForm.page = 1;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.keyword = '';
|
||||
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: AcceptanceInfo) {
|
||||
isEdit.value = true;
|
||||
editId.value = record.id;
|
||||
modalTitle.value = '编辑验收';
|
||||
resetForm();
|
||||
formData.customerId = record.customerId;
|
||||
formData.acceptor = record.acceptor;
|
||||
formData.acceptDate = record.acceptDate ? dayjs(record.acceptDate) : undefined;
|
||||
formData.installer = record.installer;
|
||||
formData.acceptOpinion = record.acceptOpinion;
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
formData.customerId = undefined;
|
||||
formData.acceptor = '';
|
||||
formData.acceptDate = undefined;
|
||||
formData.installer = '';
|
||||
formData.acceptOpinion = '';
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
modalLoading.value = true;
|
||||
try {
|
||||
const submitData: any = {
|
||||
customerId: formData.customerId,
|
||||
acceptor: formData.acceptor,
|
||||
acceptDate: formData.acceptDate ? formData.acceptDate.format('YYYY-MM-DD') : undefined,
|
||||
installer: formData.installer,
|
||||
acceptOpinion: formData.acceptOpinion,
|
||||
};
|
||||
|
||||
let res: any;
|
||||
if (isEdit.value && editId.value) {
|
||||
res = await acceptanceApi.update(editId.value, submitData);
|
||||
} else {
|
||||
res = await acceptanceApi.create(submitData);
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
message.success(isEdit.value ? '更新成功' : '创建成功');
|
||||
modalVisible.value = false;
|
||||
fetchList();
|
||||
} else {
|
||||
message.error(res.message || '操作失败');
|
||||
}
|
||||
} catch {
|
||||
message.error('操作失败');
|
||||
} finally {
|
||||
modalLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
modalVisible.value = false;
|
||||
resetForm();
|
||||
}
|
||||
|
||||
async function handleDelete(id: number) {
|
||||
try {
|
||||
const res: any = await acceptanceApi.delete(id);
|
||||
if (res.code === 200) {
|
||||
message.success('删除成功');
|
||||
fetchList();
|
||||
} else {
|
||||
message.error(res.message || '删除失败');
|
||||
}
|
||||
} catch {
|
||||
message.error('删除失败');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-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>
|
||||
@@ -0,0 +1,448 @@
|
||||
<template>
|
||||
<div class="page-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: 220px"
|
||||
@pressEnter="handleSearch"
|
||||
>
|
||||
<template #prefix><SearchOutlined /></template>
|
||||
</a-input>
|
||||
<a-select v-model:value="searchForm.status" placeholder="状态" allow-clear style="width: 130px">
|
||||
<a-select-option :value="1">有效</a-select-option>
|
||||
<a-select-option :value="0">终止</a-select-option>
|
||||
</a-select>
|
||||
<a-select
|
||||
v-if="isAdmin"
|
||||
v-model:value="searchForm.salesmanId"
|
||||
placeholder="业务员"
|
||||
allow-clear
|
||||
style="width: 150px"
|
||||
: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 === 'customerName'">
|
||||
{{ getCustomerName(record.customerId) }}
|
||||
</template>
|
||||
<template v-if="column.key === 'contractAmount'">
|
||||
{{ formatMoney(record.contractAmount) }}
|
||||
</template>
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="record.status === 1 ? 'green' : 'default'">
|
||||
{{ record.status === 1 ? '有效' : '终止' }}
|
||||
</a-tag>
|
||||
</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="customerId">
|
||||
<a-select
|
||||
v-model:value="formData.customerId"
|
||||
placeholder="请选择客户"
|
||||
:options="customerOptions"
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="鑫潼合同号" name="xtContractNo">
|
||||
<a-input v-model:value="formData.xtContractNo" placeholder="请输入鑫潼合同号" maxlength="64" />
|
||||
</a-form-item>
|
||||
<a-form-item label="合同金额" name="contractAmount">
|
||||
<a-input-number v-model:value="formData.contractAmount" :min="0" style="width: 100%" placeholder="请输入合同金额" />
|
||||
</a-form-item>
|
||||
<a-form-item label="支付周期" name="payCycle">
|
||||
<a-select v-model:value="formData.payCycle" placeholder="请选择支付周期" :options="payCycleOptions" allow-clear />
|
||||
</a-form-item>
|
||||
<a-form-item label="签订日期" name="signDate">
|
||||
<a-date-picker v-model:value="formData.signDate" style="width: 100%" placeholder="请选择签订日期" />
|
||||
</a-form-item>
|
||||
<a-form-item label="起止日期" name="contractDates">
|
||||
<a-range-picker v-model:value="formData.contractDates" style="width: 100%" />
|
||||
</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="isAdmin" label="业务员" name="salesmanId">
|
||||
<a-select
|
||||
v-model:value="formData.salesmanId"
|
||||
placeholder="请选择业务员"
|
||||
:options="salesmanOptions"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="合同说明" name="contractDesc">
|
||||
<a-textarea v-model:value="formData.contractDesc" :rows="2" placeholder="请输入合同说明" maxlength="2000" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { SearchOutlined, ReloadOutlined, PlusOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons-vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { contractApi, type ContractInfo, type ContractQuery } from '@/api/contract';
|
||||
import { customerApi } from '@/api/customer';
|
||||
import { userApi } from '@/api/user';
|
||||
import { useTableScroll } from '@/composables/useTableScroll';
|
||||
import { usePermissionStore } from '@/stores/permission';
|
||||
|
||||
const { wrapperRef, tableHeight } = useTableScroll();
|
||||
const permissionStore = usePermissionStore();
|
||||
const isAdmin = computed(() => permissionStore.hasRole('ADMIN'));
|
||||
|
||||
const payCycleOptions = [
|
||||
{ label: '按月支付', value: '按月' },
|
||||
{ label: '按年支付', value: '按年' },
|
||||
];
|
||||
|
||||
const loading = ref(false);
|
||||
const dataSource = ref<ContractInfo[]>([]);
|
||||
const total = ref(0);
|
||||
const searchForm = reactive<ContractQuery>({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
keyword: '',
|
||||
salesmanId: 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 salesmanOptions = ref<{ label: string; value: number }[]>([]);
|
||||
|
||||
const formData = reactive({
|
||||
customerId: undefined as number | undefined,
|
||||
xtContractNo: '',
|
||||
contractAmount: undefined as number | undefined,
|
||||
payCycle: undefined as string | undefined,
|
||||
signDate: undefined as any,
|
||||
contractDates: [] as any[],
|
||||
status: 1,
|
||||
salesmanId: undefined as number | undefined,
|
||||
contractDesc: '',
|
||||
});
|
||||
|
||||
const formRules = {
|
||||
customerId: [{ required: true, message: '请选择所属客户', trigger: 'change' }],
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{ title: '合同单号', dataIndex: 'contractNo', key: 'contractNo', ellipsis: true, width: 170 },
|
||||
{ title: '鑫潼合同号', dataIndex: 'xtContractNo', key: 'xtContractNo', ellipsis: true },
|
||||
{ title: '所属客户', key: 'customerName', ellipsis: true },
|
||||
{ title: '合同金额', key: 'contractAmount', width: 130 },
|
||||
{ title: '支付周期', dataIndex: 'payCycle', key: 'payCycle', width: 90, align: 'center' as const },
|
||||
{ title: '签订日期', dataIndex: 'signDate', key: 'signDate', width: 110 },
|
||||
{ title: '状态', key: 'status', align: 'center' as const, width: 90 },
|
||||
{ title: '操作', key: 'action', width: 140, 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();
|
||||
fetchSalesmanList();
|
||||
});
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res: any = await contractApi.getList(searchForm);
|
||||
if (res.code === 200) {
|
||||
dataSource.value = res.data.list;
|
||||
total.value = res.data.total;
|
||||
}
|
||||
} catch {
|
||||
message.error('获取合同列表失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchCustomerOptions() {
|
||||
try {
|
||||
const res: any = await customerApi.getCustomerOptions();
|
||||
if (res.code === 200) {
|
||||
customerOptions.value = res.data.map((item: any) => ({ label: item.customerName, value: item.id }));
|
||||
}
|
||||
} catch {
|
||||
console.error('获取客户选项失败');
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
console.error('获取业务员列表失败');
|
||||
}
|
||||
}
|
||||
|
||||
function getCustomerName(customerId: number): string {
|
||||
const found = customerOptions.value.find((c) => c.value === customerId);
|
||||
return found ? found.label : `#${customerId}`;
|
||||
}
|
||||
|
||||
function formatMoney(amount?: number) {
|
||||
if (amount === undefined || amount === null) return '-';
|
||||
return `¥${Number(amount).toLocaleString('zh-CN', { minimumFractionDigits: 2 })}`;
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
searchForm.page = 1;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.keyword = '';
|
||||
searchForm.salesmanId = 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();
|
||||
formData.status = 1;
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function handleEdit(record: ContractInfo) {
|
||||
isEdit.value = true;
|
||||
editId.value = record.id;
|
||||
modalTitle.value = '编辑合同';
|
||||
resetForm();
|
||||
formData.customerId = record.customerId;
|
||||
formData.xtContractNo = record.xtContractNo;
|
||||
formData.contractAmount = record.contractAmount;
|
||||
formData.payCycle = record.payCycle;
|
||||
formData.signDate = record.signDate ? dayjs(record.signDate) : undefined;
|
||||
const start = record.startDate ? dayjs(record.startDate) : undefined;
|
||||
const end = record.endDate ? dayjs(record.endDate) : undefined;
|
||||
formData.contractDates = start && end ? [start, end] : [];
|
||||
formData.status = record.status ?? 1;
|
||||
formData.salesmanId = record.salesmanId;
|
||||
formData.contractDesc = record.contractDesc;
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
formData.customerId = undefined;
|
||||
formData.xtContractNo = '';
|
||||
formData.contractAmount = undefined;
|
||||
formData.payCycle = undefined;
|
||||
formData.signDate = undefined;
|
||||
formData.contractDates = [];
|
||||
formData.status = 1;
|
||||
formData.salesmanId = undefined;
|
||||
formData.contractDesc = '';
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
modalLoading.value = true;
|
||||
try {
|
||||
const submitData: any = {
|
||||
customerId: formData.customerId,
|
||||
xtContractNo: formData.xtContractNo,
|
||||
contractAmount: formData.contractAmount,
|
||||
payCycle: formData.payCycle,
|
||||
signDate: formData.signDate ? formData.signDate.format('YYYY-MM-DD') : undefined,
|
||||
startDate: formData.contractDates?.[0] ? formData.contractDates[0].format('YYYY-MM-DD') : undefined,
|
||||
endDate: formData.contractDates?.[1] ? formData.contractDates[1].format('YYYY-MM-DD') : undefined,
|
||||
status: formData.status,
|
||||
contractDesc: formData.contractDesc,
|
||||
};
|
||||
if (isAdmin.value && formData.salesmanId !== undefined) {
|
||||
submitData.salesmanId = formData.salesmanId;
|
||||
}
|
||||
|
||||
let res: any;
|
||||
if (isEdit.value && editId.value) {
|
||||
res = await contractApi.update(editId.value, submitData);
|
||||
} else {
|
||||
res = await contractApi.create(submitData);
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
message.success(isEdit.value ? '更新成功' : '创建成功');
|
||||
modalVisible.value = false;
|
||||
fetchList();
|
||||
} else {
|
||||
message.error(res.message || '操作失败');
|
||||
}
|
||||
} catch {
|
||||
message.error('操作失败');
|
||||
} finally {
|
||||
modalLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
modalVisible.value = false;
|
||||
resetForm();
|
||||
}
|
||||
|
||||
async function handleDelete(id: number) {
|
||||
try {
|
||||
const res: any = await contractApi.delete(id);
|
||||
if (res.code === 200) {
|
||||
message.success('删除成功');
|
||||
fetchList();
|
||||
} else {
|
||||
message.error(res.message || '删除失败');
|
||||
}
|
||||
} catch {
|
||||
message.error('删除失败');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-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>
|
||||
@@ -0,0 +1,477 @@
|
||||
<template>
|
||||
<div class="page-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: 220px"
|
||||
@pressEnter="handleSearch"
|
||||
>
|
||||
<template #prefix><SearchOutlined /></template>
|
||||
</a-input>
|
||||
<a-select v-model:value="searchForm.status" placeholder="状态" allow-clear style="width: 130px">
|
||||
<a-select-option :value="0">草稿</a-select-option>
|
||||
<a-select-option :value="1">已确认</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 === 'customerName'">
|
||||
{{ getCustomerName(record.customerId) }}
|
||||
</template>
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="record.status === 1 ? 'green' : 'default'">
|
||||
{{ record.status === 1 ? '已确认' : '草稿' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'registerTime'">
|
||||
{{ record.registerTime || '-' }}
|
||||
</template>
|
||||
<template v-if="column.key === 'details'">
|
||||
<a-badge :count="(record.details || []).length" :number-style="{ backgroundColor: '#1677ff' }" />
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleView(record)">
|
||||
<template #icon><SearchOutlined /></template>
|
||||
明细
|
||||
</a-button>
|
||||
<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="820px"
|
||||
@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"
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="登记人" name="registrar">
|
||||
<a-input v-model:value="formData.registrar" placeholder="请输入登记人" maxlength="64" />
|
||||
</a-form-item>
|
||||
<a-form-item label="登记时间" name="registerTime">
|
||||
<a-date-picker v-model:value="formData.registerTime" style="width: 100%" placeholder="请选择登记时间" />
|
||||
</a-form-item>
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-radio-group v-model:value="formData.status">
|
||||
<a-radio :value="0">草稿</a-radio>
|
||||
<a-radio :value="1">已确认</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="方案说明" name="planDesc">
|
||||
<a-textarea v-model:value="formData.planDesc" :rows="2" placeholder="请输入方案说明" maxlength="2000" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<div class="detail-block">
|
||||
<div class="detail-header">
|
||||
<span>方案明细</span>
|
||||
<a-button type="dashed" size="small" @click="addDetailRow">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
添加明细
|
||||
</a-button>
|
||||
</div>
|
||||
<a-table
|
||||
:columns="detailColumns"
|
||||
:data-source="formData.details"
|
||||
row-key="id"
|
||||
:pagination="false"
|
||||
size="small"
|
||||
:scroll="{ y: 200 }"
|
||||
>
|
||||
<template #bodyCell="{ column, record, index }">
|
||||
<template v-if="column.key === 'equipName'">
|
||||
<a-input v-model:value="record.equipName" placeholder="设备名称" maxlength="128" />
|
||||
</template>
|
||||
<template v-if="column.key === 'quantity'">
|
||||
<a-input-number v-model:value="record.quantity" :min="0" style="width: 100%" placeholder="数量" />
|
||||
</template>
|
||||
<template v-if="column.key === 'remark'">
|
||||
<a-input v-model:value="record.remark" placeholder="备注" maxlength="2000" />
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-button type="link" size="small" danger @click="removeDetailRow(index)">删除</a-button>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</a-modal>
|
||||
|
||||
<a-modal v-model:open="viewVisible" title="方案明细" width="700px" :footer="null" @cancel="viewVisible = false">
|
||||
<a-descriptions :column="1" bordered size="small">
|
||||
<a-descriptions-item label="方案单号">{{ viewData.planNo }}</a-descriptions-item>
|
||||
<a-descriptions-item label="所属客户">{{ getCustomerName(viewData.customerId) }}</a-descriptions-item>
|
||||
<a-descriptions-item label="登记人">{{ viewData.registrar || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="登记时间">{{ viewData.registerTime || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="方案说明">{{ viewData.planDesc || '-' }}</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
<a-table
|
||||
:columns="viewDetailColumns"
|
||||
:data-source="viewData.details || []"
|
||||
row-key="id"
|
||||
:pagination="false"
|
||||
size="small"
|
||||
style="margin-top: 16px"
|
||||
/>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { SearchOutlined, ReloadOutlined, PlusOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons-vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { projectPlanApi, type ProjectPlanInfo, type ProjectPlanQuery, type PlanDetail } from '@/api/projectPlan';
|
||||
import { customerApi } from '@/api/customer';
|
||||
import { useTableScroll } from '@/composables/useTableScroll';
|
||||
|
||||
const { wrapperRef, tableHeight } = useTableScroll();
|
||||
|
||||
const loading = ref(false);
|
||||
const dataSource = ref<ProjectPlanInfo[]>([]);
|
||||
const total = ref(0);
|
||||
const searchForm = reactive<ProjectPlanQuery>({ page: 1, pageSize: 10, keyword: '', 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 viewVisible = ref(false);
|
||||
const viewData = reactive<Partial<ProjectPlanInfo>>({});
|
||||
let detailSeq = 1;
|
||||
|
||||
const formData = reactive({
|
||||
customerId: undefined as number | undefined,
|
||||
registrar: '',
|
||||
registerTime: undefined as any,
|
||||
status: 0,
|
||||
planDesc: '',
|
||||
details: [] as any[],
|
||||
});
|
||||
|
||||
const formRules = {
|
||||
customerId: [{ required: true, message: '请选择所属客户', trigger: 'change' }],
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{ title: '方案单号', dataIndex: 'planNo', key: 'planNo', ellipsis: true, width: 170 },
|
||||
{ title: '所属客户', key: 'customerName', ellipsis: true },
|
||||
{ title: '登记人', dataIndex: 'registrar', key: 'registrar', ellipsis: true },
|
||||
{ title: '登记时间', key: 'registerTime', width: 110 },
|
||||
{ title: '明细数', key: 'details', align: 'center' as const, width: 90 },
|
||||
{ title: '状态', key: 'status', align: 'center' as const, width: 90 },
|
||||
{ title: '操作', key: 'action', width: 180, fixed: 'right' as const },
|
||||
];
|
||||
|
||||
const detailColumns = [
|
||||
{ title: '设备名称', key: 'equipName', dataIndex: 'equipName' },
|
||||
{ title: '数量', key: 'quantity', dataIndex: 'quantity', width: 120 },
|
||||
{ title: '备注', key: 'remark', dataIndex: 'remark' },
|
||||
{ title: '操作', key: 'action', width: 70, align: 'center' as const },
|
||||
];
|
||||
|
||||
const viewDetailColumns = [
|
||||
{ title: '设备名称', dataIndex: 'equipName', key: 'equipName' },
|
||||
{ title: '数量', dataIndex: 'quantity', key: 'quantity', width: 120 },
|
||||
{ title: '备注', dataIndex: 'remark', key: 'remark', ellipsis: true },
|
||||
];
|
||||
|
||||
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 projectPlanApi.getList(searchForm);
|
||||
if (res.code === 200) {
|
||||
dataSource.value = res.data.list;
|
||||
total.value = res.data.total;
|
||||
}
|
||||
} catch {
|
||||
message.error('获取方案列表失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchCustomerOptions() {
|
||||
try {
|
||||
const res: any = await customerApi.getCustomerOptions();
|
||||
if (res.code === 200) {
|
||||
customerOptions.value = res.data.map((item: any) => ({ label: item.customerName, value: item.id }));
|
||||
}
|
||||
} catch {
|
||||
console.error('获取客户选项失败');
|
||||
}
|
||||
}
|
||||
|
||||
function getCustomerName(customerId: number): string {
|
||||
const found = customerOptions.value.find((c) => c.value === customerId);
|
||||
return found ? found.label : `#${customerId}`;
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
searchForm.page = 1;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.keyword = '';
|
||||
searchForm.status = undefined as any;
|
||||
searchForm.page = 1;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleTableChange(pag: any) {
|
||||
searchForm.page = pag.current;
|
||||
searchForm.pageSize = pag.pageSize;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function addDetailRow() {
|
||||
formData.details.push({ id: `new-${detailSeq++}`, equipName: '', quantity: undefined, remark: '' });
|
||||
}
|
||||
|
||||
function removeDetailRow(index: number) {
|
||||
formData.details.splice(index, 1);
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
isEdit.value = false;
|
||||
editId.value = null;
|
||||
modalTitle.value = '新增方案';
|
||||
resetForm();
|
||||
formData.details = [];
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function handleEdit(record: ProjectPlanInfo) {
|
||||
isEdit.value = true;
|
||||
editId.value = record.id;
|
||||
modalTitle.value = '编辑方案';
|
||||
resetForm();
|
||||
formData.customerId = record.customerId;
|
||||
formData.registrar = record.registrar;
|
||||
formData.registerTime = record.registerTime ? dayjs(record.registerTime) : undefined;
|
||||
formData.status = record.status ?? 0;
|
||||
formData.planDesc = record.planDesc;
|
||||
formData.details = (record.details || []).map((d) => ({
|
||||
id: d.id ?? `new-${detailSeq++}`,
|
||||
equipName: d.equipName,
|
||||
quantity: d.quantity,
|
||||
remark: d.remark,
|
||||
}));
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function handleView(record: ProjectPlanInfo) {
|
||||
Object.assign(viewData, record);
|
||||
viewVisible.value = true;
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
formData.customerId = undefined;
|
||||
formData.registrar = '';
|
||||
formData.registerTime = undefined;
|
||||
formData.status = 0;
|
||||
formData.planDesc = '';
|
||||
formData.details = [];
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
modalLoading.value = true;
|
||||
try {
|
||||
const submitData: any = {
|
||||
customerId: formData.customerId,
|
||||
registrar: formData.registrar,
|
||||
registerTime: formData.registerTime ? formData.registerTime.format('YYYY-MM-DD') : undefined,
|
||||
status: formData.status,
|
||||
planDesc: formData.planDesc,
|
||||
details: formData.details
|
||||
.filter((d: any) => d.equipName || d.quantity)
|
||||
.map((d: any) => ({ id: Number(d.id) || undefined, equipName: d.equipName, quantity: d.quantity, remark: d.remark })),
|
||||
};
|
||||
|
||||
let res: any;
|
||||
if (isEdit.value && editId.value) {
|
||||
res = await projectPlanApi.update(editId.value, submitData);
|
||||
} else {
|
||||
res = await projectPlanApi.create(submitData);
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
message.success(isEdit.value ? '更新成功' : '创建成功');
|
||||
modalVisible.value = false;
|
||||
fetchList();
|
||||
} else {
|
||||
message.error(res.message || '操作失败');
|
||||
}
|
||||
} catch {
|
||||
message.error('操作失败');
|
||||
} finally {
|
||||
modalLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
modalVisible.value = false;
|
||||
resetForm();
|
||||
}
|
||||
|
||||
async function handleDelete(id: number) {
|
||||
try {
|
||||
const res: any = await projectPlanApi.delete(id);
|
||||
if (res.code === 200) {
|
||||
message.success('删除成功');
|
||||
fetchList();
|
||||
} else {
|
||||
message.error(res.message || '删除失败');
|
||||
}
|
||||
} catch {
|
||||
message.error('删除失败');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-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;
|
||||
}
|
||||
.detail-block {
|
||||
margin-top: 8px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
padding-top: 12px;
|
||||
}
|
||||
.detail-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,380 @@
|
||||
<template>
|
||||
<div class="page-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: 220px"
|
||||
@pressEnter="handleSearch"
|
||||
>
|
||||
<template #prefix><SearchOutlined /></template>
|
||||
</a-input>
|
||||
<a-select v-model:value="searchForm.status" placeholder="状态" allow-clear style="width: 130px">
|
||||
<a-select-option :value="0">已派发</a-select-option>
|
||||
<a-select-option :value="1">已接单</a-select-option>
|
||||
<a-select-option :value="2">已完成</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 === 'customerName'">
|
||||
{{ getCustomerName(record.customerId) }}
|
||||
</template>
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="getStatusColor(record.status)">{{ getStatusText(record.status) }}</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'surveyTime'">
|
||||
{{ record.surveyTime ? formatDate(record.surveyTime) : '-' }}
|
||||
</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="620px"
|
||||
@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"
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="勘察人" name="surveyor">
|
||||
<a-input v-model:value="formData.surveyor" placeholder="请输入勘察人" maxlength="64" />
|
||||
</a-form-item>
|
||||
<a-form-item label="勘察时间" name="surveyTime">
|
||||
<a-date-picker v-model:value="formData.surveyTime" style="width: 100%" placeholder="请选择勘察时间" />
|
||||
</a-form-item>
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-radio-group v-model:value="formData.status">
|
||||
<a-radio :value="0">已派发</a-radio>
|
||||
<a-radio :value="1">已接单</a-radio>
|
||||
<a-radio :value="2">已完成</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="备注" name="remark">
|
||||
<a-textarea v-model:value="formData.remark" :rows="3" placeholder="请输入备注" maxlength="2000" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { SearchOutlined, ReloadOutlined, PlusOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons-vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { surveyOrderApi, type SurveyOrderInfo, type SurveyOrderQuery } from '@/api/surveyOrder';
|
||||
import { customerApi } from '@/api/customer';
|
||||
import { useTableScroll } from '@/composables/useTableScroll';
|
||||
|
||||
const { wrapperRef, tableHeight } = useTableScroll();
|
||||
|
||||
const loading = ref(false);
|
||||
const dataSource = ref<SurveyOrderInfo[]>([]);
|
||||
const total = ref(0);
|
||||
const searchForm = reactive<SurveyOrderQuery>({ page: 1, pageSize: 10, keyword: '', 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 formData = reactive({
|
||||
customerId: undefined as number | undefined,
|
||||
surveyor: '',
|
||||
surveyTime: undefined as any,
|
||||
status: 0,
|
||||
remark: '',
|
||||
});
|
||||
|
||||
const formRules = {
|
||||
customerId: [{ required: true, message: '请选择所属客户', trigger: 'change' }],
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{ title: '勘察单号', dataIndex: 'surveyNo', key: 'surveyNo', ellipsis: true, width: 170 },
|
||||
{ title: '所属客户', key: 'customerName', ellipsis: true },
|
||||
{ title: '勘察人', dataIndex: 'surveyor', key: 'surveyor', ellipsis: true },
|
||||
{ title: '勘察时间', key: 'surveyTime', width: 110 },
|
||||
{ title: '状态', key: 'status', align: 'center' as const, width: 100 },
|
||||
{ title: '备注', dataIndex: 'remark', key: 'remark', ellipsis: true },
|
||||
{ title: '操作', key: 'action', width: 140, 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 surveyOrderApi.getList(searchForm);
|
||||
if (res.code === 200) {
|
||||
dataSource.value = res.data.list;
|
||||
total.value = res.data.total;
|
||||
}
|
||||
} catch {
|
||||
message.error('获取勘察单列表失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchCustomerOptions() {
|
||||
try {
|
||||
const res: any = await customerApi.getCustomerOptions();
|
||||
if (res.code === 200) {
|
||||
customerOptions.value = res.data.map((item: any) => ({ label: item.customerName, value: item.id }));
|
||||
}
|
||||
} catch {
|
||||
console.error('获取客户选项失败');
|
||||
}
|
||||
}
|
||||
|
||||
function getCustomerName(customerId: number): string {
|
||||
const found = customerOptions.value.find((c) => c.value === customerId);
|
||||
return found ? found.label : `#${customerId}`;
|
||||
}
|
||||
|
||||
function getStatusColor(status: number): string {
|
||||
const map: Record<number, string> = { 0: 'blue', 1: 'orange', 2: 'green' };
|
||||
return map[status] || 'default';
|
||||
}
|
||||
|
||||
function getStatusText(status: number): string {
|
||||
const map: Record<number, string> = { 0: '已派发', 1: '已接单', 2: '已完成' };
|
||||
return map[status] || '未知';
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
searchForm.page = 1;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.keyword = '';
|
||||
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: SurveyOrderInfo) {
|
||||
isEdit.value = true;
|
||||
editId.value = record.id;
|
||||
modalTitle.value = '编辑勘察单';
|
||||
resetForm();
|
||||
formData.customerId = record.customerId;
|
||||
formData.surveyor = record.surveyor;
|
||||
formData.surveyTime = record.surveyTime ? dayjs(record.surveyTime) : undefined;
|
||||
formData.status = record.status ?? 0;
|
||||
formData.remark = record.remark;
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
formData.customerId = undefined;
|
||||
formData.surveyor = '';
|
||||
formData.surveyTime = undefined;
|
||||
formData.status = 0;
|
||||
formData.remark = '';
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
modalLoading.value = true;
|
||||
try {
|
||||
const submitData: any = {
|
||||
customerId: formData.customerId,
|
||||
surveyor: formData.surveyor,
|
||||
surveyTime: formData.surveyTime ? formData.surveyTime.format('YYYY-MM-DD') : undefined,
|
||||
status: formData.status,
|
||||
remark: formData.remark,
|
||||
};
|
||||
|
||||
let res: any;
|
||||
if (isEdit.value && editId.value) {
|
||||
res = await surveyOrderApi.update(editId.value, submitData);
|
||||
} else {
|
||||
res = await surveyOrderApi.create(submitData);
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
message.success(isEdit.value ? '更新成功' : '创建成功');
|
||||
modalVisible.value = false;
|
||||
fetchList();
|
||||
} else {
|
||||
message.error(res.message || '操作失败');
|
||||
}
|
||||
} catch {
|
||||
message.error('操作失败');
|
||||
} finally {
|
||||
modalLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
modalVisible.value = false;
|
||||
resetForm();
|
||||
}
|
||||
|
||||
async function handleDelete(id: number) {
|
||||
try {
|
||||
const res: any = await surveyOrderApi.delete(id);
|
||||
if (res.code === 200) {
|
||||
message.success('删除成功');
|
||||
fetchList();
|
||||
} else {
|
||||
message.error(res.message || '删除失败');
|
||||
}
|
||||
} catch {
|
||||
message.error('删除失败');
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(str: string) {
|
||||
return dayjs(str).format('YYYY-MM-DD');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-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>
|
||||
@@ -0,0 +1,361 @@
|
||||
<template>
|
||||
<div class="page-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: 220px"
|
||||
@pressEnter="handleSearch"
|
||||
>
|
||||
<template #prefix><SearchOutlined /></template>
|
||||
</a-input>
|
||||
<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 === 'customerName'">
|
||||
{{ getCustomerName(record.customerId) }}
|
||||
</template>
|
||||
<template v-if="column.key === 'visitTime'">
|
||||
{{ record.visitTime || '-' }}
|
||||
</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="620px"
|
||||
@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"
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="联系人" name="contactPerson">
|
||||
<a-input v-model:value="formData.contactPerson" placeholder="请输入联系人" maxlength="64" />
|
||||
</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="visitor">
|
||||
<a-input v-model:value="formData.visitor" placeholder="请输入回访人" maxlength="64" />
|
||||
</a-form-item>
|
||||
<a-form-item label="回访时间" name="visitTime">
|
||||
<a-date-picker v-model:value="formData.visitTime" style="width: 100%" placeholder="请选择回访时间" />
|
||||
</a-form-item>
|
||||
<a-form-item label="客户意见" name="customerOpinion">
|
||||
<a-textarea v-model:value="formData.customerOpinion" :rows="3" placeholder="请输入客户意见" maxlength="2000" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { SearchOutlined, ReloadOutlined, PlusOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons-vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { visitRecordApi, type VisitRecordInfo, type VisitRecordQuery } from '@/api/visitRecord';
|
||||
import { customerApi } from '@/api/customer';
|
||||
import { useTableScroll } from '@/composables/useTableScroll';
|
||||
|
||||
const { wrapperRef, tableHeight } = useTableScroll();
|
||||
|
||||
const loading = ref(false);
|
||||
const dataSource = ref<VisitRecordInfo[]>([]);
|
||||
const total = ref(0);
|
||||
const searchForm = reactive<VisitRecordQuery>({ page: 1, pageSize: 10, keyword: '' });
|
||||
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 formData = reactive({
|
||||
customerId: undefined as number | undefined,
|
||||
contactPerson: '',
|
||||
contactPhone: '',
|
||||
visitor: '',
|
||||
visitTime: undefined as any,
|
||||
customerOpinion: '',
|
||||
});
|
||||
|
||||
const formRules = {
|
||||
customerId: [{ required: true, message: '请选择所属客户', trigger: 'change' }],
|
||||
contactPhone: [{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' }],
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{ title: '所属客户', key: 'customerName', ellipsis: true },
|
||||
{ title: '联系人', dataIndex: 'contactPerson', key: 'contactPerson', ellipsis: true },
|
||||
{ title: '联系电话', dataIndex: 'contactPhone', key: 'contactPhone', ellipsis: true },
|
||||
{ title: '回访人', dataIndex: 'visitor', key: 'visitor', ellipsis: true },
|
||||
{ title: '回访时间', key: 'visitTime', width: 110 },
|
||||
{ title: '客户意见', dataIndex: 'customerOpinion', key: 'customerOpinion', ellipsis: true },
|
||||
{ title: '操作', key: 'action', width: 140, 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 visitRecordApi.getList(searchForm);
|
||||
if (res.code === 200) {
|
||||
dataSource.value = res.data.list;
|
||||
total.value = res.data.total;
|
||||
}
|
||||
} catch {
|
||||
message.error('获取回访记录失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchCustomerOptions() {
|
||||
try {
|
||||
const res: any = await customerApi.getCustomerOptions();
|
||||
if (res.code === 200) {
|
||||
customerOptions.value = res.data.map((item: any) => ({ label: item.customerName, value: item.id }));
|
||||
}
|
||||
} catch {
|
||||
console.error('获取客户选项失败');
|
||||
}
|
||||
}
|
||||
|
||||
function getCustomerName(customerId: number): string {
|
||||
const found = customerOptions.value.find((c) => c.value === customerId);
|
||||
return found ? found.label : `#${customerId}`;
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
searchForm.page = 1;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.keyword = '';
|
||||
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: VisitRecordInfo) {
|
||||
isEdit.value = true;
|
||||
editId.value = record.id;
|
||||
modalTitle.value = '编辑回访';
|
||||
resetForm();
|
||||
formData.customerId = record.customerId;
|
||||
formData.contactPerson = record.contactPerson;
|
||||
formData.contactPhone = record.contactPhone;
|
||||
formData.visitor = record.visitor;
|
||||
formData.visitTime = record.visitTime ? dayjs(record.visitTime) : undefined;
|
||||
formData.customerOpinion = record.customerOpinion;
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
formData.customerId = undefined;
|
||||
formData.contactPerson = '';
|
||||
formData.contactPhone = '';
|
||||
formData.visitor = '';
|
||||
formData.visitTime = undefined;
|
||||
formData.customerOpinion = '';
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
modalLoading.value = true;
|
||||
try {
|
||||
const submitData: any = {
|
||||
customerId: formData.customerId,
|
||||
contactPerson: formData.contactPerson,
|
||||
contactPhone: formData.contactPhone,
|
||||
visitor: formData.visitor,
|
||||
visitTime: formData.visitTime ? formData.visitTime.format('YYYY-MM-DD') : undefined,
|
||||
customerOpinion: formData.customerOpinion,
|
||||
};
|
||||
|
||||
let res: any;
|
||||
if (isEdit.value && editId.value) {
|
||||
res = await visitRecordApi.update(editId.value, submitData);
|
||||
} else {
|
||||
res = await visitRecordApi.create(submitData);
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
message.success(isEdit.value ? '更新成功' : '创建成功');
|
||||
modalVisible.value = false;
|
||||
fetchList();
|
||||
} else {
|
||||
message.error(res.message || '操作失败');
|
||||
}
|
||||
} catch {
|
||||
message.error('操作失败');
|
||||
} finally {
|
||||
modalLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
modalVisible.value = false;
|
||||
resetForm();
|
||||
}
|
||||
|
||||
async function handleDelete(id: number) {
|
||||
try {
|
||||
const res: any = await visitRecordApi.delete(id);
|
||||
if (res.code === 200) {
|
||||
message.success('删除成功');
|
||||
fetchList();
|
||||
} else {
|
||||
message.error(res.message || '删除失败');
|
||||
}
|
||||
} catch {
|
||||
message.error('删除失败');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-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>
|
||||
+31
-5
@@ -47,7 +47,7 @@ SELECT setval('sys_role_id_seq', 4, true);
|
||||
-- ------------------------------------------------------------
|
||||
INSERT INTO sys_menu (id, parent_id, menu_name, menu_path, component, icon, sort_order, menu_type, permission, visible) VALUES
|
||||
(1, NULL, '客户管理', '/customer', NULL, 'TeamOutlined', 1, 1, 'customer:list', 1),
|
||||
(2, NULL, '项目管理', '/project', NULL, 'ProjectOutlined', 2, 2, 'project:list', 1),
|
||||
(2, NULL, '项目管理', '/project', NULL, 'ProjectOutlined', 2, 1, 'project:list', 1),
|
||||
(3, NULL, '运营派单', '/dispatch', NULL, 'ScheduleOutlined', 3, 2, 'dispatch:list', 1),
|
||||
(4, NULL, '设备监控', '/device', NULL, 'MonitorOutlined', 4, 2, 'device:list', 1),
|
||||
(5, NULL, '统计报表', '/report', NULL, 'BarChartOutlined', 5, 2, 'report:list', 1),
|
||||
@@ -106,8 +106,32 @@ ON CONFLICT (id) DO UPDATE SET
|
||||
permission = EXCLUDED.permission,
|
||||
visible = EXCLUDED.visible;
|
||||
|
||||
-- 初始菜单(客户扩展子菜单 + 项目域子菜单,id:24-31)
|
||||
-- 客户域:客户拜访(customer:visit) / 客户建筑(customer:building) / 客户房间(customer:room)
|
||||
-- 项目域:项目勘察(project:survey) / 项目方案(project:plan) / 合同管理(project:contract)
|
||||
-- 项目验收(project:acceptance) / 回访记录(project:visit)
|
||||
INSERT INTO sys_menu (id, parent_id, menu_name, menu_path, component, icon, sort_order, menu_type, permission, visible) VALUES
|
||||
(24, 1, '客户拜访', '/customer/visit', 'customer/visit/index', 'AccountBookOutlined', 4, 2, 'customer:visit', 1),
|
||||
(25, 1, '客户建筑', '/customer/building', 'customer/building/index', 'HomeOutlined', 5, 2, 'customer:building',1),
|
||||
(26, 1, '客户房间', '/customer/room', 'customer/room/index', 'ApartmentOutlined', 6, 2, 'customer:room', 1),
|
||||
(27, 2, '项目勘察', '/project/survey', 'project/survey/index', 'CarryOutOutlined', 1, 2, 'project:survey', 1),
|
||||
(28, 2, '项目方案', '/project/plan', 'project/plan/index', 'FileSearchOutlined', 2, 2, 'project:plan', 1),
|
||||
(29, 2, '合同管理', '/project/contract', 'project/contract/index', 'FileDoneOutlined', 3, 2, 'project:contract', 1),
|
||||
(30, 2, '项目验收', '/project/acceptance', 'project/acceptance/index', 'ThunderboltOutlined', 4, 2, 'project:acceptance',1),
|
||||
(31, 2, '回访记录', '/project/visit', 'project/visit/index', 'AuditOutlined', 5, 2, 'project:visit', 1)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
parent_id = EXCLUDED.parent_id,
|
||||
menu_name = EXCLUDED.menu_name,
|
||||
menu_path = EXCLUDED.menu_path,
|
||||
component = EXCLUDED.component,
|
||||
icon = EXCLUDED.icon,
|
||||
sort_order = EXCLUDED.sort_order,
|
||||
menu_type = EXCLUDED.menu_type,
|
||||
permission = EXCLUDED.permission,
|
||||
visible = EXCLUDED.visible;
|
||||
|
||||
-- 重置 sys_menu 自增序列
|
||||
SELECT setval('sys_menu_id_seq', 23, true);
|
||||
SELECT setval('sys_menu_id_seq', 31, true);
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 3. 初始字典(sys_dict 无业务唯一键,清空重建以支持幂等执行)
|
||||
@@ -160,18 +184,20 @@ INSERT INTO sys_user_role (user_id, role_id) VALUES (1, 1)
|
||||
ON CONFLICT (user_id, role_id) DO NOTHING;
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 6. 角色-菜单绑定:ADMIN 拥有全部 23 条菜单,SALESMAN 拥有客户域+项目+统计报表
|
||||
-- 6. 角色-菜单绑定:ADMIN 拥有全部 31 条菜单,SALESMAN 拥有客户域+项目+统计报表
|
||||
-- WORKER、CUSTOMER 不绑定任何菜单
|
||||
-- ------------------------------------------------------------
|
||||
INSERT INTO sys_role_menu (role_id, menu_id) VALUES
|
||||
(1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (1, 8), (1, 9), (1, 10), (1, 11),
|
||||
(1, 12), (1, 13), (1, 14), (1, 15), (1, 16), (1, 17), (1, 18), (1, 19), (1, 20), (1, 21), (1, 22), (1, 23),
|
||||
(1, 24), (1, 25), (1, 26), (1, 27), (1, 28), (1, 29), (1, 30), (1, 31),
|
||||
(2, 1), (2, 2), (2, 5),
|
||||
(2, 12), (2, 13), (2, 14), (2, 15), (2, 16), (2, 17), (2, 18), (2, 19), (2, 20), (2, 21), (2, 22), (2, 23)
|
||||
(2, 12), (2, 13), (2, 14), (2, 15), (2, 16), (2, 17), (2, 18), (2, 19), (2, 20), (2, 21), (2, 22), (2, 23),
|
||||
(2, 24), (2, 25), (2, 26), (2, 27), (2, 28), (2, 29), (2, 30), (2, 31)
|
||||
ON CONFLICT (role_id, menu_id) DO NOTHING;
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- ============================================================
|
||||
-- 完 · 7 张系统表种子数据(4 角色 + 23 菜单 + 20 字典 + 1 管理员 + 绑定关系)
|
||||
-- 完 · 7 张系统表种子数据(4 角色 + 31 菜单 + 20 字典 + 1 管理员 + 绑定关系)
|
||||
-- ============================================================
|
||||
@@ -0,0 +1,308 @@
|
||||
-- ============================================================
|
||||
-- 增量脚本:客户管理扩展 + 项目管理模块表结构
|
||||
-- 日期:2026-08-26
|
||||
-- 数据库:PostgreSQL 15+ (xintong)
|
||||
-- 说明:新建客户域扩展(客户拜访/建筑/房间)与项目域
|
||||
-- (勘察派单/方案+明细/合同/验收/回访)共 9 张表。
|
||||
-- 沿用 Sequelize Entity 定义 + 幂等建表,可重复执行。
|
||||
--
|
||||
-- 兼容性说明:
|
||||
-- - 使用 CREATE TABLE IF NOT EXISTS,已有表自动跳过。
|
||||
-- - project_plan_detail 对 customer_equipment / smart_device 仅预留
|
||||
-- equip_id / smart_device_id 数值列,不建外键(两表后续迭代实现)。
|
||||
-- - 对已实现表(customer / customer_department / customer_building /
|
||||
-- project_plan / contract)保留外键约束。
|
||||
--
|
||||
-- 用法:
|
||||
-- psql -U postgres -d xintong -f 20260826_project_customer_ext.sql
|
||||
-- ============================================================
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- 依赖:trigger_set_updated_at() 已由 db_init.sql 创建(若缺失则这里兜底创建)
|
||||
CREATE OR REPLACE FUNCTION trigger_set_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- ============================================================
|
||||
-- 1. 客户拜访信息表 customer_visit
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS customer_visit (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
visit_no VARCHAR(64) NOT NULL UNIQUE,
|
||||
customer_id BIGINT NOT NULL REFERENCES customer(id),
|
||||
visit_person VARCHAR(64) NOT NULL,
|
||||
visit_phone VARCHAR(20),
|
||||
visit_content TEXT,
|
||||
visit_time DATE,
|
||||
salesman_id BIGINT,
|
||||
will_plan SMALLINT DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
COMMENT ON TABLE customer_visit IS '客户拜访信息表';
|
||||
COMMENT ON COLUMN customer_visit.id IS '自增主键';
|
||||
COMMENT ON COLUMN customer_visit.visit_no IS '拜访单号(唯一)';
|
||||
COMMENT ON COLUMN customer_visit.customer_id IS '客户 ID(→ customer)';
|
||||
COMMENT ON COLUMN customer_visit.visit_person IS '拜访人';
|
||||
COMMENT ON COLUMN customer_visit.visit_phone IS '拜访人电话';
|
||||
COMMENT ON COLUMN customer_visit.visit_content IS '拜访内容';
|
||||
COMMENT ON COLUMN customer_visit.visit_time IS '拜访时间';
|
||||
COMMENT ON COLUMN customer_visit.salesman_id IS '业务员';
|
||||
COMMENT ON COLUMN customer_visit.will_plan IS '是否做方案:0不同意 1同意';
|
||||
COMMENT ON COLUMN customer_visit.created_at IS '创建时间';
|
||||
CREATE INDEX IF NOT EXISTS idx_visit_customer ON customer_visit (customer_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_visit_salesman ON customer_visit (salesman_id);
|
||||
|
||||
-- ============================================================
|
||||
-- 2. 客户智电改造建筑信息表 customer_building
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS customer_building (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
customer_id BIGINT NOT NULL REFERENCES customer(id),
|
||||
building_name VARCHAR(128) NOT NULL,
|
||||
basic_info TEXT,
|
||||
purpose VARCHAR(128),
|
||||
floor_count INT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
COMMENT ON TABLE customer_building IS '客户智电改造建筑信息表';
|
||||
COMMENT ON COLUMN customer_building.id IS '自增主键';
|
||||
COMMENT ON COLUMN customer_building.customer_id IS '客户 ID(→ customer)';
|
||||
COMMENT ON COLUMN customer_building.building_name IS '建筑名称';
|
||||
COMMENT ON COLUMN customer_building.basic_info IS '基本情况';
|
||||
COMMENT ON COLUMN customer_building.purpose IS '用途';
|
||||
COMMENT ON COLUMN customer_building.floor_count IS '层数';
|
||||
COMMENT ON COLUMN customer_building.created_at IS '创建时间';
|
||||
CREATE INDEX IF NOT EXISTS idx_building_customer ON customer_building (customer_id);
|
||||
|
||||
-- ============================================================
|
||||
-- 3. 客户建筑房间信息表 customer_room
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS customer_room (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
room_code VARCHAR(64) NOT NULL,
|
||||
customer_id BIGINT NOT NULL REFERENCES customer(id),
|
||||
room_name VARCHAR(128) NOT NULL,
|
||||
building_id BIGINT NOT NULL REFERENCES customer_building(id),
|
||||
floor_no VARCHAR(16),
|
||||
room_purpose VARCHAR(128),
|
||||
dept_id BIGINT REFERENCES customer_department(id),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
COMMENT ON TABLE customer_room IS '客户建筑房间信息表';
|
||||
COMMENT ON COLUMN customer_room.id IS '自增主键';
|
||||
COMMENT ON COLUMN customer_room.room_code IS '房间编码';
|
||||
COMMENT ON COLUMN customer_room.customer_id IS '客户 ID(→ customer)';
|
||||
COMMENT ON COLUMN customer_room.room_name IS '房间名称';
|
||||
COMMENT ON COLUMN customer_room.building_id IS '所属建筑 ID(→ customer_building)';
|
||||
COMMENT ON COLUMN customer_room.floor_no IS '所在楼层';
|
||||
COMMENT ON COLUMN customer_room.room_purpose IS '房间用途';
|
||||
COMMENT ON COLUMN customer_room.dept_id IS '房间所属客户部门(→ customer_department)';
|
||||
COMMENT ON COLUMN customer_room.created_at IS '创建时间';
|
||||
CREATE INDEX IF NOT EXISTS idx_room_customer ON customer_room (customer_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_room_building ON customer_room (building_id);
|
||||
|
||||
-- ============================================================
|
||||
-- 4. 智电改造方案表 project_plan
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS project_plan (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
plan_no VARCHAR(64) NOT NULL UNIQUE,
|
||||
customer_id BIGINT NOT NULL REFERENCES customer(id),
|
||||
registrar VARCHAR(64),
|
||||
register_time DATE,
|
||||
plan_desc TEXT,
|
||||
plan_file_url VARCHAR(512),
|
||||
status SMALLINT NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
COMMENT ON TABLE project_plan IS '智电改造方案信息表';
|
||||
COMMENT ON COLUMN project_plan.id IS '自增主键';
|
||||
COMMENT ON COLUMN project_plan.plan_no IS '方案单号(唯一)';
|
||||
COMMENT ON COLUMN project_plan.customer_id IS '客户 ID(→ customer)';
|
||||
COMMENT ON COLUMN project_plan.registrar IS '方案登记人(业务员)';
|
||||
COMMENT ON COLUMN project_plan.register_time IS '方案登记时间';
|
||||
COMMENT ON COLUMN project_plan.plan_desc IS '方案说明';
|
||||
COMMENT ON COLUMN project_plan.plan_file_url IS '方案最终文件';
|
||||
COMMENT ON COLUMN project_plan.status IS '状态:0草稿 1已确认';
|
||||
COMMENT ON COLUMN project_plan.created_at IS '创建时间';
|
||||
CREATE INDEX IF NOT EXISTS idx_plan_customer ON project_plan (customer_id);
|
||||
|
||||
-- ============================================================
|
||||
-- 5. 项目方案智电设备明细表 project_plan_detail
|
||||
-- equip_id / smart_device_id 仅预留数值列,不建外键
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS project_plan_detail (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
plan_id BIGINT NOT NULL REFERENCES project_plan(id) ON DELETE CASCADE,
|
||||
customer_id BIGINT NOT NULL REFERENCES customer(id),
|
||||
equip_name VARCHAR(128),
|
||||
equip_id BIGINT,
|
||||
smart_device_id BIGINT,
|
||||
quantity NUMERIC(10,2) NOT NULL DEFAULT 1,
|
||||
remark TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 兼容已有表:若 project_plan_detail 表已存在(旧脚本建表)则补齐 equip_name 列
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'project_plan_detail' AND column_name = 'equip_name'
|
||||
) THEN
|
||||
ALTER TABLE project_plan_detail ADD COLUMN equip_name VARCHAR(128);
|
||||
RAISE NOTICE 'project_plan_detail: 已新增列 equip_name';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
COMMENT ON TABLE project_plan_detail IS '项目方案智电设备明细信息表';
|
||||
COMMENT ON COLUMN project_plan_detail.id IS '自增主键';
|
||||
COMMENT ON COLUMN project_plan_detail.plan_id IS '方案 ID(→ project_plan)';
|
||||
COMMENT ON COLUMN project_plan_detail.customer_id IS '客户 ID(→ customer)';
|
||||
COMMENT ON COLUMN project_plan_detail.equip_name IS '设备名称(设备域未实现前为自由文本)';
|
||||
COMMENT ON COLUMN project_plan_detail.equip_id IS '客户设备 ID(预留)';
|
||||
COMMENT ON COLUMN project_plan_detail.smart_device_id IS '智电设备型号 ID(预留)';
|
||||
COMMENT ON COLUMN project_plan_detail.quantity IS '数量';
|
||||
COMMENT ON COLUMN project_plan_detail.remark IS '备注';
|
||||
COMMENT ON COLUMN project_plan_detail.created_at IS '创建时间';
|
||||
CREATE INDEX IF NOT EXISTS idx_pd_plan ON project_plan_detail (plan_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_pd_customer ON project_plan_detail (customer_id);
|
||||
|
||||
-- ============================================================
|
||||
-- 6. 智电项目合同表 contract
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS contract (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
contract_no VARCHAR(64) NOT NULL UNIQUE,
|
||||
xt_contract_no VARCHAR(64),
|
||||
customer_id BIGINT NOT NULL REFERENCES customer(id),
|
||||
contract_amount NUMERIC(14,2),
|
||||
pay_cycle VARCHAR(32),
|
||||
salesman_id BIGINT,
|
||||
sign_date DATE,
|
||||
contract_desc TEXT,
|
||||
start_date DATE,
|
||||
end_date DATE,
|
||||
plan_id BIGINT REFERENCES project_plan(id),
|
||||
contract_file_url VARCHAR(512),
|
||||
status SMALLINT NOT NULL DEFAULT 1,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
COMMENT ON TABLE contract IS '智电项目合同信息表';
|
||||
COMMENT ON COLUMN contract.id IS '自增主键';
|
||||
COMMENT ON COLUMN contract.contract_no IS '合同单号(唯一)';
|
||||
COMMENT ON COLUMN contract.xt_contract_no IS '鑫潼合同号(XT+年份+序号)';
|
||||
COMMENT ON COLUMN contract.customer_id IS '客户 ID(→ customer)';
|
||||
COMMENT ON COLUMN contract.contract_amount IS '合同金额';
|
||||
COMMENT ON COLUMN contract.pay_cycle IS '支付周期:按月/按年';
|
||||
COMMENT ON COLUMN contract.salesman_id IS '业务员';
|
||||
COMMENT ON COLUMN contract.sign_date IS '签订日期';
|
||||
COMMENT ON COLUMN contract.contract_desc IS '合同说明';
|
||||
COMMENT ON COLUMN contract.start_date IS '开始日期';
|
||||
COMMENT ON COLUMN contract.end_date IS '结束日期';
|
||||
COMMENT ON COLUMN contract.plan_id IS '方案 ID(→ project_plan)';
|
||||
COMMENT ON COLUMN contract.contract_file_url IS '签字盖章电子合同';
|
||||
COMMENT ON COLUMN contract.status IS '状态:1有效 0终止';
|
||||
COMMENT ON COLUMN contract.created_at IS '创建时间';
|
||||
CREATE INDEX IF NOT EXISTS idx_contract_customer ON contract (customer_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_contract_xt ON contract (xt_contract_no);
|
||||
|
||||
-- ============================================================
|
||||
-- 7. 勘察派单表 survey_order
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS survey_order (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
survey_no VARCHAR(64) NOT NULL UNIQUE,
|
||||
customer_id BIGINT NOT NULL REFERENCES customer(id),
|
||||
surveyor VARCHAR(64),
|
||||
worker_id BIGINT,
|
||||
survey_time TIMESTAMPTZ,
|
||||
remark TEXT,
|
||||
status SMALLINT NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
COMMENT ON TABLE survey_order IS '客户勘察派单信息表';
|
||||
COMMENT ON COLUMN survey_order.id IS '自增主键';
|
||||
COMMENT ON COLUMN survey_order.survey_no IS '勘察单号(唯一)';
|
||||
COMMENT ON COLUMN survey_order.customer_id IS '客户 ID(→ customer)';
|
||||
COMMENT ON COLUMN survey_order.surveyor IS '勘察人(共享电工)';
|
||||
COMMENT ON COLUMN survey_order.worker_id IS '共享电工 ID';
|
||||
COMMENT ON COLUMN survey_order.survey_time IS '勘察时间';
|
||||
COMMENT ON COLUMN survey_order.remark IS '备注';
|
||||
COMMENT ON COLUMN survey_order.status IS '状态:0已派发 1已接单 2已完成';
|
||||
COMMENT ON COLUMN survey_order.created_at IS '创建时间';
|
||||
CREATE INDEX IF NOT EXISTS idx_survey_customer ON survey_order (customer_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_survey_worker ON survey_order (worker_id);
|
||||
|
||||
-- ============================================================
|
||||
-- 8. 项目验收表 acceptance
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS acceptance (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
accept_no VARCHAR(64) NOT NULL UNIQUE,
|
||||
customer_id BIGINT NOT NULL REFERENCES customer(id),
|
||||
acceptor VARCHAR(64),
|
||||
accept_opinion TEXT,
|
||||
accept_date DATE,
|
||||
installer VARCHAR(64),
|
||||
accept_file_url VARCHAR(512),
|
||||
contract_id BIGINT REFERENCES contract(id),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
COMMENT ON TABLE acceptance IS '项目验收信息表';
|
||||
COMMENT ON COLUMN acceptance.id IS '自增主键';
|
||||
COMMENT ON COLUMN acceptance.accept_no IS '验收单号(唯一)';
|
||||
COMMENT ON COLUMN acceptance.customer_id IS '客户 ID(→ customer)';
|
||||
COMMENT ON COLUMN acceptance.acceptor IS '验收人';
|
||||
COMMENT ON COLUMN acceptance.accept_opinion IS '验收意见';
|
||||
COMMENT ON COLUMN acceptance.accept_date IS '验收日期';
|
||||
COMMENT ON COLUMN acceptance.installer IS '安装人';
|
||||
COMMENT ON COLUMN acceptance.accept_file_url IS '验收文件电子版';
|
||||
COMMENT ON COLUMN acceptance.contract_id IS '合同 ID(→ contract)';
|
||||
COMMENT ON COLUMN acceptance.created_at IS '创建时间';
|
||||
CREATE INDEX IF NOT EXISTS idx_accept_customer ON acceptance (customer_id);
|
||||
|
||||
-- ============================================================
|
||||
-- 9. 客户回访信息表 visit_record
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS visit_record (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
visit_no VARCHAR(64) NOT NULL UNIQUE,
|
||||
customer_id BIGINT NOT NULL REFERENCES customer(id),
|
||||
contact_person VARCHAR(64),
|
||||
contact_phone VARCHAR(20),
|
||||
visitor VARCHAR(64),
|
||||
visit_time DATE,
|
||||
customer_opinion TEXT,
|
||||
photos JSONB,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
COMMENT ON TABLE visit_record IS '客户回访信息表';
|
||||
COMMENT ON COLUMN visit_record.id IS '自增主键';
|
||||
COMMENT ON COLUMN visit_record.visit_no IS '回访单号(唯一)';
|
||||
COMMENT ON COLUMN visit_record.customer_id IS '客户 ID(→ customer)';
|
||||
COMMENT ON COLUMN visit_record.contact_person IS '联系人';
|
||||
COMMENT ON COLUMN visit_record.contact_phone IS '电话';
|
||||
COMMENT ON COLUMN visit_record.visitor IS '回访人';
|
||||
COMMENT ON COLUMN visit_record.visit_time IS '回访时间';
|
||||
COMMENT ON COLUMN visit_record.customer_opinion IS '客户意见';
|
||||
COMMENT ON COLUMN visit_record.photos IS '回访照片(JSONB)';
|
||||
COMMENT ON COLUMN visit_record.created_at IS '创建时间';
|
||||
CREATE INDEX IF NOT EXISTS idx_visit_rec_customer ON visit_record (customer_id);
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- ============================================================
|
||||
-- 验证查询(可选)
|
||||
-- ============================================================
|
||||
SELECT tablename FROM pg_tables
|
||||
WHERE schemaname = 'public'
|
||||
AND tablename IN ('customer_visit','customer_building','customer_room',
|
||||
'project_plan','project_plan_detail','contract',
|
||||
'survey_order','acceptance','visit_record')
|
||||
ORDER BY tablename;
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Controller, Get, Post, Put, Del, Inject, Query, Body, Param } from "@midwayjs/core";
|
||||
import { Context } from "@midwayjs/koa";
|
||||
import { AcceptanceService } from "../service/acceptance.service";
|
||||
|
||||
@Controller("/api/acceptances")
|
||||
export class AcceptanceController {
|
||||
@Inject() ctx!: Context;
|
||||
@Inject() acceptanceService!: AcceptanceService;
|
||||
|
||||
@Get("/") async list(@Query() query: any) { return this.acceptanceService.list(query, this.ctx); }
|
||||
@Get("/:id") async detail(@Param("id") id: string) { return this.acceptanceService.detail(Number(id), this.ctx); }
|
||||
@Post("/") async create(@Body() body: any) { return this.acceptanceService.create(body, this.ctx); }
|
||||
@Put("/:id") async update(@Param("id") id: string, @Body() body: any) { return this.acceptanceService.update(Number(id), body, this.ctx); }
|
||||
@Del("/:id") async delete(@Param("id") id: string) { return this.acceptanceService.delete(Number(id), this.ctx); }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Controller, Get, Post, Put, Del, Inject, Query, Body, Param } from "@midwayjs/core";
|
||||
import { Context } from "@midwayjs/koa";
|
||||
import { ContractService } from "../service/contract.service";
|
||||
|
||||
@Controller("/api/contracts")
|
||||
export class ContractController {
|
||||
@Inject() ctx!: Context;
|
||||
@Inject() contractService!: ContractService;
|
||||
|
||||
@Get("/") async list(@Query() query: any) { return this.contractService.list(query, this.ctx); }
|
||||
@Get("/:id") async detail(@Param("id") id: string) { return this.contractService.detail(Number(id), this.ctx); }
|
||||
@Post("/") async create(@Body() body: any) { return this.contractService.create(body, this.ctx); }
|
||||
@Put("/:id") async update(@Param("id") id: string, @Body() body: any) { return this.contractService.update(Number(id), body, this.ctx); }
|
||||
@Del("/:id") async delete(@Param("id") id: string) { return this.contractService.delete(Number(id), this.ctx); }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Controller, Get, Post, Put, Del, Inject, Query, Body, Param } from "@midwayjs/core";
|
||||
import { Context } from "@midwayjs/koa";
|
||||
import { CustomerBuildingService } from "../service/customer_building.service";
|
||||
|
||||
@Controller("/api/customer-buildings")
|
||||
export class CustomerBuildingController {
|
||||
@Inject() ctx!: Context;
|
||||
@Inject() customerBuildingService!: CustomerBuildingService;
|
||||
|
||||
@Get("/") async list(@Query() query: any) { return this.customerBuildingService.list(query, this.ctx); }
|
||||
@Get("/:id") async detail(@Param("id") id: string) { return this.customerBuildingService.detail(Number(id), this.ctx); }
|
||||
@Post("/") async create(@Body() body: any) { return this.customerBuildingService.create(body, this.ctx); }
|
||||
@Put("/:id") async update(@Param("id") id: string, @Body() body: any) { return this.customerBuildingService.update(Number(id), body, this.ctx); }
|
||||
@Del("/:id") async delete(@Param("id") id: string) { return this.customerBuildingService.delete(Number(id), this.ctx); }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Controller, Get, Post, Put, Del, Inject, Query, Body, Param } from "@midwayjs/core";
|
||||
import { Context } from "@midwayjs/koa";
|
||||
import { CustomerRoomService } from "../service/customer_room.service";
|
||||
|
||||
@Controller("/api/customer-rooms")
|
||||
export class CustomerRoomController {
|
||||
@Inject() ctx!: Context;
|
||||
@Inject() customerRoomService!: CustomerRoomService;
|
||||
|
||||
@Get("/") async list(@Query() query: any) { return this.customerRoomService.list(query, this.ctx); }
|
||||
@Get("/:id") async detail(@Param("id") id: string) { return this.customerRoomService.detail(Number(id), this.ctx); }
|
||||
@Post("/") async create(@Body() body: any) { return this.customerRoomService.create(body, this.ctx); }
|
||||
@Put("/:id") async update(@Param("id") id: string, @Body() body: any) { return this.customerRoomService.update(Number(id), body, this.ctx); }
|
||||
@Del("/:id") async delete(@Param("id") id: string) { return this.customerRoomService.delete(Number(id), this.ctx); }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Controller, Get, Post, Put, Del, Inject, Query, Body, Param } from "@midwayjs/core";
|
||||
import { Context } from "@midwayjs/koa";
|
||||
import { CustomerVisitService } from "../service/customer_visit.service";
|
||||
|
||||
@Controller("/api/customer-visits")
|
||||
export class CustomerVisitController {
|
||||
@Inject() ctx!: Context;
|
||||
@Inject() customerVisitService!: CustomerVisitService;
|
||||
|
||||
@Get("/") async list(@Query() query: any) { return this.customerVisitService.list(query, this.ctx); }
|
||||
@Get("/:id") async detail(@Param("id") id: string) { return this.customerVisitService.detail(Number(id), this.ctx); }
|
||||
@Post("/") async create(@Body() body: any) { return this.customerVisitService.create(body, this.ctx); }
|
||||
@Put("/:id") async update(@Param("id") id: string, @Body() body: any) { return this.customerVisitService.update(Number(id), body, this.ctx); }
|
||||
@Del("/:id") async delete(@Param("id") id: string) { return this.customerVisitService.delete(Number(id), this.ctx); }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Controller, Get, Post, Put, Del, Inject, Query, Body, Param } from "@midwayjs/core";
|
||||
import { Context } from "@midwayjs/koa";
|
||||
import { ProjectPlanService } from "../service/project_plan.service";
|
||||
|
||||
@Controller("/api/project-plans")
|
||||
export class ProjectPlanController {
|
||||
@Inject() ctx!: Context;
|
||||
@Inject() projectPlanService!: ProjectPlanService;
|
||||
|
||||
@Get("/") async list(@Query() query: any) { return this.projectPlanService.list(query, this.ctx); }
|
||||
@Get("/:id") async detail(@Param("id") id: string) { return this.projectPlanService.detail(Number(id), this.ctx); }
|
||||
@Post("/") async create(@Body() body: any) { return this.projectPlanService.create(body, this.ctx); }
|
||||
@Put("/:id") async update(@Param("id") id: string, @Body() body: any) { return this.projectPlanService.update(Number(id), body, this.ctx); }
|
||||
@Del("/:id") async delete(@Param("id") id: string) { return this.projectPlanService.delete(Number(id), this.ctx); }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Controller, Get, Post, Put, Del, Inject, Query, Body, Param } from "@midwayjs/core";
|
||||
import { Context } from "@midwayjs/koa";
|
||||
import { SurveyOrderService } from "../service/survey_order.service";
|
||||
|
||||
@Controller("/api/survey-orders")
|
||||
export class SurveyOrderController {
|
||||
@Inject() ctx!: Context;
|
||||
@Inject() surveyOrderService!: SurveyOrderService;
|
||||
|
||||
@Get("/") async list(@Query() query: any) { return this.surveyOrderService.list(query, this.ctx); }
|
||||
@Get("/:id") async detail(@Param("id") id: string) { return this.surveyOrderService.detail(Number(id), this.ctx); }
|
||||
@Post("/") async create(@Body() body: any) { return this.surveyOrderService.create(body, this.ctx); }
|
||||
@Put("/:id") async update(@Param("id") id: string, @Body() body: any) { return this.surveyOrderService.update(Number(id), body, this.ctx); }
|
||||
@Del("/:id") async delete(@Param("id") id: string) { return this.surveyOrderService.delete(Number(id), this.ctx); }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Controller, Get, Post, Put, Del, Inject, Query, Body, Param } from "@midwayjs/core";
|
||||
import { Context } from "@midwayjs/koa";
|
||||
import { VisitRecordService } from "../service/visit_record.service";
|
||||
|
||||
@Controller("/api/visit-records")
|
||||
export class VisitRecordController {
|
||||
@Inject() ctx!: Context;
|
||||
@Inject() visitRecordService!: VisitRecordService;
|
||||
|
||||
@Get("/") async list(@Query() query: any) { return this.visitRecordService.list(query, this.ctx); }
|
||||
@Get("/:id") async detail(@Param("id") id: string) { return this.visitRecordService.detail(Number(id), this.ctx); }
|
||||
@Post("/") async create(@Body() body: any) { return this.visitRecordService.create(body, this.ctx); }
|
||||
@Put("/:id") async update(@Param("id") id: string, @Body() body: any) { return this.visitRecordService.update(Number(id), body, this.ctx); }
|
||||
@Del("/:id") async delete(@Param("id") id: string) { return this.visitRecordService.delete(Number(id), this.ctx); }
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { IsNotEmpty, IsOptional, IsString, IsInt, Matches, Min, Max, MaxLength } from "class-validator";
|
||||
|
||||
export class AcceptanceQueryDTO {
|
||||
@IsOptional() @IsInt() @Min(1) page?: number = 1;
|
||||
@IsOptional() @IsInt() @Min(1) @Max(200) pageSize?: number = 10;
|
||||
@IsOptional() @IsString() @MaxLength(64) keyword?: string;
|
||||
@IsOptional() @IsInt() customerId?: number;
|
||||
}
|
||||
|
||||
export class AcceptanceCreateDTO {
|
||||
@IsNotEmpty({ message: '所属客户不能为空' }) @IsInt() customerId!: number;
|
||||
@IsOptional() @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) acceptor?: string;
|
||||
@IsOptional() @IsString() @MaxLength(2000) @Matches(/^[^<>]*$/) acceptOpinion?: string;
|
||||
@IsOptional() @IsString() @MaxLength(10) acceptDate?: string;
|
||||
@IsOptional() @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) installer?: string;
|
||||
@IsOptional() @IsString() @MaxLength(512) acceptFileUrl?: string;
|
||||
@IsOptional() @IsInt() contractId?: number;
|
||||
}
|
||||
|
||||
export class AcceptanceUpdateDTO {
|
||||
@IsOptional() @IsInt() customerId?: number;
|
||||
@IsOptional() @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) acceptor?: string;
|
||||
@IsOptional() @IsString() @MaxLength(2000) @Matches(/^[^<>]*$/) acceptOpinion?: string;
|
||||
@IsOptional() @IsString() @MaxLength(10) acceptDate?: string;
|
||||
@IsOptional() @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) installer?: string;
|
||||
@IsOptional() @IsString() @MaxLength(512) acceptFileUrl?: string;
|
||||
@IsOptional() @IsInt() contractId?: number;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { IsNotEmpty, IsOptional, IsString, IsInt, IsNumber, Matches, Min, Max, MaxLength } from "class-validator";
|
||||
|
||||
export class ContractQueryDTO {
|
||||
@IsOptional() @IsInt() @Min(1) page?: number = 1;
|
||||
@IsOptional() @IsInt() @Min(1) @Max(200) pageSize?: number = 10;
|
||||
@IsOptional() @IsString() @MaxLength(64) keyword?: string;
|
||||
@IsOptional() @IsInt() customerId?: number;
|
||||
@IsOptional() @IsInt() salesmanId?: number;
|
||||
@IsOptional() @IsInt() status?: number;
|
||||
}
|
||||
|
||||
export class ContractCreateDTO {
|
||||
@IsNotEmpty({ message: '所属客户不能为空' }) @IsInt() customerId!: number;
|
||||
@IsOptional() @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) xtContractNo?: string;
|
||||
@IsOptional() @IsNumber() @Min(0) contractAmount?: number;
|
||||
@IsOptional() @IsString() @MaxLength(32) @Matches(/^[^<>]*$/) payCycle?: string;
|
||||
@IsOptional() @IsInt() salesmanId?: number;
|
||||
@IsOptional() @IsString() @MaxLength(10) signDate?: string;
|
||||
@IsOptional() @IsString() @MaxLength(2000) @Matches(/^[^<>]*$/) contractDesc?: string;
|
||||
@IsOptional() @IsString() @MaxLength(10) startDate?: string;
|
||||
@IsOptional() @IsString() @MaxLength(10) endDate?: string;
|
||||
@IsOptional() @IsInt() planId?: number;
|
||||
@IsOptional() @IsString() @MaxLength(512) contractFileUrl?: string;
|
||||
@IsOptional() @IsInt() @Min(0) @Max(1) status?: number;
|
||||
}
|
||||
|
||||
export class ContractUpdateDTO {
|
||||
@IsOptional() @IsInt() customerId?: number;
|
||||
@IsOptional() @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) xtContractNo?: string;
|
||||
@IsOptional() @IsNumber() @Min(0) contractAmount?: number;
|
||||
@IsOptional() @IsString() @MaxLength(32) @Matches(/^[^<>]*$/) payCycle?: string;
|
||||
@IsOptional() @IsInt() salesmanId?: number;
|
||||
@IsOptional() @IsString() @MaxLength(10) signDate?: string;
|
||||
@IsOptional() @IsString() @MaxLength(2000) @Matches(/^[^<>]*$/) contractDesc?: string;
|
||||
@IsOptional() @IsString() @MaxLength(10) startDate?: string;
|
||||
@IsOptional() @IsString() @MaxLength(10) endDate?: string;
|
||||
@IsOptional() @IsInt() planId?: number;
|
||||
@IsOptional() @IsString() @MaxLength(512) contractFileUrl?: string;
|
||||
@IsOptional() @IsInt() @Min(0) @Max(1) status?: number;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { IsNotEmpty, IsOptional, IsString, IsInt, Matches, Min, Max, MaxLength } from "class-validator";
|
||||
|
||||
export class CustomerBuildingQueryDTO {
|
||||
@IsOptional() @IsInt() @Min(1) page?: number = 1;
|
||||
@IsOptional() @IsInt() @Min(1) @Max(200) pageSize?: number = 10;
|
||||
@IsOptional() @IsString() @MaxLength(64) keyword?: string;
|
||||
@IsOptional() @IsInt() customerId?: number;
|
||||
}
|
||||
|
||||
export class CustomerBuildingCreateDTO {
|
||||
@IsNotEmpty({ message: '所属客户不能为空' }) @IsInt() customerId!: number;
|
||||
@IsNotEmpty({ message: '建筑名称不能为空' }) @IsString() @MaxLength(128) @Matches(/^[^<>]*$/) buildingName!: string;
|
||||
@IsOptional() @IsString() @MaxLength(2000) @Matches(/^[^<>]*$/) basicInfo?: string;
|
||||
@IsOptional() @IsString() @MaxLength(128) @Matches(/^[^<>]*$/) purpose?: string;
|
||||
@IsOptional() @IsInt() @Min(0) floorCount?: number;
|
||||
}
|
||||
|
||||
export class CustomerBuildingUpdateDTO {
|
||||
@IsOptional() @IsInt() customerId?: number;
|
||||
@IsOptional() @IsString() @MaxLength(128) @Matches(/^[^<>]*$/) buildingName?: string;
|
||||
@IsOptional() @IsString() @MaxLength(2000) @Matches(/^[^<>]*$/) basicInfo?: string;
|
||||
@IsOptional() @IsString() @MaxLength(128) @Matches(/^[^<>]*$/) purpose?: string;
|
||||
@IsOptional() @IsInt() @Min(0) floorCount?: number;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { IsNotEmpty, IsOptional, IsString, IsInt, Matches, Min, Max, MaxLength } from "class-validator";
|
||||
|
||||
export class CustomerRoomQueryDTO {
|
||||
@IsOptional() @IsInt() @Min(1) page?: number = 1;
|
||||
@IsOptional() @IsInt() @Min(1) @Max(200) pageSize?: number = 10;
|
||||
@IsOptional() @IsString() @MaxLength(64) keyword?: string;
|
||||
@IsOptional() @IsInt() customerId?: number;
|
||||
@IsOptional() @IsInt() buildingId?: number;
|
||||
}
|
||||
|
||||
export class CustomerRoomCreateDTO {
|
||||
@IsNotEmpty({ message: '房间编码不能为空' }) @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) roomCode!: string;
|
||||
@IsNotEmpty({ message: '所属客户不能为空' }) @IsInt() customerId!: number;
|
||||
@IsNotEmpty({ message: '房间名称不能为空' }) @IsString() @MaxLength(128) @Matches(/^[^<>]*$/) roomName!: string;
|
||||
@IsNotEmpty({ message: '所属建筑不能为空' }) @IsInt() buildingId!: number;
|
||||
@IsOptional() @IsString() @MaxLength(16) @Matches(/^[^<>]*$/) floorNo?: string;
|
||||
@IsOptional() @IsString() @MaxLength(128) @Matches(/^[^<>]*$/) roomPurpose?: string;
|
||||
@IsOptional() @IsInt() deptId?: number;
|
||||
}
|
||||
|
||||
export class CustomerRoomUpdateDTO {
|
||||
@IsOptional() @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) roomCode?: string;
|
||||
@IsOptional() @IsInt() customerId?: number;
|
||||
@IsOptional() @IsString() @MaxLength(128) @Matches(/^[^<>]*$/) roomName?: string;
|
||||
@IsOptional() @IsInt() buildingId?: number;
|
||||
@IsOptional() @IsString() @MaxLength(16) @Matches(/^[^<>]*$/) floorNo?: string;
|
||||
@IsOptional() @IsString() @MaxLength(128) @Matches(/^[^<>]*$/) roomPurpose?: string;
|
||||
@IsOptional() @IsInt() deptId?: number;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { IsNotEmpty, IsOptional, IsString, IsInt, IsNumber, Matches, Min, Max, MaxLength } from "class-validator";
|
||||
|
||||
export class CustomerVisitQueryDTO {
|
||||
@IsOptional() @IsInt() @Min(1) page?: number = 1;
|
||||
@IsOptional() @IsInt() @Min(1) @Max(200) pageSize?: number = 10;
|
||||
@IsOptional() @IsString() @MaxLength(64) keyword?: string;
|
||||
@IsOptional() @IsInt() customerId?: number;
|
||||
@IsOptional() @IsInt() salesmanId?: number;
|
||||
}
|
||||
|
||||
export class CustomerVisitCreateDTO {
|
||||
@IsNotEmpty({ message: '所属客户不能为空' }) @IsInt() customerId!: number;
|
||||
@IsNotEmpty({ message: '拜访人不能为空' }) @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) visitPerson!: string;
|
||||
@IsOptional() @Matches(/^1[3-9]\d{9}$/, { message: '手机号格式不正确' }) visitPhone?: string;
|
||||
@IsOptional() @IsString() @MaxLength(2000) @Matches(/^[^<>]*$/) visitContent?: string;
|
||||
@IsOptional() @IsString() @MaxLength(10) visitTime?: string;
|
||||
@IsOptional() @IsInt() salesmanId?: number;
|
||||
@IsOptional() @IsInt() @Min(0) @Max(1) willPlan?: number;
|
||||
}
|
||||
|
||||
export class CustomerVisitUpdateDTO {
|
||||
@IsOptional() @IsInt() customerId?: number;
|
||||
@IsOptional() @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) visitPerson?: string;
|
||||
@IsOptional() @Matches(/^1[3-9]\d{9}$/) visitPhone?: string;
|
||||
@IsOptional() @IsString() @MaxLength(2000) @Matches(/^[^<>]*$/) visitContent?: string;
|
||||
@IsOptional() @IsString() @MaxLength(10) visitTime?: string;
|
||||
@IsOptional() @IsInt() salesmanId?: number;
|
||||
@IsOptional() @IsInt() @Min(0) @Max(1) willPlan?: number;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { IsNotEmpty, IsOptional, IsString, IsInt, IsNumber, IsArray, ValidateNested, Matches, Min, Max, MaxLength } from "class-validator";
|
||||
import { Type } from "class-transformer";
|
||||
|
||||
export class PlanDetailDTO {
|
||||
@IsOptional() @IsInt() id?: number;
|
||||
@IsOptional() @IsString() @MaxLength(128) @Matches(/^[^<>]*$/) equipName?: string;
|
||||
@IsOptional() @IsInt() equipId?: number;
|
||||
@IsOptional() @IsInt() smartDeviceId?: number;
|
||||
@IsOptional() @IsNumber() @Min(0.01) quantity?: number;
|
||||
@IsOptional() @IsString() @MaxLength(2000) @Matches(/^[^<>]*$/) remark?: string;
|
||||
}
|
||||
|
||||
export class ProjectPlanQueryDTO {
|
||||
@IsOptional() @IsInt() @Min(1) page?: number = 1;
|
||||
@IsOptional() @IsInt() @Min(1) @Max(200) pageSize?: number = 10;
|
||||
@IsOptional() @IsString() @MaxLength(64) keyword?: string;
|
||||
@IsOptional() @IsInt() customerId?: number;
|
||||
@IsOptional() @IsInt() status?: number;
|
||||
}
|
||||
|
||||
export class ProjectPlanCreateDTO {
|
||||
@IsNotEmpty({ message: '所属客户不能为空' }) @IsInt() customerId!: number;
|
||||
@IsOptional() @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) registrar?: string;
|
||||
@IsOptional() @IsString() @MaxLength(10) registerTime?: string;
|
||||
@IsOptional() @IsString() @MaxLength(2000) @Matches(/^[^<>]*$/) planDesc?: string;
|
||||
@IsOptional() @IsString() @MaxLength(512) planFileUrl?: string;
|
||||
@IsOptional() @IsInt() @Min(0) @Max(1) status?: number;
|
||||
@IsOptional() @IsArray() @ValidateNested({ each: true }) @Type(() => PlanDetailDTO) details?: PlanDetailDTO[];
|
||||
}
|
||||
|
||||
export class ProjectPlanUpdateDTO {
|
||||
@IsOptional() @IsInt() customerId?: number;
|
||||
@IsOptional() @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) registrar?: string;
|
||||
@IsOptional() @IsString() @MaxLength(10) registerTime?: string;
|
||||
@IsOptional() @IsString() @MaxLength(2000) @Matches(/^[^<>]*$/) planDesc?: string;
|
||||
@IsOptional() @IsString() @MaxLength(512) planFileUrl?: string;
|
||||
@IsOptional() @IsInt() @Min(0) @Max(1) status?: number;
|
||||
@IsOptional() @IsArray() @ValidateNested({ each: true }) @Type(() => PlanDetailDTO) details?: PlanDetailDTO[];
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { IsNotEmpty, IsOptional, IsString, IsInt, Matches, Min, Max, MaxLength } from "class-validator";
|
||||
|
||||
export class SurveyOrderQueryDTO {
|
||||
@IsOptional() @IsInt() @Min(1) page?: number = 1;
|
||||
@IsOptional() @IsInt() @Min(1) @Max(200) pageSize?: number = 10;
|
||||
@IsOptional() @IsString() @MaxLength(64) keyword?: string;
|
||||
@IsOptional() @IsInt() customerId?: number;
|
||||
@IsOptional() @IsInt() status?: number;
|
||||
}
|
||||
|
||||
export class SurveyOrderCreateDTO {
|
||||
@IsNotEmpty({ message: '所属客户不能为空' }) @IsInt() customerId!: number;
|
||||
@IsOptional() @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) surveyor?: string;
|
||||
@IsOptional() @IsInt() workerId?: number;
|
||||
@IsOptional() @IsString() surveyTime?: string;
|
||||
@IsOptional() @IsString() @MaxLength(2000) @Matches(/^[^<>]*$/) remark?: string;
|
||||
@IsOptional() @IsInt() @Min(0) @Max(2) status?: number;
|
||||
}
|
||||
|
||||
export class SurveyOrderUpdateDTO {
|
||||
@IsOptional() @IsInt() customerId?: number;
|
||||
@IsOptional() @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) surveyor?: string;
|
||||
@IsOptional() @IsInt() workerId?: number;
|
||||
@IsOptional() @IsString() surveyTime?: string;
|
||||
@IsOptional() @IsString() @MaxLength(2000) @Matches(/^[^<>]*$/) remark?: string;
|
||||
@IsOptional() @IsInt() @Min(0) @Max(2) status?: number;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { IsNotEmpty, IsOptional, IsString, IsInt, IsArray, IsObject, Matches, Min, Max, MaxLength } from "class-validator";
|
||||
|
||||
export class VisitRecordQueryDTO {
|
||||
@IsOptional() @IsInt() @Min(1) page?: number = 1;
|
||||
@IsOptional() @IsInt() @Min(1) @Max(200) pageSize?: number = 10;
|
||||
@IsOptional() @IsString() @MaxLength(64) keyword?: string;
|
||||
@IsOptional() @IsInt() customerId?: number;
|
||||
}
|
||||
|
||||
export class VisitRecordCreateDTO {
|
||||
@IsNotEmpty({ message: '所属客户不能为空' }) @IsInt() customerId!: number;
|
||||
@IsOptional() @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) contactPerson?: string;
|
||||
@IsOptional() @Matches(/^1[3-9]\d{9}$/, { message: '手机号格式不正确' }) contactPhone?: string;
|
||||
@IsOptional() @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) visitor?: string;
|
||||
@IsOptional() @IsString() @MaxLength(10) visitTime?: string;
|
||||
@IsOptional() @IsString() @MaxLength(2000) @Matches(/^[^<>]*$/) customerOpinion?: string;
|
||||
@IsOptional() @IsArray() photos?: any[];
|
||||
}
|
||||
|
||||
export class VisitRecordUpdateDTO {
|
||||
@IsOptional() @IsInt() customerId?: number;
|
||||
@IsOptional() @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) contactPerson?: string;
|
||||
@IsOptional() @Matches(/^1[3-9]\d{9}$/) contactPhone?: string;
|
||||
@IsOptional() @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) visitor?: string;
|
||||
@IsOptional() @IsString() @MaxLength(10) visitTime?: string;
|
||||
@IsOptional() @IsString() @MaxLength(2000) @Matches(/^[^<>]*$/) customerOpinion?: string;
|
||||
@IsOptional() @IsArray() photos?: any[];
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Column, Model, Table, DataType, CreatedAt, ForeignKey, BelongsTo } from "sequelize-typescript";
|
||||
import Customer from "./customer.entity";
|
||||
import Contract from "./contract.entity";
|
||||
|
||||
@Table({
|
||||
tableName: "acceptance",
|
||||
comment: "项目验收信息表",
|
||||
timestamps: true,
|
||||
createdAt: "created_at",
|
||||
updatedAt: false,
|
||||
indexes: [{ name: "idx_accept_customer", fields: ["customer_id"] }]
|
||||
})
|
||||
export default class Acceptance extends Model {
|
||||
@Column({ type: DataType.BIGINT, autoIncrement: true, primaryKey: true })
|
||||
id!: number;
|
||||
|
||||
@Column({ type: DataType.STRING(64), allowNull: false, unique: true, field: "accept_no", comment: "验收单号(唯一)" })
|
||||
acceptNo!: string;
|
||||
|
||||
@ForeignKey(() => Customer)
|
||||
@Column({ type: DataType.BIGINT, allowNull: false, field: "customer_id", comment: "客户ID" })
|
||||
customerId!: number;
|
||||
|
||||
@Column({ type: DataType.STRING(64), comment: "验收人" })
|
||||
acceptor!: string;
|
||||
|
||||
@Column({ type: DataType.TEXT, field: "accept_opinion", comment: "验收意见" })
|
||||
acceptOpinion!: string;
|
||||
|
||||
@Column({ type: DataType.DATEONLY, field: "accept_date", comment: "验收日期" })
|
||||
acceptDate!: string;
|
||||
|
||||
@Column({ type: DataType.STRING(64), comment: "安装人" })
|
||||
installer!: string;
|
||||
|
||||
@Column({ type: DataType.STRING(512), field: "accept_file_url", comment: "验收文件" })
|
||||
acceptFileUrl!: string;
|
||||
|
||||
@ForeignKey(() => Contract)
|
||||
@Column({ type: DataType.BIGINT, field: "contract_id", comment: "合同ID" })
|
||||
contractId!: number;
|
||||
|
||||
@BelongsTo(() => Customer)
|
||||
customer!: Customer;
|
||||
|
||||
@BelongsTo(() => Contract)
|
||||
contract!: Contract;
|
||||
|
||||
@CreatedAt
|
||||
created_at!: Date;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { Column, Model, Table, DataType, CreatedAt, ForeignKey, BelongsTo } from "sequelize-typescript";
|
||||
import Customer from "./customer.entity";
|
||||
import ProjectPlan from "./project_plan.entity";
|
||||
|
||||
@Table({
|
||||
tableName: "contract",
|
||||
comment: "智电项目合同信息表",
|
||||
timestamps: true,
|
||||
createdAt: "created_at",
|
||||
updatedAt: false,
|
||||
indexes: [
|
||||
{ name: "idx_contract_customer", fields: ["customer_id"] },
|
||||
{ name: "idx_contract_xt", fields: ["xt_contract_no"] }
|
||||
]
|
||||
})
|
||||
export default class Contract extends Model {
|
||||
@Column({ type: DataType.BIGINT, autoIncrement: true, primaryKey: true })
|
||||
id!: number;
|
||||
|
||||
@Column({ type: DataType.STRING(64), allowNull: false, unique: true, field: "contract_no", comment: "合同单号(唯一)" })
|
||||
contractNo!: string;
|
||||
|
||||
@Column({ type: DataType.STRING(64), field: "xt_contract_no", comment: "鑫潼合同号(XT+年份+序号)" })
|
||||
xtContractNo!: string;
|
||||
|
||||
@ForeignKey(() => Customer)
|
||||
@Column({ type: DataType.BIGINT, allowNull: false, field: "customer_id", comment: "客户ID" })
|
||||
customerId!: number;
|
||||
|
||||
@Column({ type: DataType.DECIMAL(14, 2), field: "contract_amount", comment: "合同金额" })
|
||||
contractAmount!: number;
|
||||
|
||||
@Column({ type: DataType.STRING(32), field: "pay_cycle", comment: "支付周期:按月/按年" })
|
||||
payCycle!: string;
|
||||
|
||||
@Column({ type: DataType.BIGINT, field: "salesman_id", comment: "业务员" })
|
||||
salesmanId!: number;
|
||||
|
||||
@Column({ type: DataType.DATEONLY, field: "sign_date", comment: "签订日期" })
|
||||
signDate!: string;
|
||||
|
||||
@Column({ type: DataType.TEXT, field: "contract_desc", comment: "合同说明" })
|
||||
contractDesc!: string;
|
||||
|
||||
@Column({ type: DataType.DATEONLY, field: "start_date", comment: "开始日期" })
|
||||
startDate!: string;
|
||||
|
||||
@Column({ type: DataType.DATEONLY, field: "end_date", comment: "结束日期" })
|
||||
endDate!: string;
|
||||
|
||||
@ForeignKey(() => ProjectPlan)
|
||||
@Column({ type: DataType.BIGINT, field: "plan_id", comment: "方案ID" })
|
||||
planId!: number;
|
||||
|
||||
@Column({ type: DataType.STRING(512), field: "contract_file_url", comment: "签字盖章合同文件" })
|
||||
contractFileUrl!: string;
|
||||
|
||||
@Column({ type: DataType.SMALLINT, allowNull: false, defaultValue: 1, comment: "状态:1有效 0终止" })
|
||||
status!: number;
|
||||
|
||||
@BelongsTo(() => Customer)
|
||||
customer!: Customer;
|
||||
|
||||
@BelongsTo(() => ProjectPlan)
|
||||
plan!: ProjectPlan;
|
||||
|
||||
@CreatedAt
|
||||
created_at!: Date;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Column, Model, Table, DataType, CreatedAt, ForeignKey, BelongsTo } from "sequelize-typescript";
|
||||
import Customer from "./customer.entity";
|
||||
|
||||
@Table({
|
||||
tableName: "customer_building",
|
||||
comment: "客户建筑信息表",
|
||||
timestamps: true,
|
||||
createdAt: "created_at",
|
||||
updatedAt: false,
|
||||
indexes: [{ name: "idx_building_customer", fields: ["customer_id"] }]
|
||||
})
|
||||
export default class CustomerBuilding extends Model {
|
||||
@Column({ type: DataType.BIGINT, autoIncrement: true, primaryKey: true })
|
||||
id!: number;
|
||||
|
||||
@ForeignKey(() => Customer)
|
||||
@Column({ type: DataType.BIGINT, allowNull: false, field: "customer_id", comment: "客户ID" })
|
||||
customerId!: number;
|
||||
|
||||
@Column({ type: DataType.STRING(128), allowNull: false, field: "building_name", comment: "建筑名称" })
|
||||
buildingName!: string;
|
||||
|
||||
@Column({ type: DataType.TEXT, field: "basic_info", comment: "基本情况" })
|
||||
basicInfo!: string;
|
||||
|
||||
@Column({ type: DataType.STRING(128), comment: "用途" })
|
||||
purpose!: string;
|
||||
|
||||
@Column({ type: DataType.INTEGER, field: "floor_count", comment: "层数" })
|
||||
floorCount!: number;
|
||||
|
||||
@BelongsTo(() => Customer)
|
||||
customer!: Customer;
|
||||
|
||||
@CreatedAt
|
||||
created_at!: Date;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Column, Model, Table, DataType, CreatedAt, ForeignKey, BelongsTo } from "sequelize-typescript";
|
||||
import Customer from "./customer.entity";
|
||||
import CustomerBuilding from "./customer_building.entity";
|
||||
import CustomerDepartment from "./customer_department.entity";
|
||||
|
||||
@Table({
|
||||
tableName: "customer_room",
|
||||
comment: "客户房间信息表",
|
||||
timestamps: true,
|
||||
createdAt: "created_at",
|
||||
updatedAt: false,
|
||||
indexes: [
|
||||
{ name: "idx_room_customer", fields: ["customer_id"] },
|
||||
{ name: "idx_room_building", fields: ["building_id"] }
|
||||
]
|
||||
})
|
||||
export default class CustomerRoom extends Model {
|
||||
@Column({ type: DataType.BIGINT, autoIncrement: true, primaryKey: true })
|
||||
id!: number;
|
||||
|
||||
@Column({ type: DataType.STRING(64), allowNull: false, field: "room_code", comment: "房间编码" })
|
||||
roomCode!: string;
|
||||
|
||||
@ForeignKey(() => Customer)
|
||||
@Column({ type: DataType.BIGINT, allowNull: false, field: "customer_id", comment: "客户ID" })
|
||||
customerId!: number;
|
||||
|
||||
@Column({ type: DataType.STRING(128), allowNull: false, field: "room_name", comment: "房间名称" })
|
||||
roomName!: string;
|
||||
|
||||
@ForeignKey(() => CustomerBuilding)
|
||||
@Column({ type: DataType.BIGINT, allowNull: false, field: "building_id", comment: "所属建筑ID" })
|
||||
buildingId!: number;
|
||||
|
||||
@Column({ type: DataType.STRING(16), field: "floor_no", comment: "所在楼层" })
|
||||
floorNo!: string;
|
||||
|
||||
@Column({ type: DataType.STRING(128), field: "room_purpose", comment: "房间用途" })
|
||||
roomPurpose!: string;
|
||||
|
||||
@ForeignKey(() => CustomerDepartment)
|
||||
@Column({ type: DataType.BIGINT, field: "dept_id", comment: "房间所属部门ID" })
|
||||
deptId!: number;
|
||||
|
||||
@BelongsTo(() => Customer)
|
||||
customer!: Customer;
|
||||
|
||||
@BelongsTo(() => CustomerBuilding)
|
||||
building!: CustomerBuilding;
|
||||
|
||||
@CreatedAt
|
||||
created_at!: Date;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Column, Model, Table, DataType, CreatedAt, ForeignKey, BelongsTo } from "sequelize-typescript";
|
||||
import Customer from "./customer.entity";
|
||||
|
||||
@Table({
|
||||
tableName: "customer_visit",
|
||||
comment: "客户拜访信息表",
|
||||
timestamps: true,
|
||||
createdAt: "created_at",
|
||||
updatedAt: false,
|
||||
indexes: [
|
||||
{ name: "idx_visit_customer", fields: ["customer_id"] },
|
||||
{ name: "idx_visit_salesman", fields: ["salesman_id"] }
|
||||
]
|
||||
})
|
||||
export default class CustomerVisit extends Model {
|
||||
@Column({ type: DataType.BIGINT, autoIncrement: true, primaryKey: true })
|
||||
id!: number;
|
||||
|
||||
@Column({ type: DataType.STRING(64), allowNull: false, unique: true, field: "visit_no", comment: "拜访单号(唯一)" })
|
||||
visitNo!: string;
|
||||
|
||||
@ForeignKey(() => Customer)
|
||||
@Column({ type: DataType.BIGINT, allowNull: false, field: "customer_id", comment: "客户ID" })
|
||||
customerId!: number;
|
||||
|
||||
@Column({ type: DataType.STRING(64), allowNull: false, field: "visit_person", comment: "拜访人" })
|
||||
visitPerson!: string;
|
||||
|
||||
@Column({ type: DataType.STRING(20), field: "visit_phone", comment: "拜访人电话" })
|
||||
visitPhone!: string;
|
||||
|
||||
@Column({ type: DataType.TEXT, field: "visit_content", comment: "拜访内容" })
|
||||
visitContent!: string;
|
||||
|
||||
@Column({ type: DataType.DATEONLY, field: "visit_time", comment: "拜访时间" })
|
||||
visitTime!: string;
|
||||
|
||||
@Column({ type: DataType.BIGINT, field: "salesman_id", comment: "业务员(关联sys_user.id)" })
|
||||
salesmanId!: number;
|
||||
|
||||
@Column({ type: DataType.SMALLINT, defaultValue: 0, field: "will_plan", comment: "是否做方案:0不同意 1同意" })
|
||||
willPlan!: number;
|
||||
|
||||
@BelongsTo(() => Customer)
|
||||
customer!: Customer;
|
||||
|
||||
@CreatedAt
|
||||
created_at!: Date;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Column, Model, Table, DataType, CreatedAt, ForeignKey, BelongsTo, HasMany } from "sequelize-typescript";
|
||||
import Customer from "./customer.entity";
|
||||
import ProjectPlanDetail from "./project_plan_detail.entity";
|
||||
|
||||
@Table({
|
||||
tableName: "project_plan",
|
||||
comment: "智电改造方案信息表",
|
||||
timestamps: true,
|
||||
createdAt: "created_at",
|
||||
updatedAt: false,
|
||||
indexes: [{ name: "idx_plan_customer", fields: ["customer_id"] }]
|
||||
})
|
||||
export default class ProjectPlan extends Model {
|
||||
@Column({ type: DataType.BIGINT, autoIncrement: true, primaryKey: true })
|
||||
id!: number;
|
||||
|
||||
@Column({ type: DataType.STRING(64), allowNull: false, unique: true, field: "plan_no", comment: "方案单号(唯一)" })
|
||||
planNo!: string;
|
||||
|
||||
@ForeignKey(() => Customer)
|
||||
@Column({ type: DataType.BIGINT, allowNull: false, field: "customer_id", comment: "客户ID" })
|
||||
customerId!: number;
|
||||
|
||||
@Column({ type: DataType.STRING(64), comment: "方案登记人(业务员)" })
|
||||
registrar!: string;
|
||||
|
||||
@Column({ type: DataType.DATEONLY, field: "register_time", comment: "方案登记时间" })
|
||||
registerTime!: string;
|
||||
|
||||
@Column({ type: DataType.TEXT, field: "plan_desc", comment: "方案说明" })
|
||||
planDesc!: string;
|
||||
|
||||
@Column({ type: DataType.STRING(512), field: "plan_file_url", comment: "方案最终文件" })
|
||||
planFileUrl!: string;
|
||||
|
||||
@Column({ type: DataType.SMALLINT, allowNull: false, defaultValue: 0, comment: "状态:0草稿 1已确认" })
|
||||
status!: number;
|
||||
|
||||
@BelongsTo(() => Customer)
|
||||
customer!: Customer;
|
||||
|
||||
@HasMany(() => ProjectPlanDetail, { foreignKey: "planId", onDelete: "CASCADE" })
|
||||
details!: ProjectPlanDetail[];
|
||||
|
||||
@CreatedAt
|
||||
created_at!: Date;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Column, Model, Table, DataType, CreatedAt, ForeignKey, BelongsTo } from "sequelize-typescript";
|
||||
import ProjectPlan from "./project_plan.entity";
|
||||
import Customer from "./customer.entity";
|
||||
|
||||
@Table({
|
||||
tableName: "project_plan_detail",
|
||||
comment: "项目方案设备明细表",
|
||||
timestamps: true,
|
||||
createdAt: "created_at",
|
||||
updatedAt: false,
|
||||
indexes: [
|
||||
{ name: "idx_pd_plan", fields: ["plan_id"] },
|
||||
{ name: "idx_pd_customer", fields: ["customer_id"] }
|
||||
]
|
||||
})
|
||||
export default class ProjectPlanDetail extends Model {
|
||||
@Column({ type: DataType.BIGINT, autoIncrement: true, primaryKey: true })
|
||||
id!: number;
|
||||
|
||||
@ForeignKey(() => ProjectPlan)
|
||||
@Column({ type: DataType.BIGINT, allowNull: false, field: "plan_id", comment: "方案ID" })
|
||||
planId!: number;
|
||||
|
||||
@ForeignKey(() => Customer)
|
||||
@Column({ type: DataType.BIGINT, allowNull: false, field: "customer_id", comment: "客户ID" })
|
||||
customerId!: number;
|
||||
|
||||
@Column({ type: DataType.STRING(128), field: "equip_name", comment: "设备名称(设备域未实现前为自由文本)" })
|
||||
equipName!: string;
|
||||
|
||||
@Column({ type: DataType.BIGINT, field: "equip_id", comment: "客户设备ID(预留)" })
|
||||
equipId!: number;
|
||||
|
||||
@Column({ type: DataType.BIGINT, field: "smart_device_id", comment: "智电设备型号ID(预留)" })
|
||||
smartDeviceId!: number;
|
||||
|
||||
@Column({ type: DataType.DECIMAL(10, 2), allowNull: false, defaultValue: 1, comment: "数量" })
|
||||
quantity!: number;
|
||||
|
||||
@Column({ type: DataType.TEXT, comment: "备注" })
|
||||
remark!: string;
|
||||
|
||||
@BelongsTo(() => ProjectPlan)
|
||||
plan!: ProjectPlan;
|
||||
|
||||
@CreatedAt
|
||||
created_at!: Date;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Column, Model, Table, DataType, CreatedAt, ForeignKey, BelongsTo } from "sequelize-typescript";
|
||||
import Customer from "./customer.entity";
|
||||
|
||||
@Table({
|
||||
tableName: "survey_order",
|
||||
comment: "客户勘察派单信息表",
|
||||
timestamps: true,
|
||||
createdAt: "created_at",
|
||||
updatedAt: false,
|
||||
indexes: [
|
||||
{ name: "idx_survey_customer", fields: ["customer_id"] },
|
||||
{ name: "idx_survey_worker", fields: ["worker_id"] }
|
||||
]
|
||||
})
|
||||
export default class SurveyOrder extends Model {
|
||||
@Column({ type: DataType.BIGINT, autoIncrement: true, primaryKey: true })
|
||||
id!: number;
|
||||
|
||||
@Column({ type: DataType.STRING(64), allowNull: false, unique: true, field: "survey_no", comment: "勘察单号(唯一)" })
|
||||
surveyNo!: string;
|
||||
|
||||
@ForeignKey(() => Customer)
|
||||
@Column({ type: DataType.BIGINT, allowNull: false, field: "customer_id", comment: "客户ID" })
|
||||
customerId!: number;
|
||||
|
||||
@Column({ type: DataType.STRING(64), comment: "勘察人(共享电工)" })
|
||||
surveyor!: string;
|
||||
|
||||
@Column({ type: DataType.BIGINT, field: "worker_id", comment: "共享电工ID" })
|
||||
workerId!: number;
|
||||
|
||||
@Column({ type: DataType.DATE, field: "survey_time", comment: "勘察时间" })
|
||||
surveyTime!: Date;
|
||||
|
||||
@Column({ type: DataType.TEXT, comment: "备注" })
|
||||
remark!: string;
|
||||
|
||||
@Column({ type: DataType.SMALLINT, allowNull: false, defaultValue: 0, comment: "状态:0已派发 1已接单 2已完成" })
|
||||
status!: number;
|
||||
|
||||
@BelongsTo(() => Customer)
|
||||
customer!: Customer;
|
||||
|
||||
@CreatedAt
|
||||
created_at!: Date;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Column, Model, Table, DataType, CreatedAt, ForeignKey, BelongsTo } from "sequelize-typescript";
|
||||
import Customer from "./customer.entity";
|
||||
|
||||
@Table({
|
||||
tableName: "visit_record",
|
||||
comment: "客户回访信息表",
|
||||
timestamps: true,
|
||||
createdAt: "created_at",
|
||||
updatedAt: false,
|
||||
indexes: [{ name: "idx_visit_rec_customer", fields: ["customer_id"] }]
|
||||
})
|
||||
export default class VisitRecord extends Model {
|
||||
@Column({ type: DataType.BIGINT, autoIncrement: true, primaryKey: true })
|
||||
id!: number;
|
||||
|
||||
@Column({ type: DataType.STRING(64), allowNull: false, unique: true, field: "visit_no", comment: "回访单号(唯一)" })
|
||||
visitNo!: string;
|
||||
|
||||
@ForeignKey(() => Customer)
|
||||
@Column({ type: DataType.BIGINT, allowNull: false, field: "customer_id", comment: "客户ID" })
|
||||
customerId!: number;
|
||||
|
||||
@Column({ type: DataType.STRING(64), field: "contact_person", comment: "联系人" })
|
||||
contactPerson!: string;
|
||||
|
||||
@Column({ type: DataType.STRING(20), field: "contact_phone", comment: "联系电话" })
|
||||
contactPhone!: string;
|
||||
|
||||
@Column({ type: DataType.STRING(64), field: "visitor", comment: "回访人" })
|
||||
visitor!: string;
|
||||
|
||||
@Column({ type: DataType.DATEONLY, field: "visit_time", comment: "回访时间" })
|
||||
visitTime!: string;
|
||||
|
||||
@Column({ type: DataType.TEXT, field: "customer_opinion", comment: "客户意见" })
|
||||
customerOpinion!: string;
|
||||
|
||||
@Column({ type: DataType.JSONB, field: "photos", comment: "回访照片(JSONB)" })
|
||||
photos!: any;
|
||||
|
||||
@BelongsTo(() => Customer)
|
||||
customer!: Customer;
|
||||
|
||||
@CreatedAt
|
||||
created_at!: Date;
|
||||
}
|
||||
@@ -16,6 +16,14 @@ const PERMISSION_MAP: Record<string, string> = {
|
||||
'/api/customers': 'customer:list',
|
||||
'/api/customer-departments': 'customer:department',
|
||||
'/api/customer-accounts': 'customer:account',
|
||||
'/api/customer-visits': 'customer:visit',
|
||||
'/api/customer-buildings': 'customer:building',
|
||||
'/api/customer-rooms': 'customer:room',
|
||||
'/api/survey-orders': 'project:survey',
|
||||
'/api/project-plans': 'project:plan',
|
||||
'/api/contracts': 'project:contract',
|
||||
'/api/acceptances': 'project:acceptance',
|
||||
'/api/visit-records': 'project:visit',
|
||||
};
|
||||
|
||||
// 超级管理员角色编码,拥有全部权限
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import { Provide } from "@midwayjs/core";
|
||||
import { InjectRepository } from "@midwayjs/sequelize";
|
||||
import { Repository } from "sequelize-typescript";
|
||||
import { Op, UniqueConstraintError } from "sequelize";
|
||||
import { Context } from "@midwayjs/koa";
|
||||
import Acceptance from "../entity/acceptance.entity";
|
||||
import Customer from "../entity/customer.entity";
|
||||
import SysUserRole from "../entity/sysUserRole.entity";
|
||||
import SysRole from "../entity/sysRole.entity";
|
||||
|
||||
@Provide()
|
||||
export class AcceptanceService {
|
||||
@InjectRepository(Acceptance)
|
||||
acceptModel!: Repository<Acceptance>;
|
||||
|
||||
@InjectRepository(Customer)
|
||||
customerModel!: Repository<Customer>;
|
||||
|
||||
@InjectRepository(SysUserRole)
|
||||
userRoleModel!: Repository<SysUserRole>;
|
||||
|
||||
@InjectRepository(SysRole)
|
||||
roleModel!: Repository<SysRole>;
|
||||
|
||||
private async isAdmin(userId: number): Promise<boolean> {
|
||||
const userRoles = await this.userRoleModel.findAll({ where: { userId }, include: [this.roleModel] });
|
||||
return userRoles.some((ur) => (ur as any).role?.roleCode === "ADMIN");
|
||||
}
|
||||
|
||||
private async isCustomerOwner(userId: number, customerId: number): Promise<boolean> {
|
||||
if (await this.isAdmin(userId)) return true;
|
||||
const customer = await this.customerModel.findByPk(customerId);
|
||||
return !!customer && customer.salesmanId === userId;
|
||||
}
|
||||
|
||||
private async generateAcceptNo(retry: number = 0): Promise<string> {
|
||||
const now = new Date();
|
||||
const d = new Date(now.getTime() + 8 * 60 * 60 * 1000);
|
||||
const yyyymmdd = `${d.getUTCFullYear()}${String(d.getUTCMonth() + 1).padStart(2, "0")}${String(d.getUTCDate()).padStart(2, "0")}`;
|
||||
const prefix = `YS${yyyymmdd}`;
|
||||
const last = await this.acceptModel.findOne({ where: { acceptNo: { [Op.like]: `${prefix}%` } }, order: [["accept_no", "DESC"]] });
|
||||
let seq = 1;
|
||||
if (last) seq = (parseInt(last.acceptNo.slice(-3), 10) || 0) + 1;
|
||||
const result = `${prefix}${String(seq).padStart(3, "0")}`;
|
||||
const exists = await this.acceptModel.findOne({ where: { acceptNo: result } });
|
||||
if (exists) {
|
||||
if (retry < 1) return this.generateAcceptNo(retry + 1);
|
||||
throw new Error("验收单号生成冲突,请重试");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async list(query: any, ctx: Context): Promise<any> {
|
||||
const page = Number(query.page) || 1;
|
||||
const pageSize = Number(query.pageSize) || 10;
|
||||
const userId = ctx.state.user.userId;
|
||||
const where: any = {};
|
||||
|
||||
if (query.keyword) {
|
||||
where[Op.or] = [{ acceptor: { [Op.like]: `%${query.keyword}%` } }, { acceptOpinion: { [Op.like]: `%${query.keyword}%` } }];
|
||||
}
|
||||
if (query.customerId !== undefined) where.customerId = query.customerId;
|
||||
|
||||
if (!(await this.isAdmin(userId))) {
|
||||
const myCustomers = await this.customerModel.findAll({ where: { salesmanId: userId }, attributes: ["id"] });
|
||||
const ids = myCustomers.map((c) => c.id);
|
||||
if (ids.length === 0) return { code: 200, message: "获取项目验收列表成功", data: { list: [], total: 0, page: Number(page), pageSize: Number(pageSize) } };
|
||||
if (where.customerId !== undefined) {
|
||||
if (!ids.includes(where.customerId)) return { code: 200, message: "获取项目验收列表成功", data: { list: [], total: 0, page: Number(page), pageSize: Number(pageSize) } };
|
||||
} else {
|
||||
where.customerId = { [Op.in]: ids };
|
||||
}
|
||||
}
|
||||
|
||||
const { count, rows } = await this.acceptModel.findAndCountAll({ where, limit: pageSize, offset: (page - 1) * pageSize, order: [["created_at", "DESC"]] });
|
||||
return { code: 200, message: "获取项目验收列表成功", data: { list: rows.map((r) => r.toJSON()), total: count, page: Number(page), pageSize: Number(pageSize) } };
|
||||
}
|
||||
|
||||
async detail(id: number, ctx: Context): Promise<any> {
|
||||
const record = await this.acceptModel.findByPk(id);
|
||||
if (!record) return { code: 404, message: "项目验收不存在", data: null };
|
||||
if (!(await this.isCustomerOwner(ctx.state.user.userId, record.customerId))) return { code: 403, message: "无权限访问该验收", data: null };
|
||||
return { code: 200, message: "获取项目验收详情成功", data: record.toJSON() };
|
||||
}
|
||||
|
||||
async create(data: any, ctx: Context): Promise<any> {
|
||||
if (!(await this.isCustomerOwner(ctx.state.user.userId, data.customerId))) return { code: 403, message: "无权限为该客户创建验收", data: null };
|
||||
try {
|
||||
data.acceptNo = await this.generateAcceptNo();
|
||||
const created = await this.acceptModel.create(data);
|
||||
return { code: 200, message: "创建项目验收成功", data: created.toJSON() };
|
||||
} catch (err: any) {
|
||||
if (err instanceof UniqueConstraintError) {
|
||||
data.acceptNo = await this.generateAcceptNo();
|
||||
const created = await this.acceptModel.create(data);
|
||||
return { code: 200, message: "创建项目验收成功", data: created.toJSON() };
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async update(id: number, data: any, ctx: Context): Promise<any> {
|
||||
const record = await this.acceptModel.findByPk(id);
|
||||
if (!record) return { code: 404, message: "项目验收不存在", data: null };
|
||||
if (!(await this.isAdmin(ctx.state.user.userId))) {
|
||||
if (!(await this.isCustomerOwner(ctx.state.user.userId, record.customerId))) return { code: 403, message: "无权限修改该验收", data: null };
|
||||
delete data.customerId;
|
||||
}
|
||||
delete data.acceptNo;
|
||||
await record.update(data);
|
||||
return { code: 200, message: "更新项目验收成功", data: record.toJSON() };
|
||||
}
|
||||
|
||||
async delete(id: number, ctx: Context): Promise<any> {
|
||||
const record = await this.acceptModel.findByPk(id);
|
||||
if (!record) return { code: 404, message: "项目验收不存在", data: null };
|
||||
if (!(await this.isCustomerOwner(ctx.state.user.userId, record.customerId))) return { code: 403, message: "无权限删除该验收", data: null };
|
||||
await record.destroy();
|
||||
return { code: 200, message: "删除项目验收成功", data: null };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
import { Provide } from "@midwayjs/core";
|
||||
import { InjectRepository } from "@midwayjs/sequelize";
|
||||
import { Repository } from "sequelize-typescript";
|
||||
import { Op, UniqueConstraintError } from "sequelize";
|
||||
import { Context } from "@midwayjs/koa";
|
||||
import Contract from "../entity/contract.entity";
|
||||
import Customer from "../entity/customer.entity";
|
||||
import SysUserRole from "../entity/sysUserRole.entity";
|
||||
import SysRole from "../entity/sysRole.entity";
|
||||
|
||||
@Provide()
|
||||
export class ContractService {
|
||||
@InjectRepository(Contract)
|
||||
contractModel!: Repository<Contract>;
|
||||
|
||||
@InjectRepository(Customer)
|
||||
customerModel!: Repository<Customer>;
|
||||
|
||||
@InjectRepository(SysUserRole)
|
||||
userRoleModel!: Repository<SysUserRole>;
|
||||
|
||||
@InjectRepository(SysRole)
|
||||
roleModel!: Repository<SysRole>;
|
||||
|
||||
private async isAdmin(userId: number): Promise<boolean> {
|
||||
const userRoles = await this.userRoleModel.findAll({ where: { userId }, include: [this.roleModel] });
|
||||
return userRoles.some((ur) => (ur as any).role?.roleCode === "ADMIN");
|
||||
}
|
||||
|
||||
private async isCustomerOwner(userId: number, customerId: number): Promise<boolean> {
|
||||
if (await this.isAdmin(userId)) return true;
|
||||
const customer = await this.customerModel.findByPk(customerId);
|
||||
return !!customer && customer.salesmanId === userId;
|
||||
}
|
||||
|
||||
private async generateContractNo(retry: number = 0): Promise<string> {
|
||||
const now = new Date();
|
||||
const d = new Date(now.getTime() + 8 * 60 * 60 * 1000);
|
||||
const yyyy = d.getUTCFullYear();
|
||||
const yyyymmdd = `${yyyy}${String(d.getUTCMonth() + 1).padStart(2, "0")}${String(d.getUTCDate()).padStart(2, "0")}`;
|
||||
const prefix = `HT${yyyymmdd}`;
|
||||
const last = await this.contractModel.findOne({ where: { contractNo: { [Op.like]: `${prefix}%` } }, order: [["contract_no", "DESC"]] });
|
||||
let seq = 1;
|
||||
if (last) seq = (parseInt(last.contractNo.slice(-3), 10) || 0) + 1;
|
||||
const result = `${prefix}${String(seq).padStart(3, "0")}`;
|
||||
const exists = await this.contractModel.findOne({ where: { contractNo: result } });
|
||||
if (exists) {
|
||||
if (retry < 1) return this.generateContractNo(retry + 1);
|
||||
throw new Error("合同单号生成冲突,请重试");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private async generateXtContractNo(): Promise<string> {
|
||||
const now = new Date();
|
||||
const d = new Date(now.getTime() + 8 * 60 * 60 * 1000);
|
||||
const prefix = `XT${d.getUTCFullYear()}`;
|
||||
const last = await this.contractModel.findOne({ where: { xtContractNo: { [Op.like]: `${prefix}%` } }, order: [["xt_contract_no", "DESC"]] });
|
||||
let seq = 1;
|
||||
if (last) {
|
||||
const lastNo = last.xtContractNo ?? "";
|
||||
const seqStr = lastNo.slice(-4);
|
||||
seq = (parseInt(seqStr, 10) || 0) + 1;
|
||||
}
|
||||
return `${prefix}${String(seq).padStart(4, "0")}`;
|
||||
}
|
||||
|
||||
async list(query: any, ctx: Context): Promise<any> {
|
||||
const page = Number(query.page) || 1;
|
||||
const pageSize = Number(query.pageSize) || 10;
|
||||
const userId = ctx.state.user.userId;
|
||||
const where: any = {};
|
||||
|
||||
if (query.keyword) {
|
||||
where[Op.or] = [{ xtContractNo: { [Op.like]: `%${query.keyword}%` } }, { contractDesc: { [Op.like]: `%${query.keyword}%` } }];
|
||||
}
|
||||
if (query.customerId !== undefined) where.customerId = query.customerId;
|
||||
if (query.salesmanId) where.salesmanId = query.salesmanId;
|
||||
if (query.status !== undefined) where.status = query.status;
|
||||
|
||||
if (!(await this.isAdmin(userId))) {
|
||||
const myCustomers = await this.customerModel.findAll({ where: { salesmanId: userId }, attributes: ["id"] });
|
||||
const ids = myCustomers.map((c) => c.id);
|
||||
if (ids.length === 0) return { code: 200, message: "获取项目合同列表成功", data: { list: [], total: 0, page: Number(page), pageSize: Number(pageSize) } };
|
||||
if (where.customerId !== undefined) {
|
||||
if (!ids.includes(where.customerId)) return { code: 200, message: "获取项目合同列表成功", data: { list: [], total: 0, page: Number(page), pageSize: Number(pageSize) } };
|
||||
} else {
|
||||
where.customerId = { [Op.in]: ids };
|
||||
}
|
||||
delete where.salesmanId;
|
||||
}
|
||||
|
||||
const { count, rows } = await this.contractModel.findAndCountAll({ where, limit: pageSize, offset: (page - 1) * pageSize, order: [["created_at", "DESC"]] });
|
||||
return { code: 200, message: "获取项目合同列表成功", data: { list: rows.map((r) => r.toJSON()), total: count, page: Number(page), pageSize: Number(pageSize) } };
|
||||
}
|
||||
|
||||
async detail(id: number, ctx: Context): Promise<any> {
|
||||
const record = await this.contractModel.findByPk(id);
|
||||
if (!record) return { code: 404, message: "项目合同不存在", data: null };
|
||||
if (!(await this.isCustomerOwner(ctx.state.user.userId, record.customerId))) return { code: 403, message: "无权限访问该合同", data: null };
|
||||
return { code: 200, message: "获取项目合同详情成功", data: record.toJSON() };
|
||||
}
|
||||
|
||||
async create(data: any, ctx: Context): Promise<any> {
|
||||
const userId = ctx.state.user.userId;
|
||||
if (!(await this.isCustomerOwner(userId, data.customerId))) return { code: 403, message: "无权限为该客户创建合同", data: null };
|
||||
if (!(await this.isAdmin(userId))) data.salesmanId = userId;
|
||||
try {
|
||||
data.contractNo = await this.generateContractNo();
|
||||
if (!data.xtContractNo) data.xtContractNo = await this.generateXtContractNo();
|
||||
const created = await this.contractModel.create(data);
|
||||
return { code: 200, message: "创建项目合同成功", data: created.toJSON() };
|
||||
} catch (err: any) {
|
||||
if (err instanceof UniqueConstraintError) {
|
||||
data.contractNo = await this.generateContractNo();
|
||||
const created = await this.contractModel.create(data);
|
||||
return { code: 200, message: "创建项目合同成功", data: created.toJSON() };
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async update(id: number, data: any, ctx: Context): Promise<any> {
|
||||
const record = await this.contractModel.findByPk(id);
|
||||
if (!record) return { code: 404, message: "项目合同不存在", data: null };
|
||||
const isAdmin = await this.isAdmin(ctx.state.user.userId);
|
||||
if (!isAdmin) {
|
||||
if (!(await this.isCustomerOwner(ctx.state.user.userId, record.customerId))) return { code: 403, message: "无权限修改该合同", data: null };
|
||||
delete data.salesmanId;
|
||||
delete data.customerId;
|
||||
}
|
||||
delete data.contractNo;
|
||||
await record.update(data);
|
||||
return { code: 200, message: "更新项目合同成功", data: record.toJSON() };
|
||||
}
|
||||
|
||||
async delete(id: number, ctx: Context): Promise<any> {
|
||||
const record = await this.contractModel.findByPk(id);
|
||||
if (!record) return { code: 404, message: "项目合同不存在", data: null };
|
||||
if (!(await this.isCustomerOwner(ctx.state.user.userId, record.customerId))) return { code: 403, message: "无权限删除该合同", data: null };
|
||||
await record.destroy();
|
||||
return { code: 200, message: "删除项目合同成功", data: null };
|
||||
}
|
||||
|
||||
async findOptions(customerId: number): Promise<any> {
|
||||
const where: any = {};
|
||||
if (customerId) where.customerId = customerId;
|
||||
const records = await this.contractModel.findAll({ where, order: [["created_at", "DESC"]] });
|
||||
return { code: 200, message: "获取项目合同选项成功", data: records.map((r) => r.toJSON()) };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { Provide } from "@midwayjs/core";
|
||||
import { InjectRepository } from "@midwayjs/sequelize";
|
||||
import { Repository } from "sequelize-typescript";
|
||||
import { Op } from "sequelize";
|
||||
import { Context } from "@midwayjs/koa";
|
||||
import CustomerBuilding from "../entity/customer_building.entity";
|
||||
import Customer from "../entity/customer.entity";
|
||||
import SysUserRole from "../entity/sysUserRole.entity";
|
||||
import SysRole from "../entity/sysRole.entity";
|
||||
|
||||
@Provide()
|
||||
export class CustomerBuildingService {
|
||||
@InjectRepository(CustomerBuilding)
|
||||
buildingModel!: Repository<CustomerBuilding>;
|
||||
|
||||
@InjectRepository(Customer)
|
||||
customerModel!: Repository<Customer>;
|
||||
|
||||
@InjectRepository(SysUserRole)
|
||||
userRoleModel!: Repository<SysUserRole>;
|
||||
|
||||
@InjectRepository(SysRole)
|
||||
roleModel!: Repository<SysRole>;
|
||||
|
||||
private async isAdmin(userId: number): Promise<boolean> {
|
||||
const userRoles = await this.userRoleModel.findAll({ where: { userId }, include: [this.roleModel] });
|
||||
return userRoles.some((ur) => (ur as any).role?.roleCode === "ADMIN");
|
||||
}
|
||||
|
||||
private async isCustomerOwner(userId: number, customerId: number): Promise<boolean> {
|
||||
if (await this.isAdmin(userId)) return true;
|
||||
const customer = await this.customerModel.findByPk(customerId);
|
||||
return !!customer && customer.salesmanId === userId;
|
||||
}
|
||||
|
||||
async list(query: any, ctx: Context): Promise<any> {
|
||||
const page = Number(query.page) || 1;
|
||||
const pageSize = Number(query.pageSize) || 10;
|
||||
const userId = ctx.state.user.userId;
|
||||
const where: any = {};
|
||||
|
||||
if (query.keyword) {
|
||||
where[Op.or] = [{ buildingName: { [Op.like]: `%${query.keyword}%` } }, { purpose: { [Op.like]: `%${query.keyword}%` } }];
|
||||
}
|
||||
if (query.customerId !== undefined) where.customerId = query.customerId;
|
||||
|
||||
if (!(await this.isAdmin(userId))) {
|
||||
const myCustomers = await this.customerModel.findAll({ where: { salesmanId: userId }, attributes: ["id"] });
|
||||
const ids = myCustomers.map((c) => c.id);
|
||||
if (ids.length === 0) return { code: 200, message: "获取客户建筑列表成功", data: { list: [], total: 0, page: Number(page), pageSize: Number(pageSize) } };
|
||||
if (where.customerId !== undefined) {
|
||||
if (!ids.includes(where.customerId)) return { code: 200, message: "获取客户建筑列表成功", data: { list: [], total: 0, page: Number(page), pageSize: Number(pageSize) } };
|
||||
} else {
|
||||
where.customerId = { [Op.in]: ids };
|
||||
}
|
||||
}
|
||||
|
||||
const { count, rows } = await this.buildingModel.findAndCountAll({ where, limit: pageSize, offset: (page - 1) * pageSize, order: [["created_at", "DESC"]] });
|
||||
return { code: 200, message: "获取客户建筑列表成功", data: { list: rows.map((r) => r.toJSON()), total: count, page: Number(page), pageSize: Number(pageSize) } };
|
||||
}
|
||||
|
||||
async detail(id: number, ctx: Context): Promise<any> {
|
||||
const record = await this.buildingModel.findByPk(id);
|
||||
if (!record) return { code: 404, message: "客户建筑不存在", data: null };
|
||||
if (!(await this.isCustomerOwner(ctx.state.user.userId, record.customerId))) return { code: 403, message: "无权限访问该建筑", data: null };
|
||||
return { code: 200, message: "获取客户建筑详情成功", data: record.toJSON() };
|
||||
}
|
||||
|
||||
async create(data: any, ctx: Context): Promise<any> {
|
||||
if (!(await this.isCustomerOwner(ctx.state.user.userId, data.customerId))) return { code: 403, message: "无权限为该客户创建建筑", data: null };
|
||||
const created = await this.buildingModel.create(data);
|
||||
return { code: 200, message: "创建客户建筑成功", data: created.toJSON() };
|
||||
}
|
||||
|
||||
async update(id: number, data: any, ctx: Context): Promise<any> {
|
||||
const record = await this.buildingModel.findByPk(id);
|
||||
if (!record) return { code: 404, message: "客户建筑不存在", data: null };
|
||||
if (!(await this.isAdmin(ctx.state.user.userId))) {
|
||||
if (!(await this.isCustomerOwner(ctx.state.user.userId, record.customerId))) return { code: 403, message: "无权限修改该建筑", data: null };
|
||||
delete data.customerId;
|
||||
}
|
||||
await record.update(data);
|
||||
return { code: 200, message: "更新客户建筑成功", data: record.toJSON() };
|
||||
}
|
||||
|
||||
async delete(id: number, ctx: Context): Promise<any> {
|
||||
const record = await this.buildingModel.findByPk(id);
|
||||
if (!record) return { code: 404, message: "客户建筑不存在", data: null };
|
||||
if (!(await this.isCustomerOwner(ctx.state.user.userId, record.customerId))) return { code: 403, message: "无权限删除该建筑", data: null };
|
||||
await record.destroy();
|
||||
return { code: 200, message: "删除客户建筑成功", data: null };
|
||||
}
|
||||
|
||||
async findOptions(customerId: number): Promise<any> {
|
||||
const records = await this.buildingModel.findAll({ where: { customerId }, order: [["created_at", "DESC"]] });
|
||||
return { code: 200, message: "获取客户建筑选项成功", data: records.map((r) => r.toJSON()) };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { Provide } from "@midwayjs/core";
|
||||
import { InjectRepository } from "@midwayjs/sequelize";
|
||||
import { Repository } from "sequelize-typescript";
|
||||
import { Op } from "sequelize";
|
||||
import { Context } from "@midwayjs/koa";
|
||||
import CustomerRoom from "../entity/customer_room.entity";
|
||||
import Customer from "../entity/customer.entity";
|
||||
import CustomerBuilding from "../entity/customer_building.entity";
|
||||
import CustomerDepartment from "../entity/customer_department.entity";
|
||||
import SysUserRole from "../entity/sysUserRole.entity";
|
||||
import SysRole from "../entity/sysRole.entity";
|
||||
|
||||
@Provide()
|
||||
export class CustomerRoomService {
|
||||
@InjectRepository(CustomerRoom)
|
||||
roomModel!: Repository<CustomerRoom>;
|
||||
|
||||
@InjectRepository(Customer)
|
||||
customerModel!: Repository<Customer>;
|
||||
|
||||
@InjectRepository(CustomerBuilding)
|
||||
buildingModel!: Repository<CustomerBuilding>;
|
||||
|
||||
@InjectRepository(CustomerDepartment)
|
||||
deptModel!: Repository<CustomerDepartment>;
|
||||
|
||||
@InjectRepository(SysUserRole)
|
||||
userRoleModel!: Repository<SysUserRole>;
|
||||
|
||||
@InjectRepository(SysRole)
|
||||
roleModel!: Repository<SysRole>;
|
||||
|
||||
private async isAdmin(userId: number): Promise<boolean> {
|
||||
const userRoles = await this.userRoleModel.findAll({ where: { userId }, include: [this.roleModel] });
|
||||
return userRoles.some((ur) => (ur as any).role?.roleCode === "ADMIN");
|
||||
}
|
||||
|
||||
private async isCustomerOwner(userId: number, customerId: number): Promise<boolean> {
|
||||
if (await this.isAdmin(userId)) return true;
|
||||
const customer = await this.customerModel.findByPk(customerId);
|
||||
return !!customer && customer.salesmanId === userId;
|
||||
}
|
||||
|
||||
async list(query: any, ctx: Context): Promise<any> {
|
||||
const page = Number(query.page) || 1;
|
||||
const pageSize = Number(query.pageSize) || 10;
|
||||
const userId = ctx.state.user.userId;
|
||||
const where: any = {};
|
||||
|
||||
if (query.keyword) {
|
||||
where[Op.or] = [{ roomName: { [Op.like]: `%${query.keyword}%` } }, { roomCode: { [Op.like]: `%${query.keyword}%` } }];
|
||||
}
|
||||
if (query.customerId !== undefined) where.customerId = query.customerId;
|
||||
if (query.buildingId !== undefined) where.buildingId = query.buildingId;
|
||||
|
||||
if (!(await this.isAdmin(userId))) {
|
||||
const myCustomers = await this.customerModel.findAll({ where: { salesmanId: userId }, attributes: ["id"] });
|
||||
const ids = myCustomers.map((c) => c.id);
|
||||
if (ids.length === 0) return { code: 200, message: "获取客户房间列表成功", data: { list: [], total: 0, page: Number(page), pageSize: Number(pageSize) } };
|
||||
if (where.customerId !== undefined) {
|
||||
if (!ids.includes(where.customerId)) return { code: 200, message: "获取客户房间列表成功", data: { list: [], total: 0, page: Number(page), pageSize: Number(pageSize) } };
|
||||
} else {
|
||||
where.customerId = { [Op.in]: ids };
|
||||
}
|
||||
}
|
||||
|
||||
const { count, rows } = await this.roomModel.findAndCountAll({ where, limit: pageSize, offset: (page - 1) * pageSize, order: [["created_at", "DESC"]] });
|
||||
return { code: 200, message: "获取客户房间列表成功", data: { list: rows.map((r) => r.toJSON()), total: count, page: Number(page), pageSize: Number(pageSize) } };
|
||||
}
|
||||
|
||||
async detail(id: number, ctx: Context): Promise<any> {
|
||||
const record = await this.roomModel.findByPk(id);
|
||||
if (!record) return { code: 404, message: "客户房间不存在", data: null };
|
||||
if (!(await this.isCustomerOwner(ctx.state.user.userId, record.customerId))) return { code: 403, message: "无权限访问该房间", data: null };
|
||||
return { code: 200, message: "获取客户房间详情成功", data: record.toJSON() };
|
||||
}
|
||||
|
||||
async create(data: any, ctx: Context): Promise<any> {
|
||||
if (!(await this.isCustomerOwner(ctx.state.user.userId, data.customerId))) return { code: 403, message: "无权限为该客户创建房间", data: null };
|
||||
const created = await this.roomModel.create(data);
|
||||
return { code: 200, message: "创建客户房间成功", data: created.toJSON() };
|
||||
}
|
||||
|
||||
async update(id: number, data: any, ctx: Context): Promise<any> {
|
||||
const record = await this.roomModel.findByPk(id);
|
||||
if (!record) return { code: 404, message: "客户房间不存在", data: null };
|
||||
if (!(await this.isAdmin(ctx.state.user.userId))) {
|
||||
if (!(await this.isCustomerOwner(ctx.state.user.userId, record.customerId))) return { code: 403, message: "无权限修改该房间", data: null };
|
||||
delete data.customerId;
|
||||
}
|
||||
await record.update(data);
|
||||
return { code: 200, message: "更新客户房间成功", data: record.toJSON() };
|
||||
}
|
||||
|
||||
async delete(id: number, ctx: Context): Promise<any> {
|
||||
const record = await this.roomModel.findByPk(id);
|
||||
if (!record) return { code: 404, message: "客户房间不存在", data: null };
|
||||
if (!(await this.isCustomerOwner(ctx.state.user.userId, record.customerId))) return { code: 403, message: "无权限删除该房间", data: null };
|
||||
await record.destroy();
|
||||
return { code: 200, message: "删除客户房间成功", data: null };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import { Provide } from "@midwayjs/core";
|
||||
import { InjectRepository } from "@midwayjs/sequelize";
|
||||
import { Repository } from "sequelize-typescript";
|
||||
import { Op, UniqueConstraintError } from "sequelize";
|
||||
import { Context } from "@midwayjs/koa";
|
||||
import CustomerVisit from "../entity/customer_visit.entity";
|
||||
import Customer from "../entity/customer.entity";
|
||||
import SysUserRole from "../entity/sysUserRole.entity";
|
||||
import SysRole from "../entity/sysRole.entity";
|
||||
|
||||
@Provide()
|
||||
export class CustomerVisitService {
|
||||
@InjectRepository(CustomerVisit)
|
||||
visitModel!: Repository<CustomerVisit>;
|
||||
|
||||
@InjectRepository(Customer)
|
||||
customerModel!: Repository<Customer>;
|
||||
|
||||
@InjectRepository(SysUserRole)
|
||||
userRoleModel!: Repository<SysUserRole>;
|
||||
|
||||
@InjectRepository(SysRole)
|
||||
roleModel!: Repository<SysRole>;
|
||||
|
||||
private async isAdmin(userId: number): Promise<boolean> {
|
||||
const userRoles = await this.userRoleModel.findAll({ where: { userId }, include: [this.roleModel] });
|
||||
return userRoles.some((ur) => (ur as any).role?.roleCode === "ADMIN");
|
||||
}
|
||||
|
||||
private async isCustomerOwner(userId: number, customerId: number): Promise<boolean> {
|
||||
if (await this.isAdmin(userId)) return true;
|
||||
const customer = await this.customerModel.findByPk(customerId);
|
||||
return !!customer && customer.salesmanId === userId;
|
||||
}
|
||||
|
||||
private async generateVisitNo(retry: number = 0): Promise<string> {
|
||||
const now = new Date();
|
||||
const offsetMs = now.getTime() + 8 * 60 * 60 * 1000;
|
||||
const d = new Date(offsetMs);
|
||||
const yyyymmdd = `${d.getUTCFullYear()}${String(d.getUTCMonth() + 1).padStart(2, "0")}${String(d.getUTCDate()).padStart(2, "0")}`;
|
||||
const prefix = `BF${yyyymmdd}`;
|
||||
const last = await this.visitModel.findOne({ where: { visitNo: { [Op.like]: `${prefix}%` } }, order: [["visit_no", "DESC"]] });
|
||||
let seq = 1;
|
||||
if (last) seq = (parseInt(last.visitNo.slice(-3), 10) || 0) + 1;
|
||||
const result = `${prefix}${String(seq).padStart(3, "0")}`;
|
||||
const exists = await this.visitModel.findOne({ where: { visitNo: result } });
|
||||
if (exists) {
|
||||
if (retry < 1) return this.generateVisitNo(retry + 1);
|
||||
throw new Error("拜访单号生成冲突,请重试");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async list(query: any, ctx: Context): Promise<any> {
|
||||
const page = Number(query.page) || 1;
|
||||
const pageSize = Number(query.pageSize) || 10;
|
||||
const userId = ctx.state.user.userId;
|
||||
const where: any = {};
|
||||
|
||||
if (query.keyword) {
|
||||
where[Op.or] = [
|
||||
{ visitPerson: { [Op.like]: `%${query.keyword}%` } },
|
||||
{ visitPhone: { [Op.like]: `%${query.keyword}%` } }
|
||||
];
|
||||
}
|
||||
if (query.customerId !== undefined) where.customerId = query.customerId;
|
||||
if (query.salesmanId) where.salesmanId = query.salesmanId;
|
||||
|
||||
if (!(await this.isAdmin(userId))) {
|
||||
const myCustomers = await this.customerModel.findAll({ where: { salesmanId: userId }, attributes: ["id"] });
|
||||
const ids = myCustomers.map((c) => c.id);
|
||||
if (ids.length === 0) return { code: 200, message: "获取客户拜访列表成功", data: { list: [], total: 0, page: Number(page), pageSize: Number(pageSize) } };
|
||||
if (where.customerId !== undefined) {
|
||||
if (!ids.includes(where.customerId)) return { code: 200, message: "获取客户拜访列表成功", data: { list: [], total: 0, page: Number(page), pageSize: Number(pageSize) } };
|
||||
} else {
|
||||
where.customerId = { [Op.in]: ids };
|
||||
}
|
||||
}
|
||||
|
||||
const { count, rows } = await this.visitModel.findAndCountAll({ where, limit: pageSize, offset: (page - 1) * pageSize, order: [["created_at", "DESC"]] });
|
||||
return { code: 200, message: "获取客户拜访列表成功", data: { list: rows.map((r) => r.toJSON()), total: count, page: Number(page), pageSize: Number(pageSize) } };
|
||||
}
|
||||
|
||||
async detail(id: number, ctx: Context): Promise<any> {
|
||||
const record = await this.visitModel.findByPk(id);
|
||||
if (!record) return { code: 404, message: "客户拜访不存在", data: null };
|
||||
if (!(await this.isCustomerOwner(ctx.state.user.userId, record.customerId))) return { code: 403, message: "无权限访问该拜访记录", data: null };
|
||||
return { code: 200, message: "获取客户拜访详情成功", data: record.toJSON() };
|
||||
}
|
||||
|
||||
async create(data: any, ctx: Context): Promise<any> {
|
||||
const userId = ctx.state.user.userId;
|
||||
if (!(await this.isCustomerOwner(userId, data.customerId))) return { code: 403, message: "无权限为该客户创建拜访记录", data: null };
|
||||
if (!(await this.isAdmin(userId))) data.salesmanId = userId;
|
||||
try {
|
||||
data.visitNo = await this.generateVisitNo();
|
||||
const created = await this.visitModel.create(data);
|
||||
return { code: 200, message: "创建客户拜访成功", data: created.toJSON() };
|
||||
} catch (err: any) {
|
||||
if (err instanceof UniqueConstraintError) {
|
||||
data.visitNo = await this.generateVisitNo();
|
||||
const created = await this.visitModel.create(data);
|
||||
return { code: 200, message: "创建客户拜访成功", data: created.toJSON() };
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async update(id: number, data: any, ctx: Context): Promise<any> {
|
||||
const record = await this.visitModel.findByPk(id);
|
||||
if (!record) return { code: 404, message: "客户拜访不存在", data: null };
|
||||
const isAdmin = await this.isAdmin(ctx.state.user.userId);
|
||||
if (!isAdmin) {
|
||||
if (!(await this.isCustomerOwner(ctx.state.user.userId, record.customerId))) return { code: 403, message: "无权限修改该拜访记录", data: null };
|
||||
delete data.salesmanId;
|
||||
delete data.customerId;
|
||||
}
|
||||
delete data.visitNo;
|
||||
await record.update(data);
|
||||
return { code: 200, message: "更新客户拜访成功", data: record.toJSON() };
|
||||
}
|
||||
|
||||
async delete(id: number, ctx: Context): Promise<any> {
|
||||
const record = await this.visitModel.findByPk(id);
|
||||
if (!record) return { code: 404, message: "客户拜访不存在", data: null };
|
||||
if (!(await this.isCustomerOwner(ctx.state.user.userId, record.customerId))) return { code: 403, message: "无权限删除该拜访记录", data: null };
|
||||
await record.destroy();
|
||||
return { code: 200, message: "删除客户拜访成功", data: null };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import { Provide } from "@midwayjs/core";
|
||||
import { InjectRepository } from "@midwayjs/sequelize";
|
||||
import { Repository } from "sequelize-typescript";
|
||||
import { Op, UniqueConstraintError } from "sequelize";
|
||||
import { Context } from "@midwayjs/koa";
|
||||
import ProjectPlan from "../entity/project_plan.entity";
|
||||
import ProjectPlanDetail from "../entity/project_plan_detail.entity";
|
||||
import Customer from "../entity/customer.entity";
|
||||
import SysUserRole from "../entity/sysUserRole.entity";
|
||||
import SysRole from "../entity/sysRole.entity";
|
||||
|
||||
@Provide()
|
||||
export class ProjectPlanService {
|
||||
@InjectRepository(ProjectPlan)
|
||||
planModel!: Repository<ProjectPlan>;
|
||||
|
||||
@InjectRepository(ProjectPlanDetail)
|
||||
detailModel!: Repository<ProjectPlanDetail>;
|
||||
|
||||
@InjectRepository(Customer)
|
||||
customerModel!: Repository<Customer>;
|
||||
|
||||
@InjectRepository(SysUserRole)
|
||||
userRoleModel!: Repository<SysUserRole>;
|
||||
|
||||
@InjectRepository(SysRole)
|
||||
roleModel!: Repository<SysRole>;
|
||||
|
||||
private async isAdmin(userId: number): Promise<boolean> {
|
||||
const userRoles = await this.userRoleModel.findAll({ where: { userId }, include: [this.roleModel] });
|
||||
return userRoles.some((ur) => (ur as any).role?.roleCode === "ADMIN");
|
||||
}
|
||||
|
||||
private async isCustomerOwner(userId: number, customerId: number): Promise<boolean> {
|
||||
if (await this.isAdmin(userId)) return true;
|
||||
const customer = await this.customerModel.findByPk(customerId);
|
||||
return !!customer && customer.salesmanId === userId;
|
||||
}
|
||||
|
||||
private async generatePlanNo(retry: number = 0): Promise<string> {
|
||||
const now = new Date();
|
||||
const d = new Date(now.getTime() + 8 * 60 * 60 * 1000);
|
||||
const yyyymmdd = `${d.getUTCFullYear()}${String(d.getUTCMonth() + 1).padStart(2, "0")}${String(d.getUTCDate()).padStart(2, "0")}`;
|
||||
const prefix = `FA${yyyymmdd}`;
|
||||
const last = await this.planModel.findOne({ where: { planNo: { [Op.like]: `${prefix}%` } }, order: [["plan_no", "DESC"]] });
|
||||
let seq = 1;
|
||||
if (last) seq = (parseInt(last.planNo.slice(-3), 10) || 0) + 1;
|
||||
const result = `${prefix}${String(seq).padStart(3, "0")}`;
|
||||
const exists = await this.planModel.findOne({ where: { planNo: result } });
|
||||
if (exists) {
|
||||
if (retry < 1) return this.generatePlanNo(retry + 1);
|
||||
throw new Error("方案单号生成冲突,请重试");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async list(query: any, ctx: Context): Promise<any> {
|
||||
const page = Number(query.page) || 1;
|
||||
const pageSize = Number(query.pageSize) || 10;
|
||||
const userId = ctx.state.user.userId;
|
||||
const where: any = {};
|
||||
|
||||
if (query.keyword) {
|
||||
where[Op.or] = [{ registrar: { [Op.like]: `%${query.keyword}%` } }, { planDesc: { [Op.like]: `%${query.keyword}%` } }];
|
||||
}
|
||||
if (query.customerId !== undefined) where.customerId = query.customerId;
|
||||
if (query.status !== undefined) where.status = query.status;
|
||||
|
||||
if (!(await this.isAdmin(userId))) {
|
||||
const myCustomers = await this.customerModel.findAll({ where: { salesmanId: userId }, attributes: ["id"] });
|
||||
const ids = myCustomers.map((c) => c.id);
|
||||
if (ids.length === 0) return { code: 200, message: "获取项目方案列表成功", data: { list: [], total: 0, page: Number(page), pageSize: Number(pageSize) } };
|
||||
if (where.customerId !== undefined) {
|
||||
if (!ids.includes(where.customerId)) return { code: 200, message: "获取项目方案列表成功", data: { list: [], total: 0, page: Number(page), pageSize: Number(pageSize) } };
|
||||
} else {
|
||||
where.customerId = { [Op.in]: ids };
|
||||
}
|
||||
}
|
||||
|
||||
const { count, rows } = await this.planModel.findAndCountAll({ where, limit: pageSize, offset: (page - 1) * pageSize, order: [["created_at", "DESC"]] });
|
||||
return { code: 200, message: "获取项目方案列表成功", data: { list: rows.map((r) => r.toJSON()), total: count, page: Number(page), pageSize: Number(pageSize) } };
|
||||
}
|
||||
|
||||
async detail(id: number, ctx: Context): Promise<any> {
|
||||
const record = await this.planModel.findByPk(id, { include: [this.detailModel] });
|
||||
if (!record) return { code: 404, message: "项目方案不存在", data: null };
|
||||
if (!(await this.isCustomerOwner(ctx.state.user.userId, record.customerId))) return { code: 403, message: "无权限访问该方案", data: null };
|
||||
return { code: 200, message: "获取项目方案详情成功", data: record.toJSON() };
|
||||
}
|
||||
|
||||
async create(data: any, ctx: Context): Promise<any> {
|
||||
if (!(await this.isCustomerOwner(ctx.state.user.userId, data.customerId))) return { code: 403, message: "无权限为该客户创建方案", data: null };
|
||||
const details = data.details || [];
|
||||
delete data.details;
|
||||
try {
|
||||
data.planNo = await this.generatePlanNo();
|
||||
const created = await this.planModel.create(data);
|
||||
if (details.length > 0) {
|
||||
await this.detailModel.bulkCreate(details.map((d: any) => ({ ...d, planId: created.id, customerId: created.customerId })));
|
||||
}
|
||||
const result = await this.planModel.findByPk(created.id, { include: [this.detailModel] });
|
||||
return { code: 200, message: "创建项目方案成功", data: result?.toJSON() };
|
||||
} catch (err: any) {
|
||||
if (err instanceof UniqueConstraintError) {
|
||||
data.planNo = await this.generatePlanNo();
|
||||
const created = await this.planModel.create(data);
|
||||
if (details.length > 0) {
|
||||
await this.detailModel.bulkCreate(details.map((d: any) => ({ ...d, planId: created.id, customerId: created.customerId })));
|
||||
}
|
||||
const result = await this.planModel.findByPk(created.id, { include: [this.detailModel] });
|
||||
return { code: 200, message: "创建项目方案成功", data: result?.toJSON() };
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async update(id: number, data: any, ctx: Context): Promise<any> {
|
||||
const record = await this.planModel.findByPk(id);
|
||||
if (!record) return { code: 404, message: "项目方案不存在", data: null };
|
||||
if (!(await this.isAdmin(ctx.state.user.userId))) {
|
||||
if (!(await this.isCustomerOwner(ctx.state.user.userId, record.customerId))) return { code: 403, message: "无权限修改该方案", data: null };
|
||||
delete data.customerId;
|
||||
}
|
||||
const details = data.details;
|
||||
delete data.details;
|
||||
delete data.planNo;
|
||||
await record.update(data);
|
||||
|
||||
if (details && Array.isArray(details)) {
|
||||
await this.detailModel.destroy({ where: { planId: id } });
|
||||
if (details.length > 0) {
|
||||
await this.detailModel.bulkCreate(details.map((d: any) => ({ ...d, planId: id, customerId: record.customerId })));
|
||||
}
|
||||
}
|
||||
const result = await this.planModel.findByPk(id, { include: [this.detailModel] });
|
||||
return { code: 200, message: "更新项目方案成功", data: result?.toJSON() };
|
||||
}
|
||||
|
||||
async delete(id: number, ctx: Context): Promise<any> {
|
||||
const record = await this.planModel.findByPk(id);
|
||||
if (!record) return { code: 404, message: "项目方案不存在", data: null };
|
||||
if (!(await this.isCustomerOwner(ctx.state.user.userId, record.customerId))) return { code: 403, message: "无权限删除该方案", data: null };
|
||||
await this.detailModel.destroy({ where: { planId: id } });
|
||||
await record.destroy();
|
||||
return { code: 200, message: "删除项目方案成功", data: null };
|
||||
}
|
||||
|
||||
async findOptions(customerId: number): Promise<any> {
|
||||
const where: any = {};
|
||||
if (customerId) where.customerId = customerId;
|
||||
const records = await this.planModel.findAll({ where, order: [["created_at", "DESC"]] });
|
||||
return { code: 200, message: "获取项目方案选项成功", data: records.map((r) => r.toJSON()) };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import { Provide } from "@midwayjs/core";
|
||||
import { InjectRepository } from "@midwayjs/sequelize";
|
||||
import { Repository } from "sequelize-typescript";
|
||||
import { Op, UniqueConstraintError } from "sequelize";
|
||||
import { Context } from "@midwayjs/koa";
|
||||
import SurveyOrder from "../entity/survey_order.entity";
|
||||
import Customer from "../entity/customer.entity";
|
||||
import SysUserRole from "../entity/sysUserRole.entity";
|
||||
import SysRole from "../entity/sysRole.entity";
|
||||
|
||||
@Provide()
|
||||
export class SurveyOrderService {
|
||||
@InjectRepository(SurveyOrder)
|
||||
surveyModel!: Repository<SurveyOrder>;
|
||||
|
||||
@InjectRepository(Customer)
|
||||
customerModel!: Repository<Customer>;
|
||||
|
||||
@InjectRepository(SysUserRole)
|
||||
userRoleModel!: Repository<SysUserRole>;
|
||||
|
||||
@InjectRepository(SysRole)
|
||||
roleModel!: Repository<SysRole>;
|
||||
|
||||
private async isAdmin(userId: number): Promise<boolean> {
|
||||
const userRoles = await this.userRoleModel.findAll({ where: { userId }, include: [this.roleModel] });
|
||||
return userRoles.some((ur) => (ur as any).role?.roleCode === "ADMIN");
|
||||
}
|
||||
|
||||
private async isCustomerOwner(userId: number, customerId: number): Promise<boolean> {
|
||||
if (await this.isAdmin(userId)) return true;
|
||||
const customer = await this.customerModel.findByPk(customerId);
|
||||
return !!customer && customer.salesmanId === userId;
|
||||
}
|
||||
|
||||
private async generateSurveyNo(retry: number = 0): Promise<string> {
|
||||
const now = new Date();
|
||||
const d = new Date(now.getTime() + 8 * 60 * 60 * 1000);
|
||||
const yyyymmdd = `${d.getUTCFullYear()}${String(d.getUTCMonth() + 1).padStart(2, "0")}${String(d.getUTCDate()).padStart(2, "0")}`;
|
||||
const prefix = `KC${yyyymmdd}`;
|
||||
const last = await this.surveyModel.findOne({ where: { surveyNo: { [Op.like]: `${prefix}%` } }, order: [["survey_no", "DESC"]] });
|
||||
let seq = 1;
|
||||
if (last) seq = (parseInt(last.surveyNo.slice(-3), 10) || 0) + 1;
|
||||
const result = `${prefix}${String(seq).padStart(3, "0")}`;
|
||||
const exists = await this.surveyModel.findOne({ where: { surveyNo: result } });
|
||||
if (exists) {
|
||||
if (retry < 1) return this.generateSurveyNo(retry + 1);
|
||||
throw new Error("勘察单号生成冲突,请重试");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async list(query: any, ctx: Context): Promise<any> {
|
||||
const page = Number(query.page) || 1;
|
||||
const pageSize = Number(query.pageSize) || 10;
|
||||
const userId = ctx.state.user.userId;
|
||||
const where: any = {};
|
||||
|
||||
if (query.keyword) {
|
||||
where[Op.or] = [{ surveyor: { [Op.like]: `%${query.keyword}%` } }, { remark: { [Op.like]: `%${query.keyword}%` } }];
|
||||
}
|
||||
if (query.customerId !== undefined) where.customerId = query.customerId;
|
||||
if (query.status !== undefined) where.status = query.status;
|
||||
|
||||
if (!(await this.isAdmin(userId))) {
|
||||
const myCustomers = await this.customerModel.findAll({ where: { salesmanId: userId }, attributes: ["id"] });
|
||||
const ids = myCustomers.map((c) => c.id);
|
||||
if (ids.length === 0) return { code: 200, message: "获取勘察派单列表成功", data: { list: [], total: 0, page: Number(page), pageSize: Number(pageSize) } };
|
||||
if (where.customerId !== undefined) {
|
||||
if (!ids.includes(where.customerId)) return { code: 200, message: "获取勘察派单列表成功", data: { list: [], total: 0, page: Number(page), pageSize: Number(pageSize) } };
|
||||
} else {
|
||||
where.customerId = { [Op.in]: ids };
|
||||
}
|
||||
}
|
||||
|
||||
const { count, rows } = await this.surveyModel.findAndCountAll({ where, limit: pageSize, offset: (page - 1) * pageSize, order: [["created_at", "DESC"]] });
|
||||
return { code: 200, message: "获取勘察派单列表成功", data: { list: rows.map((r) => r.toJSON()), total: count, page: Number(page), pageSize: Number(pageSize) } };
|
||||
}
|
||||
|
||||
async detail(id: number, ctx: Context): Promise<any> {
|
||||
const record = await this.surveyModel.findByPk(id);
|
||||
if (!record) return { code: 404, message: "勘察派单不存在", data: null };
|
||||
if (!(await this.isCustomerOwner(ctx.state.user.userId, record.customerId))) return { code: 403, message: "无权限访问该勘察派单", data: null };
|
||||
return { code: 200, message: "获取勘察派单详情成功", data: record.toJSON() };
|
||||
}
|
||||
|
||||
async create(data: any, ctx: Context): Promise<any> {
|
||||
if (!(await this.isCustomerOwner(ctx.state.user.userId, data.customerId))) return { code: 403, message: "无权限为该客户创建勘察派单", data: null };
|
||||
try {
|
||||
data.surveyNo = await this.generateSurveyNo();
|
||||
const created = await this.surveyModel.create(data);
|
||||
return { code: 200, message: "创建勘察派单成功", data: created.toJSON() };
|
||||
} catch (err: any) {
|
||||
if (err instanceof UniqueConstraintError) {
|
||||
data.surveyNo = await this.generateSurveyNo();
|
||||
const created = await this.surveyModel.create(data);
|
||||
return { code: 200, message: "创建勘察派单成功", data: created.toJSON() };
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async update(id: number, data: any, ctx: Context): Promise<any> {
|
||||
const record = await this.surveyModel.findByPk(id);
|
||||
if (!record) return { code: 404, message: "勘察派单不存在", data: null };
|
||||
if (!(await this.isAdmin(ctx.state.user.userId))) {
|
||||
if (!(await this.isCustomerOwner(ctx.state.user.userId, record.customerId))) return { code: 403, message: "无权限修改该勘察派单", data: null };
|
||||
delete data.customerId;
|
||||
}
|
||||
delete data.surveyNo;
|
||||
await record.update(data);
|
||||
return { code: 200, message: "更新勘察派单成功", data: record.toJSON() };
|
||||
}
|
||||
|
||||
async delete(id: number, ctx: Context): Promise<any> {
|
||||
const record = await this.surveyModel.findByPk(id);
|
||||
if (!record) return { code: 404, message: "勘察派单不存在", data: null };
|
||||
if (!(await this.isCustomerOwner(ctx.state.user.userId, record.customerId))) return { code: 403, message: "无权限删除该勘察派单", data: null };
|
||||
await record.destroy();
|
||||
return { code: 200, message: "删除勘察派单成功", data: null };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import { Provide } from "@midwayjs/core";
|
||||
import { InjectRepository } from "@midwayjs/sequelize";
|
||||
import { Repository } from "sequelize-typescript";
|
||||
import { Op, UniqueConstraintError } from "sequelize";
|
||||
import { Context } from "@midwayjs/koa";
|
||||
import VisitRecord from "../entity/visit_record.entity";
|
||||
import Customer from "../entity/customer.entity";
|
||||
import SysUserRole from "../entity/sysUserRole.entity";
|
||||
import SysRole from "../entity/sysRole.entity";
|
||||
|
||||
@Provide()
|
||||
export class VisitRecordService {
|
||||
@InjectRepository(VisitRecord)
|
||||
visitModel!: Repository<VisitRecord>;
|
||||
|
||||
@InjectRepository(Customer)
|
||||
customerModel!: Repository<Customer>;
|
||||
|
||||
@InjectRepository(SysUserRole)
|
||||
userRoleModel!: Repository<SysUserRole>;
|
||||
|
||||
@InjectRepository(SysRole)
|
||||
roleModel!: Repository<SysRole>;
|
||||
|
||||
private async isAdmin(userId: number): Promise<boolean> {
|
||||
const userRoles = await this.userRoleModel.findAll({ where: { userId }, include: [this.roleModel] });
|
||||
return userRoles.some((ur) => (ur as any).role?.roleCode === "ADMIN");
|
||||
}
|
||||
|
||||
private async isCustomerOwner(userId: number, customerId: number): Promise<boolean> {
|
||||
if (await this.isAdmin(userId)) return true;
|
||||
const customer = await this.customerModel.findByPk(customerId);
|
||||
return !!customer && customer.salesmanId === userId;
|
||||
}
|
||||
|
||||
private async generateVisitNo(retry: number = 0): Promise<string> {
|
||||
const now = new Date();
|
||||
const d = new Date(now.getTime() + 8 * 60 * 60 * 1000);
|
||||
const yyyymmdd = `${d.getUTCFullYear()}${String(d.getUTCMonth() + 1).padStart(2, "0")}${String(d.getUTCDate()).padStart(2, "0")}`;
|
||||
const prefix = `HF${yyyymmdd}`;
|
||||
const last = await this.visitModel.findOne({ where: { visitNo: { [Op.like]: `${prefix}%` } }, order: [["visit_no", "DESC"]] });
|
||||
let seq = 1;
|
||||
if (last) seq = (parseInt(last.visitNo.slice(-3), 10) || 0) + 1;
|
||||
const result = `${prefix}${String(seq).padStart(3, "0")}`;
|
||||
const exists = await this.visitModel.findOne({ where: { visitNo: result } });
|
||||
if (exists) {
|
||||
if (retry < 1) return this.generateVisitNo(retry + 1);
|
||||
throw new Error("回访单号生成冲突,请重试");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async list(query: any, ctx: Context): Promise<any> {
|
||||
const page = Number(query.page) || 1;
|
||||
const pageSize = Number(query.pageSize) || 10;
|
||||
const userId = ctx.state.user.userId;
|
||||
const where: any = {};
|
||||
|
||||
if (query.keyword) {
|
||||
where[Op.or] = [
|
||||
{ visitor: { [Op.like]: `%${query.keyword}%` } },
|
||||
{ contactPerson: { [Op.like]: `%${query.keyword}%` } },
|
||||
{ contactPhone: { [Op.like]: `%${query.keyword}%` } },
|
||||
{ customerOpinion: { [Op.like]: `%${query.keyword}%` } }
|
||||
];
|
||||
}
|
||||
if (query.customerId !== undefined) where.customerId = query.customerId;
|
||||
|
||||
if (!(await this.isAdmin(userId))) {
|
||||
const myCustomers = await this.customerModel.findAll({ where: { salesmanId: userId }, attributes: ["id"] });
|
||||
const ids = myCustomers.map((c) => c.id);
|
||||
if (ids.length === 0) return { code: 200, message: "获取客户回访列表成功", data: { list: [], total: 0, page: Number(page), pageSize: Number(pageSize) } };
|
||||
if (where.customerId !== undefined) {
|
||||
if (!ids.includes(where.customerId)) return { code: 200, message: "获取客户回访列表成功", data: { list: [], total: 0, page: Number(page), pageSize: Number(pageSize) } };
|
||||
} else {
|
||||
where.customerId = { [Op.in]: ids };
|
||||
}
|
||||
}
|
||||
|
||||
const { count, rows } = await this.visitModel.findAndCountAll({ where, limit: pageSize, offset: (page - 1) * pageSize, order: [["created_at", "DESC"]] });
|
||||
return { code: 200, message: "获取客户回访列表成功", data: { list: rows.map((r) => r.toJSON()), total: count, page: Number(page), pageSize: Number(pageSize) } };
|
||||
}
|
||||
|
||||
async detail(id: number, ctx: Context): Promise<any> {
|
||||
const record = await this.visitModel.findByPk(id);
|
||||
if (!record) return { code: 404, message: "客户回访不存在", data: null };
|
||||
if (!(await this.isCustomerOwner(ctx.state.user.userId, record.customerId))) return { code: 403, message: "无权限访问该回访记录", data: null };
|
||||
return { code: 200, message: "获取客户回访详情成功", data: record.toJSON() };
|
||||
}
|
||||
|
||||
async create(data: any, ctx: Context): Promise<any> {
|
||||
if (!(await this.isCustomerOwner(ctx.state.user.userId, data.customerId))) return { code: 403, message: "无权限为该客户创建回访记录", data: null };
|
||||
try {
|
||||
data.visitNo = await this.generateVisitNo();
|
||||
const created = await this.visitModel.create(data);
|
||||
return { code: 200, message: "创建客户回访成功", data: created.toJSON() };
|
||||
} catch (err: any) {
|
||||
if (err instanceof UniqueConstraintError) {
|
||||
data.visitNo = await this.generateVisitNo();
|
||||
const created = await this.visitModel.create(data);
|
||||
return { code: 200, message: "创建客户回访成功", data: created.toJSON() };
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async update(id: number, data: any, ctx: Context): Promise<any> {
|
||||
const record = await this.visitModel.findByPk(id);
|
||||
if (!record) return { code: 404, message: "客户回访不存在", data: null };
|
||||
if (!(await this.isAdmin(ctx.state.user.userId))) {
|
||||
if (!(await this.isCustomerOwner(ctx.state.user.userId, record.customerId))) return { code: 403, message: "无权限修改该回访记录", data: null };
|
||||
delete data.customerId;
|
||||
}
|
||||
delete data.visitNo;
|
||||
await record.update(data);
|
||||
return { code: 200, message: "更新客户回访成功", data: record.toJSON() };
|
||||
}
|
||||
|
||||
async delete(id: number, ctx: Context): Promise<any> {
|
||||
const record = await this.visitModel.findByPk(id);
|
||||
if (!record) return { code: 404, message: "客户回访不存在", data: null };
|
||||
if (!(await this.isCustomerOwner(ctx.state.user.userId, record.customerId))) return { code: 403, message: "无权限删除该回访记录", data: null };
|
||||
await record.destroy();
|
||||
return { code: 200, message: "删除客户回访成功", data: null };
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user