feat: 新增客户拜访、建筑、房间及项目全流程业务模块

本次提交新增了客户拜访、客户建筑、客户房间的完整业务模块,同时补充了项目勘察、项目方案、合同管理、项目验收、回访记录的全项目流程相关功能,包括:
1.  新增对应业务的实体类、DTO、控制器、服务层代码
2.  完善RBAC权限映射配置
3.  添加前端API、路由及页面菜单配置
4.  更新数据库种子数据补充菜单和权限绑定
This commit is contained in:
andyesfly committed 2026-08-26 14:08:08 +08:00
1 parent f61ade107b
commit 219091f864
53 files changed
+5990 -6

No files matched your search

+65
View File
@@ -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}`),
};
+81
View File
@@ -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}`),
};
+58
View File
@@ -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}`),
};
+65
View File
@@ -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}`),
};
+66
View File
@@ -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}`),
};
+76
View File
@@ -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}`),
};
+63
View File
@@ -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}`),
};
+64
View File
@@ -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 -1
View File
@@ -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>
+432
View File
@@ -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>
+423
View File
@@ -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>
+448
View File
@@ -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>
+477
View File
@@ -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>
+380
View File
@@ -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>
+361
View File
@@ -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
View File
@@ -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); }
}
+28
View File
@@ -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;
}
+40
View File
@@ -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;
}
+24
View File
@@ -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;
}
+29
View File
@@ -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;
}
+29
View File
@@ -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;
}
+39
View File
@@ -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[];
}
+27
View File
@@ -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;
}
+28
View File
@@ -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[];
}
+51
View File
@@ -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;
}
+69
View File
@@ -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;
}
+53
View File
@@ -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;
}
+47
View File
@@ -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;
}
+46
View File
@@ -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;
}
+46
View File
@@ -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;
}
+8
View File
@@ -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',
};
// 超级管理员角色编码,拥有全部权限
+121
View File
@@ -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 };
}
}
+151
View File
@@ -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()) };
}
}
+102
View File
@@ -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 };
}
}
+154
View File
@@ -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()) };
}
}
+122
View File
@@ -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 };
}
}
+126
View File
@@ -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 };
}
}