feat: 新增客户拜访、建筑、房间及项目全流程业务模块
本次提交新增了客户拜访、客户建筑、客户房间的完整业务模块,同时补充了项目勘察、项目方案、合同管理、项目验收、回访记录的全项目流程相关功能,包括: 1. 新增对应业务的实体类、DTO、控制器、服务层代码 2. 完善RBAC权限映射配置 3. 添加前端API、路由及页面菜单配置 4. 更新数据库种子数据补充菜单和权限绑定
This commit is contained in:
1 parent
f61ade107b
commit
219091f864
53 files changed
+5990
-6
No files matched your search
@@ -0,0 +1,368 @@
|
||||
<template>
|
||||
<div class="page-management">
|
||||
<a-card title="客户建筑管理">
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="handleAdd">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新增建筑
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<div class="search-bar">
|
||||
<a-input
|
||||
v-model:value="searchForm.keyword"
|
||||
placeholder="建筑名称/用途"
|
||||
allow-clear
|
||||
style="width: 220px"
|
||||
@pressEnter="handleSearch"
|
||||
>
|
||||
<template #prefix><SearchOutlined /></template>
|
||||
</a-input>
|
||||
<a-select
|
||||
v-model:value="searchForm.customerId"
|
||||
placeholder="所属客户"
|
||||
allow-clear
|
||||
style="width: 200px"
|
||||
:options="customerOptions"
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
/>
|
||||
<a-button type="primary" @click="handleSearch">
|
||||
<template #icon><SearchOutlined /></template>
|
||||
查询
|
||||
</a-button>
|
||||
<a-button @click="handleReset">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
重置
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<div class="table-wrapper" ref="wrapperRef">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="dataSource"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
row-key="id"
|
||||
:scroll="{ x: 'max-content', y: tableHeight }"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'customerName'">
|
||||
{{ getCustomerName(record.customerId) }}
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleEdit(record)">
|
||||
<template #icon><EditOutlined /></template>
|
||||
编辑
|
||||
</a-button>
|
||||
<a-popconfirm title="确定要删除该建筑吗?" ok-text="确定" cancel-text="取消" @confirm="handleDelete(record.id)">
|
||||
<a-button type="link" size="small" danger>
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template #emptyText>
|
||||
<a-empty description="暂无建筑数据" />
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
:title="modalTitle"
|
||||
:confirm-loading="modalLoading"
|
||||
width="620px"
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 16 }"
|
||||
>
|
||||
<a-form-item label="所属客户" name="customerId">
|
||||
<a-select
|
||||
v-model:value="formData.customerId"
|
||||
placeholder="请选择客户"
|
||||
:options="customerOptions"
|
||||
:disabled="!isAdmin && isEdit"
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="建筑名称" name="buildingName">
|
||||
<a-input v-model:value="formData.buildingName" placeholder="请输入建筑名称" maxlength="128" />
|
||||
</a-form-item>
|
||||
<a-form-item label="建筑用途" name="purpose">
|
||||
<a-input v-model:value="formData.purpose" placeholder="请输入建筑用途" maxlength="128" />
|
||||
</a-form-item>
|
||||
<a-form-item label="楼层数" name="floorCount">
|
||||
<a-input-number v-model:value="formData.floorCount" :min="0" style="width: 100%" placeholder="请输入楼层数" />
|
||||
</a-form-item>
|
||||
<a-form-item label="基本信息" name="basicInfo">
|
||||
<a-textarea v-model:value="formData.basicInfo" :rows="3" placeholder="请输入建筑基本信息" maxlength="2000" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { SearchOutlined, ReloadOutlined, PlusOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons-vue';
|
||||
import { customerBuildingApi, type CustomerBuildingInfo, type CustomerBuildingQuery } from '@/api/customerBuilding';
|
||||
import { customerApi } from '@/api/customer';
|
||||
import { useTableScroll } from '@/composables/useTableScroll';
|
||||
import { usePermissionStore } from '@/stores/permission';
|
||||
|
||||
const { wrapperRef, tableHeight } = useTableScroll();
|
||||
const permissionStore = usePermissionStore();
|
||||
const isAdmin = computed(() => permissionStore.hasRole('ADMIN'));
|
||||
|
||||
const loading = ref(false);
|
||||
const dataSource = ref<CustomerBuildingInfo[]>([]);
|
||||
const total = ref(0);
|
||||
const searchForm = reactive<CustomerBuildingQuery>({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
keyword: '',
|
||||
customerId: undefined as any,
|
||||
});
|
||||
const modalVisible = ref(false);
|
||||
const modalTitle = ref('');
|
||||
const modalLoading = ref(false);
|
||||
const formRef = ref();
|
||||
const isEdit = ref(false);
|
||||
const editId = ref<number | null>(null);
|
||||
const customerOptions = ref<{ label: string; value: number }[]>([]);
|
||||
|
||||
const formData = reactive({
|
||||
customerId: undefined as number | undefined,
|
||||
buildingName: '',
|
||||
purpose: '',
|
||||
floorCount: undefined as number | undefined,
|
||||
basicInfo: '',
|
||||
});
|
||||
|
||||
const formRules = {
|
||||
customerId: [{ required: true, message: '请选择所属客户', trigger: 'change' }],
|
||||
buildingName: [{ required: true, message: '请输入建筑名称', trigger: 'blur' }],
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{ title: '所属客户', key: 'customerName', ellipsis: true },
|
||||
{ title: '建筑名称', dataIndex: 'buildingName', key: 'buildingName', ellipsis: true },
|
||||
{ title: '建筑用途', dataIndex: 'purpose', key: 'purpose', ellipsis: true },
|
||||
{ title: '楼层数', dataIndex: 'floorCount', key: 'floorCount', align: 'center' as const, width: 90 },
|
||||
{ title: '基本信息', dataIndex: 'basicInfo', key: 'basicInfo', ellipsis: true },
|
||||
{ title: '操作', key: 'action', width: 140, fixed: 'right' as const },
|
||||
];
|
||||
|
||||
const pagination = computed(() => ({
|
||||
current: searchForm.page,
|
||||
pageSize: searchForm.pageSize,
|
||||
total: total.value,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (t: number) => `共 ${t} 条数据`,
|
||||
}));
|
||||
|
||||
onMounted(() => {
|
||||
fetchList();
|
||||
fetchCustomerOptions();
|
||||
});
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res: any = await customerBuildingApi.getList(searchForm);
|
||||
if (res.code === 200) {
|
||||
dataSource.value = res.data.list;
|
||||
total.value = res.data.total;
|
||||
}
|
||||
} catch {
|
||||
message.error('获取建筑列表失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchCustomerOptions() {
|
||||
try {
|
||||
const res: any = await customerApi.getCustomerOptions();
|
||||
if (res.code === 200) {
|
||||
customerOptions.value = res.data.map((item: any) => ({ label: item.customerName, value: item.id }));
|
||||
}
|
||||
} catch {
|
||||
console.error('获取客户选项失败');
|
||||
}
|
||||
}
|
||||
|
||||
function getCustomerName(customerId: number): string {
|
||||
const found = customerOptions.value.find((c) => c.value === customerId);
|
||||
return found ? found.label : `#${customerId}`;
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
searchForm.page = 1;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.keyword = '';
|
||||
searchForm.customerId = undefined as any;
|
||||
searchForm.page = 1;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleTableChange(pag: any) {
|
||||
searchForm.page = pag.current;
|
||||
searchForm.pageSize = pag.pageSize;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
isEdit.value = false;
|
||||
editId.value = null;
|
||||
modalTitle.value = '新增建筑';
|
||||
resetForm();
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function handleEdit(record: CustomerBuildingInfo) {
|
||||
isEdit.value = true;
|
||||
editId.value = record.id;
|
||||
modalTitle.value = '编辑建筑';
|
||||
resetForm();
|
||||
formData.customerId = record.customerId;
|
||||
formData.buildingName = record.buildingName;
|
||||
formData.purpose = record.purpose;
|
||||
formData.floorCount = record.floorCount;
|
||||
formData.basicInfo = record.basicInfo;
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
formData.customerId = undefined;
|
||||
formData.buildingName = '';
|
||||
formData.purpose = '';
|
||||
formData.floorCount = undefined;
|
||||
formData.basicInfo = '';
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
modalLoading.value = true;
|
||||
try {
|
||||
const submitData: any = {
|
||||
customerId: formData.customerId,
|
||||
buildingName: formData.buildingName,
|
||||
purpose: formData.purpose,
|
||||
floorCount: formData.floorCount,
|
||||
basicInfo: formData.basicInfo,
|
||||
};
|
||||
|
||||
let res: any;
|
||||
if (isEdit.value && editId.value) {
|
||||
res = await customerBuildingApi.update(editId.value, submitData);
|
||||
} else {
|
||||
res = await customerBuildingApi.create(submitData);
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
message.success(isEdit.value ? '更新成功' : '创建成功');
|
||||
modalVisible.value = false;
|
||||
fetchList();
|
||||
} else {
|
||||
message.error(res.message || '操作失败');
|
||||
}
|
||||
} catch {
|
||||
message.error('操作失败');
|
||||
} finally {
|
||||
modalLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
modalVisible.value = false;
|
||||
resetForm();
|
||||
}
|
||||
|
||||
async function handleDelete(id: number) {
|
||||
try {
|
||||
const res: any = await customerBuildingApi.delete(id);
|
||||
if (res.code === 200) {
|
||||
message.success('删除成功');
|
||||
fetchList();
|
||||
} else {
|
||||
message.error(res.message || '删除失败');
|
||||
}
|
||||
} catch {
|
||||
message.error('删除失败');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-management {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
:deep(.ant-card) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
:deep(.ant-card-body) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
.search-bar {
|
||||
flex-shrink: 0;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
.table-wrapper {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
:deep(.ant-table) {
|
||||
font-size: 14px;
|
||||
height: 100%;
|
||||
}
|
||||
:deep(.ant-btn-link) {
|
||||
padding: 0 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,432 @@
|
||||
<template>
|
||||
<div class="page-management">
|
||||
<a-card title="客户房间管理">
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="handleAdd">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新增房间
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<div class="search-bar">
|
||||
<a-input
|
||||
v-model:value="searchForm.keyword"
|
||||
placeholder="房间名称/编码"
|
||||
allow-clear
|
||||
style="width: 220px"
|
||||
@pressEnter="handleSearch"
|
||||
>
|
||||
<template #prefix><SearchOutlined /></template>
|
||||
</a-input>
|
||||
<a-select
|
||||
v-model:value="searchForm.customerId"
|
||||
placeholder="所属客户"
|
||||
allow-clear
|
||||
style="width: 200px"
|
||||
:options="customerOptions"
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
@change="handleCustomerSearchChange"
|
||||
/>
|
||||
<a-button type="primary" @click="handleSearch">
|
||||
<template #icon><SearchOutlined /></template>
|
||||
查询
|
||||
</a-button>
|
||||
<a-button @click="handleReset">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
重置
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<div class="table-wrapper" ref="wrapperRef">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="dataSource"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
row-key="id"
|
||||
:scroll="{ x: 'max-content', y: tableHeight }"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'customerName'">
|
||||
{{ getCustomerName(record.customerId) }}
|
||||
</template>
|
||||
<template v-if="column.key === 'buildingName'">
|
||||
{{ getBuildingName(record.buildingId) }}
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleEdit(record)">
|
||||
<template #icon><EditOutlined /></template>
|
||||
编辑
|
||||
</a-button>
|
||||
<a-popconfirm title="确定要删除该房间吗?" ok-text="确定" cancel-text="取消" @confirm="handleDelete(record.id)">
|
||||
<a-button type="link" size="small" danger>
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template #emptyText>
|
||||
<a-empty description="暂无房间数据" />
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
:title="modalTitle"
|
||||
:confirm-loading="modalLoading"
|
||||
width="620px"
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 16 }"
|
||||
>
|
||||
<a-form-item label="所属客户" name="customerId">
|
||||
<a-select
|
||||
v-model:value="formData.customerId"
|
||||
placeholder="请选择客户"
|
||||
:options="customerOptions"
|
||||
:disabled="!isAdmin && isEdit"
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
@change="handleCustomerFormChange"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="所属建筑" name="buildingId">
|
||||
<a-select
|
||||
v-model:value="formData.buildingId"
|
||||
placeholder="请选择建筑"
|
||||
:options="buildingOptions"
|
||||
:not-found-content="null"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="房间编码" name="roomCode">
|
||||
<a-input v-model:value="formData.roomCode" placeholder="请输入房间编码" maxlength="64" />
|
||||
</a-form-item>
|
||||
<a-form-item label="房间名称" name="roomName">
|
||||
<a-input v-model:value="formData.roomName" placeholder="请输入房间名称" maxlength="128" />
|
||||
</a-form-item>
|
||||
<a-form-item label="所在楼层" name="floorNo">
|
||||
<a-input v-model:value="formData.floorNo" placeholder="请输入所在楼层" maxlength="16" />
|
||||
</a-form-item>
|
||||
<a-form-item label="房间用途" name="roomPurpose">
|
||||
<a-input v-model:value="formData.roomPurpose" placeholder="请输入房间用途" maxlength="128" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { SearchOutlined, ReloadOutlined, PlusOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons-vue';
|
||||
import { customerRoomApi, type CustomerRoomInfo, type CustomerRoomQuery } from '@/api/customerRoom';
|
||||
import { customerBuildingApi } from '@/api/customerBuilding';
|
||||
import { customerApi } from '@/api/customer';
|
||||
import { useTableScroll } from '@/composables/useTableScroll';
|
||||
import { usePermissionStore } from '@/stores/permission';
|
||||
|
||||
const { wrapperRef, tableHeight } = useTableScroll();
|
||||
const permissionStore = usePermissionStore();
|
||||
const isAdmin = computed(() => permissionStore.hasRole('ADMIN'));
|
||||
|
||||
const loading = ref(false);
|
||||
const dataSource = ref<CustomerRoomInfo[]>([]);
|
||||
const total = ref(0);
|
||||
const searchForm = reactive<CustomerRoomQuery>({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
keyword: '',
|
||||
customerId: undefined as any,
|
||||
});
|
||||
const modalVisible = ref(false);
|
||||
const modalTitle = ref('');
|
||||
const modalLoading = ref(false);
|
||||
const formRef = ref();
|
||||
const isEdit = ref(false);
|
||||
const editId = ref<number | null>(null);
|
||||
const customerOptions = ref<{ label: string; value: number }[]>([]);
|
||||
const buildingOptions = ref<{ label: string; value: number }[]>([]);
|
||||
const allBuildings = ref<{ label: string; value: number; customerId: number }[]>([]);
|
||||
|
||||
const formData = reactive({
|
||||
customerId: undefined as number | undefined,
|
||||
buildingId: undefined as number | undefined,
|
||||
roomCode: '',
|
||||
roomName: '',
|
||||
floorNo: '',
|
||||
roomPurpose: '',
|
||||
});
|
||||
|
||||
const formRules = {
|
||||
customerId: [{ required: true, message: '请选择所属客户', trigger: 'change' }],
|
||||
buildingId: [{ required: true, message: '请选择所属建筑', trigger: 'change' }],
|
||||
roomCode: [{ required: true, message: '请输入房间编码', trigger: 'blur' }],
|
||||
roomName: [{ required: true, message: '请输入房间名称', trigger: 'blur' }],
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{ title: '房间编码', dataIndex: 'roomCode', key: 'roomCode', ellipsis: true, width: 130 },
|
||||
{ title: '房间名称', dataIndex: 'roomName', key: 'roomName', ellipsis: true },
|
||||
{ title: '所属客户', key: 'customerName', ellipsis: true },
|
||||
{ title: '所属建筑', key: 'buildingName', ellipsis: true },
|
||||
{ title: '所在楼层', dataIndex: 'floorNo', key: 'floorNo', align: 'center' as const, width: 90 },
|
||||
{ title: '房间用途', dataIndex: 'roomPurpose', key: 'roomPurpose', ellipsis: true },
|
||||
{ title: '操作', key: 'action', width: 140, fixed: 'right' as const },
|
||||
];
|
||||
|
||||
const pagination = computed(() => ({
|
||||
current: searchForm.page,
|
||||
pageSize: searchForm.pageSize,
|
||||
total: total.value,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (t: number) => `共 ${t} 条数据`,
|
||||
}));
|
||||
|
||||
onMounted(() => {
|
||||
fetchList();
|
||||
fetchCustomerOptions();
|
||||
fetchAllBuildings();
|
||||
});
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res: any = await customerRoomApi.getList(searchForm);
|
||||
if (res.code === 200) {
|
||||
dataSource.value = res.data.list;
|
||||
total.value = res.data.total;
|
||||
}
|
||||
} catch {
|
||||
message.error('获取房间列表失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchCustomerOptions() {
|
||||
try {
|
||||
const res: any = await customerApi.getCustomerOptions();
|
||||
if (res.code === 200) {
|
||||
customerOptions.value = res.data.map((item: any) => ({ label: item.customerName, value: item.id }));
|
||||
}
|
||||
} catch {
|
||||
console.error('获取客户选项失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchAllBuildings() {
|
||||
try {
|
||||
const res: any = await customerBuildingApi.getList({ page: 1, pageSize: 999 });
|
||||
if (res.code === 200) {
|
||||
allBuildings.value = res.data.list.map((item: any) => ({
|
||||
label: item.buildingName,
|
||||
value: item.id,
|
||||
customerId: item.customerId,
|
||||
}));
|
||||
}
|
||||
} catch {
|
||||
console.error('获取建筑列表失败');
|
||||
}
|
||||
}
|
||||
|
||||
function getCustomerName(customerId: number): string {
|
||||
const found = customerOptions.value.find((c) => c.value === customerId);
|
||||
return found ? found.label : `#${customerId}`;
|
||||
}
|
||||
|
||||
function getBuildingName(buildingId: number): string {
|
||||
const found = allBuildings.value.find((b) => b.value === buildingId);
|
||||
return found ? found.label : `#${buildingId}`;
|
||||
}
|
||||
|
||||
function filterBuildings(customerId?: number) {
|
||||
if (customerId) {
|
||||
buildingOptions.value = allBuildings.value.filter((b) => b.customerId === customerId);
|
||||
} else {
|
||||
buildingOptions.value = allBuildings.value;
|
||||
}
|
||||
}
|
||||
|
||||
function handleCustomerFormChange(customerId?: number) {
|
||||
formData.buildingId = undefined;
|
||||
filterBuildings(customerId);
|
||||
}
|
||||
|
||||
function handleCustomerSearchChange(customerId?: number) {
|
||||
searchForm.customerId = customerId;
|
||||
handleSearch();
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
searchForm.page = 1;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.keyword = '';
|
||||
searchForm.customerId = undefined as any;
|
||||
searchForm.page = 1;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleTableChange(pag: any) {
|
||||
searchForm.page = pag.current;
|
||||
searchForm.pageSize = pag.pageSize;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
isEdit.value = false;
|
||||
editId.value = null;
|
||||
modalTitle.value = '新增房间';
|
||||
resetForm();
|
||||
filterBuildings(undefined);
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function handleEdit(record: CustomerRoomInfo) {
|
||||
isEdit.value = true;
|
||||
editId.value = record.id;
|
||||
modalTitle.value = '编辑房间';
|
||||
resetForm();
|
||||
formData.customerId = record.customerId;
|
||||
filterBuildings(record.customerId);
|
||||
formData.buildingId = record.buildingId;
|
||||
formData.roomCode = record.roomCode;
|
||||
formData.roomName = record.roomName;
|
||||
formData.floorNo = record.floorNo;
|
||||
formData.roomPurpose = record.roomPurpose;
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
formData.customerId = undefined;
|
||||
formData.buildingId = undefined;
|
||||
formData.roomCode = '';
|
||||
formData.roomName = '';
|
||||
formData.floorNo = '';
|
||||
formData.roomPurpose = '';
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
modalLoading.value = true;
|
||||
try {
|
||||
const submitData: any = {
|
||||
customerId: formData.customerId,
|
||||
buildingId: formData.buildingId,
|
||||
roomCode: formData.roomCode,
|
||||
roomName: formData.roomName,
|
||||
floorNo: formData.floorNo,
|
||||
roomPurpose: formData.roomPurpose,
|
||||
};
|
||||
|
||||
let res: any;
|
||||
if (isEdit.value && editId.value) {
|
||||
res = await customerRoomApi.update(editId.value, submitData);
|
||||
} else {
|
||||
res = await customerRoomApi.create(submitData);
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
message.success(isEdit.value ? '更新成功' : '创建成功');
|
||||
modalVisible.value = false;
|
||||
fetchList();
|
||||
} else {
|
||||
message.error(res.message || '操作失败');
|
||||
}
|
||||
} catch {
|
||||
message.error('操作失败');
|
||||
} finally {
|
||||
modalLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
modalVisible.value = false;
|
||||
resetForm();
|
||||
}
|
||||
|
||||
async function handleDelete(id: number) {
|
||||
try {
|
||||
const res: any = await customerRoomApi.delete(id);
|
||||
if (res.code === 200) {
|
||||
message.success('删除成功');
|
||||
fetchList();
|
||||
} else {
|
||||
message.error(res.message || '删除失败');
|
||||
}
|
||||
} catch {
|
||||
message.error('删除失败');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-management {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
:deep(.ant-card) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
:deep(.ant-card-body) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
.search-bar {
|
||||
flex-shrink: 0;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
.table-wrapper {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
:deep(.ant-table) {
|
||||
font-size: 14px;
|
||||
height: 100%;
|
||||
}
|
||||
:deep(.ant-btn-link) {
|
||||
padding: 0 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,423 @@
|
||||
<template>
|
||||
<div class="page-management">
|
||||
<a-card title="客户拜访管理">
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="handleAdd">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新增拜访
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<div class="search-bar">
|
||||
<a-input
|
||||
v-model:value="searchForm.keyword"
|
||||
placeholder="拜访人/电话"
|
||||
allow-clear
|
||||
style="width: 220px"
|
||||
@pressEnter="handleSearch"
|
||||
>
|
||||
<template #prefix><SearchOutlined /></template>
|
||||
</a-input>
|
||||
<a-select
|
||||
v-if="isAdmin"
|
||||
v-model:value="searchForm.salesmanId"
|
||||
placeholder="负责业务员"
|
||||
allow-clear
|
||||
style="width: 160px"
|
||||
:options="salesmanOptions"
|
||||
/>
|
||||
<a-button type="primary" @click="handleSearch">
|
||||
<template #icon><SearchOutlined /></template>
|
||||
查询
|
||||
</a-button>
|
||||
<a-button @click="handleReset">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
重置
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<div class="table-wrapper" ref="wrapperRef">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="dataSource"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
row-key="id"
|
||||
:scroll="{ x: 'max-content', y: tableHeight }"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'customerName'">
|
||||
{{ getCustomerName(record.customerId) }}
|
||||
</template>
|
||||
<template v-if="column.key === 'willPlan'">
|
||||
<a-tag :color="record.willPlan === 1 ? 'orange' : 'default'">
|
||||
{{ record.willPlan === 1 ? '有意向方案' : '暂无' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'createdAt'">
|
||||
{{ formatDateTime(record.createdAt) }}
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleEdit(record)">
|
||||
<template #icon><EditOutlined /></template>
|
||||
编辑
|
||||
</a-button>
|
||||
<a-popconfirm title="确定要删除该拜访记录吗?" ok-text="确定" cancel-text="取消" @confirm="handleDelete(record.id)">
|
||||
<a-button type="link" size="small" danger>
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template #emptyText>
|
||||
<a-empty description="暂无拜访数据" />
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
:title="modalTitle"
|
||||
:confirm-loading="modalLoading"
|
||||
width="680px"
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 16 }"
|
||||
>
|
||||
<a-form-item label="所属客户" name="customerId">
|
||||
<a-select
|
||||
v-model:value="formData.customerId"
|
||||
placeholder="请选择客户"
|
||||
:options="customerOptions"
|
||||
:disabled="!isAdmin && isEdit"
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="拜访人" name="visitPerson">
|
||||
<a-input v-model:value="formData.visitPerson" placeholder="请输入拜访人" maxlength="64" />
|
||||
</a-form-item>
|
||||
<a-form-item label="联系电话" name="visitPhone">
|
||||
<a-input v-model:value="formData.visitPhone" placeholder="请输入联系电话" />
|
||||
</a-form-item>
|
||||
<a-form-item label="拜访时间" name="visitTime">
|
||||
<a-date-picker v-model:value="formData.visitTime" style="width: 100%" placeholder="请选择拜访时间" />
|
||||
</a-form-item>
|
||||
<a-form-item label="是否有意向方案" name="willPlan">
|
||||
<a-radio-group v-model:value="formData.willPlan">
|
||||
<a-radio :value="1">有</a-radio>
|
||||
<a-radio :value="0">无</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="拜访内容" name="visitContent">
|
||||
<a-textarea v-model:value="formData.visitContent" :rows="3" placeholder="请输入拜访内容" maxlength="2000" />
|
||||
</a-form-item>
|
||||
<a-form-item v-if="isAdmin" label="负责业务员" name="salesmanId">
|
||||
<a-select
|
||||
v-model:value="formData.salesmanId"
|
||||
placeholder="请选择负责业务员"
|
||||
:options="salesmanOptions"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { SearchOutlined, ReloadOutlined, PlusOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons-vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { customerVisitApi, type CustomerVisitInfo, type CustomerVisitQuery } from '@/api/customerVisit';
|
||||
import { customerApi } from '@/api/customer';
|
||||
import { userApi } from '@/api/user';
|
||||
import { useTableScroll } from '@/composables/useTableScroll';
|
||||
import { usePermissionStore } from '@/stores/permission';
|
||||
|
||||
const { wrapperRef, tableHeight } = useTableScroll();
|
||||
const permissionStore = usePermissionStore();
|
||||
const isAdmin = computed(() => permissionStore.hasRole('ADMIN'));
|
||||
|
||||
const loading = ref(false);
|
||||
const dataSource = ref<CustomerVisitInfo[]>([]);
|
||||
const total = ref(0);
|
||||
const searchForm = reactive<CustomerVisitQuery>({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
keyword: '',
|
||||
salesmanId: undefined as any,
|
||||
});
|
||||
const modalVisible = ref(false);
|
||||
const modalTitle = ref('');
|
||||
const modalLoading = ref(false);
|
||||
const formRef = ref();
|
||||
const isEdit = ref(false);
|
||||
const editId = ref<number | null>(null);
|
||||
const customerOptions = ref<{ label: string; value: number }[]>([]);
|
||||
const salesmanOptions = ref<{ label: string; value: number }[]>([]);
|
||||
|
||||
const formData = reactive({
|
||||
customerId: undefined as number | undefined,
|
||||
visitPerson: '',
|
||||
visitPhone: '',
|
||||
visitTime: undefined as any,
|
||||
visitContent: '',
|
||||
willPlan: 0,
|
||||
salesmanId: undefined as number | undefined,
|
||||
});
|
||||
|
||||
const formRules = {
|
||||
customerId: [{ required: true, message: '请选择所属客户', trigger: 'change' }],
|
||||
visitPerson: [{ required: true, message: '请输入拜访人', trigger: 'blur' }],
|
||||
visitPhone: [{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' }],
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{ title: '拜访单号', dataIndex: 'visitNo', key: 'visitNo', ellipsis: true, width: 160 },
|
||||
{ title: '所属客户', key: 'customerName', ellipsis: true },
|
||||
{ title: '拜访人', dataIndex: 'visitPerson', key: 'visitPerson', ellipsis: true },
|
||||
{ title: '联系电话', dataIndex: 'visitPhone', key: 'visitPhone', ellipsis: true },
|
||||
{ title: '拜访时间', dataIndex: 'visitTime', key: 'visitTime' },
|
||||
{ title: '拜访内容', dataIndex: 'visitContent', key: 'visitContent', ellipsis: true },
|
||||
{ title: '意向方案', key: 'willPlan', align: 'center' as const, width: 100 },
|
||||
{ title: '创建时间', dataIndex: 'createdAt', key: 'createdAt', ellipsis: true },
|
||||
{ title: '操作', key: 'action', width: 140, fixed: 'right' as const },
|
||||
];
|
||||
|
||||
const pagination = computed(() => ({
|
||||
current: searchForm.page,
|
||||
pageSize: searchForm.pageSize,
|
||||
total: total.value,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (t: number) => `共 ${t} 条数据`,
|
||||
}));
|
||||
|
||||
onMounted(() => {
|
||||
fetchList();
|
||||
fetchCustomerOptions();
|
||||
fetchSalesmanList();
|
||||
});
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res: any = await customerVisitApi.getList(searchForm);
|
||||
if (res.code === 200) {
|
||||
dataSource.value = res.data.list;
|
||||
total.value = res.data.total;
|
||||
}
|
||||
} catch {
|
||||
message.error('获取拜访列表失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchCustomerOptions() {
|
||||
try {
|
||||
const res: any = await customerApi.getCustomerOptions();
|
||||
if (res.code === 200) {
|
||||
customerOptions.value = res.data.map((item: any) => ({ label: item.customerName, value: item.id }));
|
||||
}
|
||||
} catch {
|
||||
console.error('获取客户选项失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchSalesmanList() {
|
||||
try {
|
||||
const res: any = await userApi.getUserList({ page: 1, pageSize: 999 });
|
||||
if (res.code === 200) {
|
||||
salesmanOptions.value = res.data.list.map((item: any) => ({ label: item.realName, value: item.id }));
|
||||
}
|
||||
} catch {
|
||||
console.error('获取业务员列表失败');
|
||||
}
|
||||
}
|
||||
|
||||
function getCustomerName(customerId: number): string {
|
||||
const found = customerOptions.value.find((c) => c.value === customerId);
|
||||
return found ? found.label : `#${customerId}`;
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
searchForm.page = 1;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.keyword = '';
|
||||
searchForm.salesmanId = undefined as any;
|
||||
searchForm.page = 1;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleTableChange(pag: any) {
|
||||
searchForm.page = pag.current;
|
||||
searchForm.pageSize = pag.pageSize;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
isEdit.value = false;
|
||||
editId.value = null;
|
||||
modalTitle.value = '新增拜访';
|
||||
resetForm();
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function handleEdit(record: CustomerVisitInfo) {
|
||||
isEdit.value = true;
|
||||
editId.value = record.id;
|
||||
modalTitle.value = '编辑拜访';
|
||||
resetForm();
|
||||
formData.customerId = record.customerId;
|
||||
formData.visitPerson = record.visitPerson;
|
||||
formData.visitPhone = record.visitPhone;
|
||||
formData.visitTime = record.visitTime ? dayjs(record.visitTime) : undefined;
|
||||
formData.visitContent = record.visitContent;
|
||||
formData.willPlan = record.willPlan ?? 0;
|
||||
formData.salesmanId = record.salesmanId;
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
formData.customerId = undefined;
|
||||
formData.visitPerson = '';
|
||||
formData.visitPhone = '';
|
||||
formData.visitTime = undefined;
|
||||
formData.visitContent = '';
|
||||
formData.willPlan = 0;
|
||||
formData.salesmanId = undefined;
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
modalLoading.value = true;
|
||||
try {
|
||||
const submitData: any = {
|
||||
customerId: formData.customerId,
|
||||
visitPerson: formData.visitPerson,
|
||||
visitPhone: formData.visitPhone,
|
||||
visitTime: formData.visitTime ? formData.visitTime.format('YYYY-MM-DD') : undefined,
|
||||
visitContent: formData.visitContent,
|
||||
willPlan: formData.willPlan,
|
||||
};
|
||||
if (isAdmin.value && formData.salesmanId !== undefined) {
|
||||
submitData.salesmanId = formData.salesmanId;
|
||||
}
|
||||
|
||||
let res: any;
|
||||
if (isEdit.value && editId.value) {
|
||||
res = await customerVisitApi.update(editId.value, submitData);
|
||||
} else {
|
||||
res = await customerVisitApi.create(submitData);
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
message.success(isEdit.value ? '更新成功' : '创建成功');
|
||||
modalVisible.value = false;
|
||||
fetchList();
|
||||
} else {
|
||||
message.error(res.message || '操作失败');
|
||||
}
|
||||
} catch {
|
||||
message.error('操作失败');
|
||||
} finally {
|
||||
modalLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
modalVisible.value = false;
|
||||
resetForm();
|
||||
}
|
||||
|
||||
async function handleDelete(id: number) {
|
||||
try {
|
||||
const res: any = await customerVisitApi.delete(id);
|
||||
if (res.code === 200) {
|
||||
message.success('删除成功');
|
||||
fetchList();
|
||||
} else {
|
||||
message.error(res.message || '删除失败');
|
||||
}
|
||||
} catch {
|
||||
message.error('删除失败');
|
||||
}
|
||||
}
|
||||
|
||||
function formatDateTime(dateStr: string) {
|
||||
if (!dateStr) return '-';
|
||||
return dayjs(dateStr).format('YYYY-MM-DD HH:mm:ss');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-management {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
:deep(.ant-card) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
:deep(.ant-card-body) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
.search-bar {
|
||||
flex-shrink: 0;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
.table-wrapper {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
:deep(.ant-table) {
|
||||
font-size: 14px;
|
||||
height: 100%;
|
||||
}
|
||||
:deep(.ant-btn-link) {
|
||||
padding: 0 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,353 @@
|
||||
<template>
|
||||
<div class="page-management">
|
||||
<a-card title="项目验收管理">
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="handleAdd">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新增验收
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<div class="search-bar">
|
||||
<a-input
|
||||
v-model:value="searchForm.keyword"
|
||||
placeholder="验收单号/验收人"
|
||||
allow-clear
|
||||
style="width: 220px"
|
||||
@pressEnter="handleSearch"
|
||||
>
|
||||
<template #prefix><SearchOutlined /></template>
|
||||
</a-input>
|
||||
<a-button type="primary" @click="handleSearch">
|
||||
<template #icon><SearchOutlined /></template>
|
||||
查询
|
||||
</a-button>
|
||||
<a-button @click="handleReset">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
重置
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<div class="table-wrapper" ref="wrapperRef">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="dataSource"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
row-key="id"
|
||||
:scroll="{ x: 'max-content', y: tableHeight }"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'customerName'">
|
||||
{{ getCustomerName(record.customerId) }}
|
||||
</template>
|
||||
<template v-if="column.key === 'acceptDate'">
|
||||
{{ record.acceptDate || '-' }}
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleEdit(record)">
|
||||
<template #icon><EditOutlined /></template>
|
||||
编辑
|
||||
</a-button>
|
||||
<a-popconfirm title="确定要删除该验收记录吗?" ok-text="确定" cancel-text="取消" @confirm="handleDelete(record.id)">
|
||||
<a-button type="link" size="small" danger>
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template #emptyText>
|
||||
<a-empty description="暂无验收数据" />
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
:title="modalTitle"
|
||||
:confirm-loading="modalLoading"
|
||||
width="620px"
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 16 }"
|
||||
>
|
||||
<a-form-item label="所属客户" name="customerId">
|
||||
<a-select
|
||||
v-model:value="formData.customerId"
|
||||
placeholder="请选择客户"
|
||||
:options="customerOptions"
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="验收人" name="acceptor">
|
||||
<a-input v-model:value="formData.acceptor" placeholder="请输入验收人" maxlength="64" />
|
||||
</a-form-item>
|
||||
<a-form-item label="验收时间" name="acceptDate">
|
||||
<a-date-picker v-model:value="formData.acceptDate" style="width: 100%" placeholder="请选择验收时间" />
|
||||
</a-form-item>
|
||||
<a-form-item label="安装人" name="installer">
|
||||
<a-input v-model:value="formData.installer" placeholder="请输入安装人" maxlength="64" />
|
||||
</a-form-item>
|
||||
<a-form-item label="验收意见" name="acceptOpinion">
|
||||
<a-textarea v-model:value="formData.acceptOpinion" :rows="3" placeholder="请输入验收意见" maxlength="2000" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { SearchOutlined, ReloadOutlined, PlusOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons-vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { acceptanceApi, type AcceptanceInfo, type AcceptanceQuery } from '@/api/acceptance';
|
||||
import { customerApi } from '@/api/customer';
|
||||
import { useTableScroll } from '@/composables/useTableScroll';
|
||||
|
||||
const { wrapperRef, tableHeight } = useTableScroll();
|
||||
|
||||
const loading = ref(false);
|
||||
const dataSource = ref<AcceptanceInfo[]>([]);
|
||||
const total = ref(0);
|
||||
const searchForm = reactive<AcceptanceQuery>({ page: 1, pageSize: 10, keyword: '' });
|
||||
const modalVisible = ref(false);
|
||||
const modalTitle = ref('');
|
||||
const modalLoading = ref(false);
|
||||
const formRef = ref();
|
||||
const isEdit = ref(false);
|
||||
const editId = ref<number | null>(null);
|
||||
const customerOptions = ref<{ label: string; value: number }[]>([]);
|
||||
|
||||
const formData = reactive({
|
||||
customerId: undefined as number | undefined,
|
||||
acceptor: '',
|
||||
acceptDate: undefined as any,
|
||||
installer: '',
|
||||
acceptOpinion: '',
|
||||
});
|
||||
|
||||
const formRules = {
|
||||
customerId: [{ required: true, message: '请选择所属客户', trigger: 'change' }],
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{ title: '验收单号', dataIndex: 'acceptNo', key: 'acceptNo', ellipsis: true, width: 170 },
|
||||
{ title: '所属客户', key: 'customerName', ellipsis: true },
|
||||
{ title: '验收人', dataIndex: 'acceptor', key: 'acceptor', ellipsis: true },
|
||||
{ title: '验收时间', key: 'acceptDate', width: 110 },
|
||||
{ title: '安装人', dataIndex: 'installer', key: 'installer', ellipsis: true },
|
||||
{ title: '验收意见', dataIndex: 'acceptOpinion', key: 'acceptOpinion', ellipsis: true },
|
||||
{ title: '操作', key: 'action', width: 140, fixed: 'right' as const },
|
||||
];
|
||||
|
||||
const pagination = computed(() => ({
|
||||
current: searchForm.page,
|
||||
pageSize: searchForm.pageSize,
|
||||
total: total.value,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (t: number) => `共 ${t} 条数据`,
|
||||
}));
|
||||
|
||||
onMounted(() => {
|
||||
fetchList();
|
||||
fetchCustomerOptions();
|
||||
});
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res: any = await acceptanceApi.getList(searchForm);
|
||||
if (res.code === 200) {
|
||||
dataSource.value = res.data.list;
|
||||
total.value = res.data.total;
|
||||
}
|
||||
} catch {
|
||||
message.error('获取验收列表失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchCustomerOptions() {
|
||||
try {
|
||||
const res: any = await customerApi.getCustomerOptions();
|
||||
if (res.code === 200) {
|
||||
customerOptions.value = res.data.map((item: any) => ({ label: item.customerName, value: item.id }));
|
||||
}
|
||||
} catch {
|
||||
console.error('获取客户选项失败');
|
||||
}
|
||||
}
|
||||
|
||||
function getCustomerName(customerId: number): string {
|
||||
const found = customerOptions.value.find((c) => c.value === customerId);
|
||||
return found ? found.label : `#${customerId}`;
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
searchForm.page = 1;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.keyword = '';
|
||||
searchForm.page = 1;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleTableChange(pag: any) {
|
||||
searchForm.page = pag.current;
|
||||
searchForm.pageSize = pag.pageSize;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
isEdit.value = false;
|
||||
editId.value = null;
|
||||
modalTitle.value = '新增验收';
|
||||
resetForm();
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function handleEdit(record: AcceptanceInfo) {
|
||||
isEdit.value = true;
|
||||
editId.value = record.id;
|
||||
modalTitle.value = '编辑验收';
|
||||
resetForm();
|
||||
formData.customerId = record.customerId;
|
||||
formData.acceptor = record.acceptor;
|
||||
formData.acceptDate = record.acceptDate ? dayjs(record.acceptDate) : undefined;
|
||||
formData.installer = record.installer;
|
||||
formData.acceptOpinion = record.acceptOpinion;
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
formData.customerId = undefined;
|
||||
formData.acceptor = '';
|
||||
formData.acceptDate = undefined;
|
||||
formData.installer = '';
|
||||
formData.acceptOpinion = '';
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
modalLoading.value = true;
|
||||
try {
|
||||
const submitData: any = {
|
||||
customerId: formData.customerId,
|
||||
acceptor: formData.acceptor,
|
||||
acceptDate: formData.acceptDate ? formData.acceptDate.format('YYYY-MM-DD') : undefined,
|
||||
installer: formData.installer,
|
||||
acceptOpinion: formData.acceptOpinion,
|
||||
};
|
||||
|
||||
let res: any;
|
||||
if (isEdit.value && editId.value) {
|
||||
res = await acceptanceApi.update(editId.value, submitData);
|
||||
} else {
|
||||
res = await acceptanceApi.create(submitData);
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
message.success(isEdit.value ? '更新成功' : '创建成功');
|
||||
modalVisible.value = false;
|
||||
fetchList();
|
||||
} else {
|
||||
message.error(res.message || '操作失败');
|
||||
}
|
||||
} catch {
|
||||
message.error('操作失败');
|
||||
} finally {
|
||||
modalLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
modalVisible.value = false;
|
||||
resetForm();
|
||||
}
|
||||
|
||||
async function handleDelete(id: number) {
|
||||
try {
|
||||
const res: any = await acceptanceApi.delete(id);
|
||||
if (res.code === 200) {
|
||||
message.success('删除成功');
|
||||
fetchList();
|
||||
} else {
|
||||
message.error(res.message || '删除失败');
|
||||
}
|
||||
} catch {
|
||||
message.error('删除失败');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-management {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
:deep(.ant-card) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
:deep(.ant-card-body) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
.search-bar {
|
||||
flex-shrink: 0;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
.table-wrapper {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
:deep(.ant-table) {
|
||||
font-size: 14px;
|
||||
height: 100%;
|
||||
}
|
||||
:deep(.ant-btn-link) {
|
||||
padding: 0 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,448 @@
|
||||
<template>
|
||||
<div class="page-management">
|
||||
<a-card title="合同管理">
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="handleAdd">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新增合同
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<div class="search-bar">
|
||||
<a-input
|
||||
v-model:value="searchForm.keyword"
|
||||
placeholder="合同号/客户名称"
|
||||
allow-clear
|
||||
style="width: 220px"
|
||||
@pressEnter="handleSearch"
|
||||
>
|
||||
<template #prefix><SearchOutlined /></template>
|
||||
</a-input>
|
||||
<a-select v-model:value="searchForm.status" placeholder="状态" allow-clear style="width: 130px">
|
||||
<a-select-option :value="1">有效</a-select-option>
|
||||
<a-select-option :value="0">终止</a-select-option>
|
||||
</a-select>
|
||||
<a-select
|
||||
v-if="isAdmin"
|
||||
v-model:value="searchForm.salesmanId"
|
||||
placeholder="业务员"
|
||||
allow-clear
|
||||
style="width: 150px"
|
||||
:options="salesmanOptions"
|
||||
/>
|
||||
<a-button type="primary" @click="handleSearch">
|
||||
<template #icon><SearchOutlined /></template>
|
||||
查询
|
||||
</a-button>
|
||||
<a-button @click="handleReset">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
重置
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<div class="table-wrapper" ref="wrapperRef">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="dataSource"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
row-key="id"
|
||||
:scroll="{ x: 'max-content', y: tableHeight }"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'customerName'">
|
||||
{{ getCustomerName(record.customerId) }}
|
||||
</template>
|
||||
<template v-if="column.key === 'contractAmount'">
|
||||
{{ formatMoney(record.contractAmount) }}
|
||||
</template>
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="record.status === 1 ? 'green' : 'default'">
|
||||
{{ record.status === 1 ? '有效' : '终止' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleEdit(record)">
|
||||
<template #icon><EditOutlined /></template>
|
||||
编辑
|
||||
</a-button>
|
||||
<a-popconfirm title="确定要删除该合同吗?" ok-text="确定" cancel-text="取消" @confirm="handleDelete(record.id)">
|
||||
<a-button type="link" size="small" danger>
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template #emptyText>
|
||||
<a-empty description="暂无合同数据" />
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
:title="modalTitle"
|
||||
:confirm-loading="modalLoading"
|
||||
width="680px"
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 16 }"
|
||||
>
|
||||
<a-form-item label="所属客户" name="customerId">
|
||||
<a-select
|
||||
v-model:value="formData.customerId"
|
||||
placeholder="请选择客户"
|
||||
:options="customerOptions"
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="鑫潼合同号" name="xtContractNo">
|
||||
<a-input v-model:value="formData.xtContractNo" placeholder="请输入鑫潼合同号" maxlength="64" />
|
||||
</a-form-item>
|
||||
<a-form-item label="合同金额" name="contractAmount">
|
||||
<a-input-number v-model:value="formData.contractAmount" :min="0" style="width: 100%" placeholder="请输入合同金额" />
|
||||
</a-form-item>
|
||||
<a-form-item label="支付周期" name="payCycle">
|
||||
<a-select v-model:value="formData.payCycle" placeholder="请选择支付周期" :options="payCycleOptions" allow-clear />
|
||||
</a-form-item>
|
||||
<a-form-item label="签订日期" name="signDate">
|
||||
<a-date-picker v-model:value="formData.signDate" style="width: 100%" placeholder="请选择签订日期" />
|
||||
</a-form-item>
|
||||
<a-form-item label="起止日期" name="contractDates">
|
||||
<a-range-picker v-model:value="formData.contractDates" style="width: 100%" />
|
||||
</a-form-item>
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-radio-group v-model:value="formData.status">
|
||||
<a-radio :value="1">有效</a-radio>
|
||||
<a-radio :value="0">终止</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="isAdmin" label="业务员" name="salesmanId">
|
||||
<a-select
|
||||
v-model:value="formData.salesmanId"
|
||||
placeholder="请选择业务员"
|
||||
:options="salesmanOptions"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="合同说明" name="contractDesc">
|
||||
<a-textarea v-model:value="formData.contractDesc" :rows="2" placeholder="请输入合同说明" maxlength="2000" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { SearchOutlined, ReloadOutlined, PlusOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons-vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { contractApi, type ContractInfo, type ContractQuery } from '@/api/contract';
|
||||
import { customerApi } from '@/api/customer';
|
||||
import { userApi } from '@/api/user';
|
||||
import { useTableScroll } from '@/composables/useTableScroll';
|
||||
import { usePermissionStore } from '@/stores/permission';
|
||||
|
||||
const { wrapperRef, tableHeight } = useTableScroll();
|
||||
const permissionStore = usePermissionStore();
|
||||
const isAdmin = computed(() => permissionStore.hasRole('ADMIN'));
|
||||
|
||||
const payCycleOptions = [
|
||||
{ label: '按月支付', value: '按月' },
|
||||
{ label: '按年支付', value: '按年' },
|
||||
];
|
||||
|
||||
const loading = ref(false);
|
||||
const dataSource = ref<ContractInfo[]>([]);
|
||||
const total = ref(0);
|
||||
const searchForm = reactive<ContractQuery>({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
keyword: '',
|
||||
salesmanId: undefined as any,
|
||||
status: undefined as any,
|
||||
});
|
||||
const modalVisible = ref(false);
|
||||
const modalTitle = ref('');
|
||||
const modalLoading = ref(false);
|
||||
const formRef = ref();
|
||||
const isEdit = ref(false);
|
||||
const editId = ref<number | null>(null);
|
||||
const customerOptions = ref<{ label: string; value: number }[]>([]);
|
||||
const salesmanOptions = ref<{ label: string; value: number }[]>([]);
|
||||
|
||||
const formData = reactive({
|
||||
customerId: undefined as number | undefined,
|
||||
xtContractNo: '',
|
||||
contractAmount: undefined as number | undefined,
|
||||
payCycle: undefined as string | undefined,
|
||||
signDate: undefined as any,
|
||||
contractDates: [] as any[],
|
||||
status: 1,
|
||||
salesmanId: undefined as number | undefined,
|
||||
contractDesc: '',
|
||||
});
|
||||
|
||||
const formRules = {
|
||||
customerId: [{ required: true, message: '请选择所属客户', trigger: 'change' }],
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{ title: '合同单号', dataIndex: 'contractNo', key: 'contractNo', ellipsis: true, width: 170 },
|
||||
{ title: '鑫潼合同号', dataIndex: 'xtContractNo', key: 'xtContractNo', ellipsis: true },
|
||||
{ title: '所属客户', key: 'customerName', ellipsis: true },
|
||||
{ title: '合同金额', key: 'contractAmount', width: 130 },
|
||||
{ title: '支付周期', dataIndex: 'payCycle', key: 'payCycle', width: 90, align: 'center' as const },
|
||||
{ title: '签订日期', dataIndex: 'signDate', key: 'signDate', width: 110 },
|
||||
{ title: '状态', key: 'status', align: 'center' as const, width: 90 },
|
||||
{ title: '操作', key: 'action', width: 140, fixed: 'right' as const },
|
||||
];
|
||||
|
||||
const pagination = computed(() => ({
|
||||
current: searchForm.page,
|
||||
pageSize: searchForm.pageSize,
|
||||
total: total.value,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (t: number) => `共 ${t} 条数据`,
|
||||
}));
|
||||
|
||||
onMounted(() => {
|
||||
fetchList();
|
||||
fetchCustomerOptions();
|
||||
fetchSalesmanList();
|
||||
});
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res: any = await contractApi.getList(searchForm);
|
||||
if (res.code === 200) {
|
||||
dataSource.value = res.data.list;
|
||||
total.value = res.data.total;
|
||||
}
|
||||
} catch {
|
||||
message.error('获取合同列表失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchCustomerOptions() {
|
||||
try {
|
||||
const res: any = await customerApi.getCustomerOptions();
|
||||
if (res.code === 200) {
|
||||
customerOptions.value = res.data.map((item: any) => ({ label: item.customerName, value: item.id }));
|
||||
}
|
||||
} catch {
|
||||
console.error('获取客户选项失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchSalesmanList() {
|
||||
try {
|
||||
const res: any = await userApi.getUserList({ page: 1, pageSize: 999 });
|
||||
if (res.code === 200) {
|
||||
salesmanOptions.value = res.data.list.map((item: any) => ({ label: item.realName, value: item.id }));
|
||||
}
|
||||
} catch {
|
||||
console.error('获取业务员列表失败');
|
||||
}
|
||||
}
|
||||
|
||||
function getCustomerName(customerId: number): string {
|
||||
const found = customerOptions.value.find((c) => c.value === customerId);
|
||||
return found ? found.label : `#${customerId}`;
|
||||
}
|
||||
|
||||
function formatMoney(amount?: number) {
|
||||
if (amount === undefined || amount === null) return '-';
|
||||
return `¥${Number(amount).toLocaleString('zh-CN', { minimumFractionDigits: 2 })}`;
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
searchForm.page = 1;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.keyword = '';
|
||||
searchForm.salesmanId = undefined as any;
|
||||
searchForm.status = undefined as any;
|
||||
searchForm.page = 1;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleTableChange(pag: any) {
|
||||
searchForm.page = pag.current;
|
||||
searchForm.pageSize = pag.pageSize;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
isEdit.value = false;
|
||||
editId.value = null;
|
||||
modalTitle.value = '新增合同';
|
||||
resetForm();
|
||||
formData.status = 1;
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function handleEdit(record: ContractInfo) {
|
||||
isEdit.value = true;
|
||||
editId.value = record.id;
|
||||
modalTitle.value = '编辑合同';
|
||||
resetForm();
|
||||
formData.customerId = record.customerId;
|
||||
formData.xtContractNo = record.xtContractNo;
|
||||
formData.contractAmount = record.contractAmount;
|
||||
formData.payCycle = record.payCycle;
|
||||
formData.signDate = record.signDate ? dayjs(record.signDate) : undefined;
|
||||
const start = record.startDate ? dayjs(record.startDate) : undefined;
|
||||
const end = record.endDate ? dayjs(record.endDate) : undefined;
|
||||
formData.contractDates = start && end ? [start, end] : [];
|
||||
formData.status = record.status ?? 1;
|
||||
formData.salesmanId = record.salesmanId;
|
||||
formData.contractDesc = record.contractDesc;
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
formData.customerId = undefined;
|
||||
formData.xtContractNo = '';
|
||||
formData.contractAmount = undefined;
|
||||
formData.payCycle = undefined;
|
||||
formData.signDate = undefined;
|
||||
formData.contractDates = [];
|
||||
formData.status = 1;
|
||||
formData.salesmanId = undefined;
|
||||
formData.contractDesc = '';
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
modalLoading.value = true;
|
||||
try {
|
||||
const submitData: any = {
|
||||
customerId: formData.customerId,
|
||||
xtContractNo: formData.xtContractNo,
|
||||
contractAmount: formData.contractAmount,
|
||||
payCycle: formData.payCycle,
|
||||
signDate: formData.signDate ? formData.signDate.format('YYYY-MM-DD') : undefined,
|
||||
startDate: formData.contractDates?.[0] ? formData.contractDates[0].format('YYYY-MM-DD') : undefined,
|
||||
endDate: formData.contractDates?.[1] ? formData.contractDates[1].format('YYYY-MM-DD') : undefined,
|
||||
status: formData.status,
|
||||
contractDesc: formData.contractDesc,
|
||||
};
|
||||
if (isAdmin.value && formData.salesmanId !== undefined) {
|
||||
submitData.salesmanId = formData.salesmanId;
|
||||
}
|
||||
|
||||
let res: any;
|
||||
if (isEdit.value && editId.value) {
|
||||
res = await contractApi.update(editId.value, submitData);
|
||||
} else {
|
||||
res = await contractApi.create(submitData);
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
message.success(isEdit.value ? '更新成功' : '创建成功');
|
||||
modalVisible.value = false;
|
||||
fetchList();
|
||||
} else {
|
||||
message.error(res.message || '操作失败');
|
||||
}
|
||||
} catch {
|
||||
message.error('操作失败');
|
||||
} finally {
|
||||
modalLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
modalVisible.value = false;
|
||||
resetForm();
|
||||
}
|
||||
|
||||
async function handleDelete(id: number) {
|
||||
try {
|
||||
const res: any = await contractApi.delete(id);
|
||||
if (res.code === 200) {
|
||||
message.success('删除成功');
|
||||
fetchList();
|
||||
} else {
|
||||
message.error(res.message || '删除失败');
|
||||
}
|
||||
} catch {
|
||||
message.error('删除失败');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-management {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
:deep(.ant-card) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
:deep(.ant-card-body) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
.search-bar {
|
||||
flex-shrink: 0;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
.table-wrapper {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
:deep(.ant-table) {
|
||||
font-size: 14px;
|
||||
height: 100%;
|
||||
}
|
||||
:deep(.ant-btn-link) {
|
||||
padding: 0 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,477 @@
|
||||
<template>
|
||||
<div class="page-management">
|
||||
<a-card title="项目方案管理">
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="handleAdd">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新增方案
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<div class="search-bar">
|
||||
<a-input
|
||||
v-model:value="searchForm.keyword"
|
||||
placeholder="方案单号/登记人"
|
||||
allow-clear
|
||||
style="width: 220px"
|
||||
@pressEnter="handleSearch"
|
||||
>
|
||||
<template #prefix><SearchOutlined /></template>
|
||||
</a-input>
|
||||
<a-select v-model:value="searchForm.status" placeholder="状态" allow-clear style="width: 130px">
|
||||
<a-select-option :value="0">草稿</a-select-option>
|
||||
<a-select-option :value="1">已确认</a-select-option>
|
||||
</a-select>
|
||||
<a-button type="primary" @click="handleSearch">
|
||||
<template #icon><SearchOutlined /></template>
|
||||
查询
|
||||
</a-button>
|
||||
<a-button @click="handleReset">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
重置
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<div class="table-wrapper" ref="wrapperRef">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="dataSource"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
row-key="id"
|
||||
:scroll="{ x: 'max-content', y: tableHeight }"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'customerName'">
|
||||
{{ getCustomerName(record.customerId) }}
|
||||
</template>
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="record.status === 1 ? 'green' : 'default'">
|
||||
{{ record.status === 1 ? '已确认' : '草稿' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'registerTime'">
|
||||
{{ record.registerTime || '-' }}
|
||||
</template>
|
||||
<template v-if="column.key === 'details'">
|
||||
<a-badge :count="(record.details || []).length" :number-style="{ backgroundColor: '#1677ff' }" />
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleView(record)">
|
||||
<template #icon><SearchOutlined /></template>
|
||||
明细
|
||||
</a-button>
|
||||
<a-button type="link" size="small" @click="handleEdit(record)">
|
||||
<template #icon><EditOutlined /></template>
|
||||
编辑
|
||||
</a-button>
|
||||
<a-popconfirm title="确定要删除该方案吗?" ok-text="确定" cancel-text="取消" @confirm="handleDelete(record.id)">
|
||||
<a-button type="link" size="small" danger>
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template #emptyText>
|
||||
<a-empty description="暂无方案数据" />
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
:title="modalTitle"
|
||||
:confirm-loading="modalLoading"
|
||||
width="820px"
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 16 }"
|
||||
>
|
||||
<a-form-item label="所属客户" name="customerId">
|
||||
<a-select
|
||||
v-model:value="formData.customerId"
|
||||
placeholder="请选择客户"
|
||||
:options="customerOptions"
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="登记人" name="registrar">
|
||||
<a-input v-model:value="formData.registrar" placeholder="请输入登记人" maxlength="64" />
|
||||
</a-form-item>
|
||||
<a-form-item label="登记时间" name="registerTime">
|
||||
<a-date-picker v-model:value="formData.registerTime" style="width: 100%" placeholder="请选择登记时间" />
|
||||
</a-form-item>
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-radio-group v-model:value="formData.status">
|
||||
<a-radio :value="0">草稿</a-radio>
|
||||
<a-radio :value="1">已确认</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="方案说明" name="planDesc">
|
||||
<a-textarea v-model:value="formData.planDesc" :rows="2" placeholder="请输入方案说明" maxlength="2000" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<div class="detail-block">
|
||||
<div class="detail-header">
|
||||
<span>方案明细</span>
|
||||
<a-button type="dashed" size="small" @click="addDetailRow">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
添加明细
|
||||
</a-button>
|
||||
</div>
|
||||
<a-table
|
||||
:columns="detailColumns"
|
||||
:data-source="formData.details"
|
||||
row-key="id"
|
||||
:pagination="false"
|
||||
size="small"
|
||||
:scroll="{ y: 200 }"
|
||||
>
|
||||
<template #bodyCell="{ column, record, index }">
|
||||
<template v-if="column.key === 'equipName'">
|
||||
<a-input v-model:value="record.equipName" placeholder="设备名称" maxlength="128" />
|
||||
</template>
|
||||
<template v-if="column.key === 'quantity'">
|
||||
<a-input-number v-model:value="record.quantity" :min="0" style="width: 100%" placeholder="数量" />
|
||||
</template>
|
||||
<template v-if="column.key === 'remark'">
|
||||
<a-input v-model:value="record.remark" placeholder="备注" maxlength="2000" />
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-button type="link" size="small" danger @click="removeDetailRow(index)">删除</a-button>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</a-modal>
|
||||
|
||||
<a-modal v-model:open="viewVisible" title="方案明细" width="700px" :footer="null" @cancel="viewVisible = false">
|
||||
<a-descriptions :column="1" bordered size="small">
|
||||
<a-descriptions-item label="方案单号">{{ viewData.planNo }}</a-descriptions-item>
|
||||
<a-descriptions-item label="所属客户">{{ getCustomerName(viewData.customerId) }}</a-descriptions-item>
|
||||
<a-descriptions-item label="登记人">{{ viewData.registrar || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="登记时间">{{ viewData.registerTime || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="方案说明">{{ viewData.planDesc || '-' }}</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
<a-table
|
||||
:columns="viewDetailColumns"
|
||||
:data-source="viewData.details || []"
|
||||
row-key="id"
|
||||
:pagination="false"
|
||||
size="small"
|
||||
style="margin-top: 16px"
|
||||
/>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { SearchOutlined, ReloadOutlined, PlusOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons-vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { projectPlanApi, type ProjectPlanInfo, type ProjectPlanQuery, type PlanDetail } from '@/api/projectPlan';
|
||||
import { customerApi } from '@/api/customer';
|
||||
import { useTableScroll } from '@/composables/useTableScroll';
|
||||
|
||||
const { wrapperRef, tableHeight } = useTableScroll();
|
||||
|
||||
const loading = ref(false);
|
||||
const dataSource = ref<ProjectPlanInfo[]>([]);
|
||||
const total = ref(0);
|
||||
const searchForm = reactive<ProjectPlanQuery>({ page: 1, pageSize: 10, keyword: '', status: undefined as any });
|
||||
const modalVisible = ref(false);
|
||||
const modalTitle = ref('');
|
||||
const modalLoading = ref(false);
|
||||
const formRef = ref();
|
||||
const isEdit = ref(false);
|
||||
const editId = ref<number | null>(null);
|
||||
const customerOptions = ref<{ label: string; value: number }[]>([]);
|
||||
const viewVisible = ref(false);
|
||||
const viewData = reactive<Partial<ProjectPlanInfo>>({});
|
||||
let detailSeq = 1;
|
||||
|
||||
const formData = reactive({
|
||||
customerId: undefined as number | undefined,
|
||||
registrar: '',
|
||||
registerTime: undefined as any,
|
||||
status: 0,
|
||||
planDesc: '',
|
||||
details: [] as any[],
|
||||
});
|
||||
|
||||
const formRules = {
|
||||
customerId: [{ required: true, message: '请选择所属客户', trigger: 'change' }],
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{ title: '方案单号', dataIndex: 'planNo', key: 'planNo', ellipsis: true, width: 170 },
|
||||
{ title: '所属客户', key: 'customerName', ellipsis: true },
|
||||
{ title: '登记人', dataIndex: 'registrar', key: 'registrar', ellipsis: true },
|
||||
{ title: '登记时间', key: 'registerTime', width: 110 },
|
||||
{ title: '明细数', key: 'details', align: 'center' as const, width: 90 },
|
||||
{ title: '状态', key: 'status', align: 'center' as const, width: 90 },
|
||||
{ title: '操作', key: 'action', width: 180, fixed: 'right' as const },
|
||||
];
|
||||
|
||||
const detailColumns = [
|
||||
{ title: '设备名称', key: 'equipName', dataIndex: 'equipName' },
|
||||
{ title: '数量', key: 'quantity', dataIndex: 'quantity', width: 120 },
|
||||
{ title: '备注', key: 'remark', dataIndex: 'remark' },
|
||||
{ title: '操作', key: 'action', width: 70, align: 'center' as const },
|
||||
];
|
||||
|
||||
const viewDetailColumns = [
|
||||
{ title: '设备名称', dataIndex: 'equipName', key: 'equipName' },
|
||||
{ title: '数量', dataIndex: 'quantity', key: 'quantity', width: 120 },
|
||||
{ title: '备注', dataIndex: 'remark', key: 'remark', ellipsis: true },
|
||||
];
|
||||
|
||||
const pagination = computed(() => ({
|
||||
current: searchForm.page,
|
||||
pageSize: searchForm.pageSize,
|
||||
total: total.value,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (t: number) => `共 ${t} 条数据`,
|
||||
}));
|
||||
|
||||
onMounted(() => {
|
||||
fetchList();
|
||||
fetchCustomerOptions();
|
||||
});
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res: any = await projectPlanApi.getList(searchForm);
|
||||
if (res.code === 200) {
|
||||
dataSource.value = res.data.list;
|
||||
total.value = res.data.total;
|
||||
}
|
||||
} catch {
|
||||
message.error('获取方案列表失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchCustomerOptions() {
|
||||
try {
|
||||
const res: any = await customerApi.getCustomerOptions();
|
||||
if (res.code === 200) {
|
||||
customerOptions.value = res.data.map((item: any) => ({ label: item.customerName, value: item.id }));
|
||||
}
|
||||
} catch {
|
||||
console.error('获取客户选项失败');
|
||||
}
|
||||
}
|
||||
|
||||
function getCustomerName(customerId: number): string {
|
||||
const found = customerOptions.value.find((c) => c.value === customerId);
|
||||
return found ? found.label : `#${customerId}`;
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
searchForm.page = 1;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.keyword = '';
|
||||
searchForm.status = undefined as any;
|
||||
searchForm.page = 1;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleTableChange(pag: any) {
|
||||
searchForm.page = pag.current;
|
||||
searchForm.pageSize = pag.pageSize;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function addDetailRow() {
|
||||
formData.details.push({ id: `new-${detailSeq++}`, equipName: '', quantity: undefined, remark: '' });
|
||||
}
|
||||
|
||||
function removeDetailRow(index: number) {
|
||||
formData.details.splice(index, 1);
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
isEdit.value = false;
|
||||
editId.value = null;
|
||||
modalTitle.value = '新增方案';
|
||||
resetForm();
|
||||
formData.details = [];
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function handleEdit(record: ProjectPlanInfo) {
|
||||
isEdit.value = true;
|
||||
editId.value = record.id;
|
||||
modalTitle.value = '编辑方案';
|
||||
resetForm();
|
||||
formData.customerId = record.customerId;
|
||||
formData.registrar = record.registrar;
|
||||
formData.registerTime = record.registerTime ? dayjs(record.registerTime) : undefined;
|
||||
formData.status = record.status ?? 0;
|
||||
formData.planDesc = record.planDesc;
|
||||
formData.details = (record.details || []).map((d) => ({
|
||||
id: d.id ?? `new-${detailSeq++}`,
|
||||
equipName: d.equipName,
|
||||
quantity: d.quantity,
|
||||
remark: d.remark,
|
||||
}));
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function handleView(record: ProjectPlanInfo) {
|
||||
Object.assign(viewData, record);
|
||||
viewVisible.value = true;
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
formData.customerId = undefined;
|
||||
formData.registrar = '';
|
||||
formData.registerTime = undefined;
|
||||
formData.status = 0;
|
||||
formData.planDesc = '';
|
||||
formData.details = [];
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
modalLoading.value = true;
|
||||
try {
|
||||
const submitData: any = {
|
||||
customerId: formData.customerId,
|
||||
registrar: formData.registrar,
|
||||
registerTime: formData.registerTime ? formData.registerTime.format('YYYY-MM-DD') : undefined,
|
||||
status: formData.status,
|
||||
planDesc: formData.planDesc,
|
||||
details: formData.details
|
||||
.filter((d: any) => d.equipName || d.quantity)
|
||||
.map((d: any) => ({ id: Number(d.id) || undefined, equipName: d.equipName, quantity: d.quantity, remark: d.remark })),
|
||||
};
|
||||
|
||||
let res: any;
|
||||
if (isEdit.value && editId.value) {
|
||||
res = await projectPlanApi.update(editId.value, submitData);
|
||||
} else {
|
||||
res = await projectPlanApi.create(submitData);
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
message.success(isEdit.value ? '更新成功' : '创建成功');
|
||||
modalVisible.value = false;
|
||||
fetchList();
|
||||
} else {
|
||||
message.error(res.message || '操作失败');
|
||||
}
|
||||
} catch {
|
||||
message.error('操作失败');
|
||||
} finally {
|
||||
modalLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
modalVisible.value = false;
|
||||
resetForm();
|
||||
}
|
||||
|
||||
async function handleDelete(id: number) {
|
||||
try {
|
||||
const res: any = await projectPlanApi.delete(id);
|
||||
if (res.code === 200) {
|
||||
message.success('删除成功');
|
||||
fetchList();
|
||||
} else {
|
||||
message.error(res.message || '删除失败');
|
||||
}
|
||||
} catch {
|
||||
message.error('删除失败');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-management {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
:deep(.ant-card) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
:deep(.ant-card-body) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
.search-bar {
|
||||
flex-shrink: 0;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
.table-wrapper {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
:deep(.ant-table) {
|
||||
font-size: 14px;
|
||||
height: 100%;
|
||||
}
|
||||
:deep(.ant-btn-link) {
|
||||
padding: 0 4px;
|
||||
}
|
||||
.detail-block {
|
||||
margin-top: 8px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
padding-top: 12px;
|
||||
}
|
||||
.detail-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,380 @@
|
||||
<template>
|
||||
<div class="page-management">
|
||||
<a-card title="项目勘察管理">
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="handleAdd">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新增勘察单
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<div class="search-bar">
|
||||
<a-input
|
||||
v-model:value="searchForm.keyword"
|
||||
placeholder="勘察单号/勘察人"
|
||||
allow-clear
|
||||
style="width: 220px"
|
||||
@pressEnter="handleSearch"
|
||||
>
|
||||
<template #prefix><SearchOutlined /></template>
|
||||
</a-input>
|
||||
<a-select v-model:value="searchForm.status" placeholder="状态" allow-clear style="width: 130px">
|
||||
<a-select-option :value="0">已派发</a-select-option>
|
||||
<a-select-option :value="1">已接单</a-select-option>
|
||||
<a-select-option :value="2">已完成</a-select-option>
|
||||
</a-select>
|
||||
<a-button type="primary" @click="handleSearch">
|
||||
<template #icon><SearchOutlined /></template>
|
||||
查询
|
||||
</a-button>
|
||||
<a-button @click="handleReset">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
重置
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<div class="table-wrapper" ref="wrapperRef">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="dataSource"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
row-key="id"
|
||||
:scroll="{ x: 'max-content', y: tableHeight }"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'customerName'">
|
||||
{{ getCustomerName(record.customerId) }}
|
||||
</template>
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="getStatusColor(record.status)">{{ getStatusText(record.status) }}</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'surveyTime'">
|
||||
{{ record.surveyTime ? formatDate(record.surveyTime) : '-' }}
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleEdit(record)">
|
||||
<template #icon><EditOutlined /></template>
|
||||
编辑
|
||||
</a-button>
|
||||
<a-popconfirm title="确定要删除该勘察单吗?" ok-text="确定" cancel-text="取消" @confirm="handleDelete(record.id)">
|
||||
<a-button type="link" size="small" danger>
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template #emptyText>
|
||||
<a-empty description="暂无勘察单数据" />
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
:title="modalTitle"
|
||||
:confirm-loading="modalLoading"
|
||||
width="620px"
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 16 }"
|
||||
>
|
||||
<a-form-item label="所属客户" name="customerId">
|
||||
<a-select
|
||||
v-model:value="formData.customerId"
|
||||
placeholder="请选择客户"
|
||||
:options="customerOptions"
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="勘察人" name="surveyor">
|
||||
<a-input v-model:value="formData.surveyor" placeholder="请输入勘察人" maxlength="64" />
|
||||
</a-form-item>
|
||||
<a-form-item label="勘察时间" name="surveyTime">
|
||||
<a-date-picker v-model:value="formData.surveyTime" style="width: 100%" placeholder="请选择勘察时间" />
|
||||
</a-form-item>
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-radio-group v-model:value="formData.status">
|
||||
<a-radio :value="0">已派发</a-radio>
|
||||
<a-radio :value="1">已接单</a-radio>
|
||||
<a-radio :value="2">已完成</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="备注" name="remark">
|
||||
<a-textarea v-model:value="formData.remark" :rows="3" placeholder="请输入备注" maxlength="2000" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { SearchOutlined, ReloadOutlined, PlusOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons-vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { surveyOrderApi, type SurveyOrderInfo, type SurveyOrderQuery } from '@/api/surveyOrder';
|
||||
import { customerApi } from '@/api/customer';
|
||||
import { useTableScroll } from '@/composables/useTableScroll';
|
||||
|
||||
const { wrapperRef, tableHeight } = useTableScroll();
|
||||
|
||||
const loading = ref(false);
|
||||
const dataSource = ref<SurveyOrderInfo[]>([]);
|
||||
const total = ref(0);
|
||||
const searchForm = reactive<SurveyOrderQuery>({ page: 1, pageSize: 10, keyword: '', status: undefined as any });
|
||||
const modalVisible = ref(false);
|
||||
const modalTitle = ref('');
|
||||
const modalLoading = ref(false);
|
||||
const formRef = ref();
|
||||
const isEdit = ref(false);
|
||||
const editId = ref<number | null>(null);
|
||||
const customerOptions = ref<{ label: string; value: number }[]>([]);
|
||||
|
||||
const formData = reactive({
|
||||
customerId: undefined as number | undefined,
|
||||
surveyor: '',
|
||||
surveyTime: undefined as any,
|
||||
status: 0,
|
||||
remark: '',
|
||||
});
|
||||
|
||||
const formRules = {
|
||||
customerId: [{ required: true, message: '请选择所属客户', trigger: 'change' }],
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{ title: '勘察单号', dataIndex: 'surveyNo', key: 'surveyNo', ellipsis: true, width: 170 },
|
||||
{ title: '所属客户', key: 'customerName', ellipsis: true },
|
||||
{ title: '勘察人', dataIndex: 'surveyor', key: 'surveyor', ellipsis: true },
|
||||
{ title: '勘察时间', key: 'surveyTime', width: 110 },
|
||||
{ title: '状态', key: 'status', align: 'center' as const, width: 100 },
|
||||
{ title: '备注', dataIndex: 'remark', key: 'remark', ellipsis: true },
|
||||
{ title: '操作', key: 'action', width: 140, fixed: 'right' as const },
|
||||
];
|
||||
|
||||
const pagination = computed(() => ({
|
||||
current: searchForm.page,
|
||||
pageSize: searchForm.pageSize,
|
||||
total: total.value,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (t: number) => `共 ${t} 条数据`,
|
||||
}));
|
||||
|
||||
onMounted(() => {
|
||||
fetchList();
|
||||
fetchCustomerOptions();
|
||||
});
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res: any = await surveyOrderApi.getList(searchForm);
|
||||
if (res.code === 200) {
|
||||
dataSource.value = res.data.list;
|
||||
total.value = res.data.total;
|
||||
}
|
||||
} catch {
|
||||
message.error('获取勘察单列表失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchCustomerOptions() {
|
||||
try {
|
||||
const res: any = await customerApi.getCustomerOptions();
|
||||
if (res.code === 200) {
|
||||
customerOptions.value = res.data.map((item: any) => ({ label: item.customerName, value: item.id }));
|
||||
}
|
||||
} catch {
|
||||
console.error('获取客户选项失败');
|
||||
}
|
||||
}
|
||||
|
||||
function getCustomerName(customerId: number): string {
|
||||
const found = customerOptions.value.find((c) => c.value === customerId);
|
||||
return found ? found.label : `#${customerId}`;
|
||||
}
|
||||
|
||||
function getStatusColor(status: number): string {
|
||||
const map: Record<number, string> = { 0: 'blue', 1: 'orange', 2: 'green' };
|
||||
return map[status] || 'default';
|
||||
}
|
||||
|
||||
function getStatusText(status: number): string {
|
||||
const map: Record<number, string> = { 0: '已派发', 1: '已接单', 2: '已完成' };
|
||||
return map[status] || '未知';
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
searchForm.page = 1;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.keyword = '';
|
||||
searchForm.status = undefined as any;
|
||||
searchForm.page = 1;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleTableChange(pag: any) {
|
||||
searchForm.page = pag.current;
|
||||
searchForm.pageSize = pag.pageSize;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
isEdit.value = false;
|
||||
editId.value = null;
|
||||
modalTitle.value = '新增勘察单';
|
||||
resetForm();
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function handleEdit(record: SurveyOrderInfo) {
|
||||
isEdit.value = true;
|
||||
editId.value = record.id;
|
||||
modalTitle.value = '编辑勘察单';
|
||||
resetForm();
|
||||
formData.customerId = record.customerId;
|
||||
formData.surveyor = record.surveyor;
|
||||
formData.surveyTime = record.surveyTime ? dayjs(record.surveyTime) : undefined;
|
||||
formData.status = record.status ?? 0;
|
||||
formData.remark = record.remark;
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
formData.customerId = undefined;
|
||||
formData.surveyor = '';
|
||||
formData.surveyTime = undefined;
|
||||
formData.status = 0;
|
||||
formData.remark = '';
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
modalLoading.value = true;
|
||||
try {
|
||||
const submitData: any = {
|
||||
customerId: formData.customerId,
|
||||
surveyor: formData.surveyor,
|
||||
surveyTime: formData.surveyTime ? formData.surveyTime.format('YYYY-MM-DD') : undefined,
|
||||
status: formData.status,
|
||||
remark: formData.remark,
|
||||
};
|
||||
|
||||
let res: any;
|
||||
if (isEdit.value && editId.value) {
|
||||
res = await surveyOrderApi.update(editId.value, submitData);
|
||||
} else {
|
||||
res = await surveyOrderApi.create(submitData);
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
message.success(isEdit.value ? '更新成功' : '创建成功');
|
||||
modalVisible.value = false;
|
||||
fetchList();
|
||||
} else {
|
||||
message.error(res.message || '操作失败');
|
||||
}
|
||||
} catch {
|
||||
message.error('操作失败');
|
||||
} finally {
|
||||
modalLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
modalVisible.value = false;
|
||||
resetForm();
|
||||
}
|
||||
|
||||
async function handleDelete(id: number) {
|
||||
try {
|
||||
const res: any = await surveyOrderApi.delete(id);
|
||||
if (res.code === 200) {
|
||||
message.success('删除成功');
|
||||
fetchList();
|
||||
} else {
|
||||
message.error(res.message || '删除失败');
|
||||
}
|
||||
} catch {
|
||||
message.error('删除失败');
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(str: string) {
|
||||
return dayjs(str).format('YYYY-MM-DD');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-management {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
:deep(.ant-card) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
:deep(.ant-card-body) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
.search-bar {
|
||||
flex-shrink: 0;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
.table-wrapper {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
:deep(.ant-table) {
|
||||
font-size: 14px;
|
||||
height: 100%;
|
||||
}
|
||||
:deep(.ant-btn-link) {
|
||||
padding: 0 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,361 @@
|
||||
<template>
|
||||
<div class="page-management">
|
||||
<a-card title="回访记录管理">
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="handleAdd">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新增回访
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<div class="search-bar">
|
||||
<a-input
|
||||
v-model:value="searchForm.keyword"
|
||||
placeholder="联系人/回访人"
|
||||
allow-clear
|
||||
style="width: 220px"
|
||||
@pressEnter="handleSearch"
|
||||
>
|
||||
<template #prefix><SearchOutlined /></template>
|
||||
</a-input>
|
||||
<a-button type="primary" @click="handleSearch">
|
||||
<template #icon><SearchOutlined /></template>
|
||||
查询
|
||||
</a-button>
|
||||
<a-button @click="handleReset">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
重置
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<div class="table-wrapper" ref="wrapperRef">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="dataSource"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
row-key="id"
|
||||
:scroll="{ x: 'max-content', y: tableHeight }"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'customerName'">
|
||||
{{ getCustomerName(record.customerId) }}
|
||||
</template>
|
||||
<template v-if="column.key === 'visitTime'">
|
||||
{{ record.visitTime || '-' }}
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleEdit(record)">
|
||||
<template #icon><EditOutlined /></template>
|
||||
编辑
|
||||
</a-button>
|
||||
<a-popconfirm title="确定要删除该回访记录吗?" ok-text="确定" cancel-text="取消" @confirm="handleDelete(record.id)">
|
||||
<a-button type="link" size="small" danger>
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template #emptyText>
|
||||
<a-empty description="暂无回访记录" />
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
:title="modalTitle"
|
||||
:confirm-loading="modalLoading"
|
||||
width="620px"
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 16 }"
|
||||
>
|
||||
<a-form-item label="所属客户" name="customerId">
|
||||
<a-select
|
||||
v-model:value="formData.customerId"
|
||||
placeholder="请选择客户"
|
||||
:options="customerOptions"
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="联系人" name="contactPerson">
|
||||
<a-input v-model:value="formData.contactPerson" placeholder="请输入联系人" maxlength="64" />
|
||||
</a-form-item>
|
||||
<a-form-item label="联系电话" name="contactPhone">
|
||||
<a-input v-model:value="formData.contactPhone" placeholder="请输入联系电话" />
|
||||
</a-form-item>
|
||||
<a-form-item label="回访人" name="visitor">
|
||||
<a-input v-model:value="formData.visitor" placeholder="请输入回访人" maxlength="64" />
|
||||
</a-form-item>
|
||||
<a-form-item label="回访时间" name="visitTime">
|
||||
<a-date-picker v-model:value="formData.visitTime" style="width: 100%" placeholder="请选择回访时间" />
|
||||
</a-form-item>
|
||||
<a-form-item label="客户意见" name="customerOpinion">
|
||||
<a-textarea v-model:value="formData.customerOpinion" :rows="3" placeholder="请输入客户意见" maxlength="2000" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { SearchOutlined, ReloadOutlined, PlusOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons-vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { visitRecordApi, type VisitRecordInfo, type VisitRecordQuery } from '@/api/visitRecord';
|
||||
import { customerApi } from '@/api/customer';
|
||||
import { useTableScroll } from '@/composables/useTableScroll';
|
||||
|
||||
const { wrapperRef, tableHeight } = useTableScroll();
|
||||
|
||||
const loading = ref(false);
|
||||
const dataSource = ref<VisitRecordInfo[]>([]);
|
||||
const total = ref(0);
|
||||
const searchForm = reactive<VisitRecordQuery>({ page: 1, pageSize: 10, keyword: '' });
|
||||
const modalVisible = ref(false);
|
||||
const modalTitle = ref('');
|
||||
const modalLoading = ref(false);
|
||||
const formRef = ref();
|
||||
const isEdit = ref(false);
|
||||
const editId = ref<number | null>(null);
|
||||
const customerOptions = ref<{ label: string; value: number }[]>([]);
|
||||
|
||||
const formData = reactive({
|
||||
customerId: undefined as number | undefined,
|
||||
contactPerson: '',
|
||||
contactPhone: '',
|
||||
visitor: '',
|
||||
visitTime: undefined as any,
|
||||
customerOpinion: '',
|
||||
});
|
||||
|
||||
const formRules = {
|
||||
customerId: [{ required: true, message: '请选择所属客户', trigger: 'change' }],
|
||||
contactPhone: [{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' }],
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{ title: '所属客户', key: 'customerName', ellipsis: true },
|
||||
{ title: '联系人', dataIndex: 'contactPerson', key: 'contactPerson', ellipsis: true },
|
||||
{ title: '联系电话', dataIndex: 'contactPhone', key: 'contactPhone', ellipsis: true },
|
||||
{ title: '回访人', dataIndex: 'visitor', key: 'visitor', ellipsis: true },
|
||||
{ title: '回访时间', key: 'visitTime', width: 110 },
|
||||
{ title: '客户意见', dataIndex: 'customerOpinion', key: 'customerOpinion', ellipsis: true },
|
||||
{ title: '操作', key: 'action', width: 140, fixed: 'right' as const },
|
||||
];
|
||||
|
||||
const pagination = computed(() => ({
|
||||
current: searchForm.page,
|
||||
pageSize: searchForm.pageSize,
|
||||
total: total.value,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (t: number) => `共 ${t} 条数据`,
|
||||
}));
|
||||
|
||||
onMounted(() => {
|
||||
fetchList();
|
||||
fetchCustomerOptions();
|
||||
});
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res: any = await visitRecordApi.getList(searchForm);
|
||||
if (res.code === 200) {
|
||||
dataSource.value = res.data.list;
|
||||
total.value = res.data.total;
|
||||
}
|
||||
} catch {
|
||||
message.error('获取回访记录失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchCustomerOptions() {
|
||||
try {
|
||||
const res: any = await customerApi.getCustomerOptions();
|
||||
if (res.code === 200) {
|
||||
customerOptions.value = res.data.map((item: any) => ({ label: item.customerName, value: item.id }));
|
||||
}
|
||||
} catch {
|
||||
console.error('获取客户选项失败');
|
||||
}
|
||||
}
|
||||
|
||||
function getCustomerName(customerId: number): string {
|
||||
const found = customerOptions.value.find((c) => c.value === customerId);
|
||||
return found ? found.label : `#${customerId}`;
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
searchForm.page = 1;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.keyword = '';
|
||||
searchForm.page = 1;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleTableChange(pag: any) {
|
||||
searchForm.page = pag.current;
|
||||
searchForm.pageSize = pag.pageSize;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
isEdit.value = false;
|
||||
editId.value = null;
|
||||
modalTitle.value = '新增回访';
|
||||
resetForm();
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function handleEdit(record: VisitRecordInfo) {
|
||||
isEdit.value = true;
|
||||
editId.value = record.id;
|
||||
modalTitle.value = '编辑回访';
|
||||
resetForm();
|
||||
formData.customerId = record.customerId;
|
||||
formData.contactPerson = record.contactPerson;
|
||||
formData.contactPhone = record.contactPhone;
|
||||
formData.visitor = record.visitor;
|
||||
formData.visitTime = record.visitTime ? dayjs(record.visitTime) : undefined;
|
||||
formData.customerOpinion = record.customerOpinion;
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
formData.customerId = undefined;
|
||||
formData.contactPerson = '';
|
||||
formData.contactPhone = '';
|
||||
formData.visitor = '';
|
||||
formData.visitTime = undefined;
|
||||
formData.customerOpinion = '';
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
modalLoading.value = true;
|
||||
try {
|
||||
const submitData: any = {
|
||||
customerId: formData.customerId,
|
||||
contactPerson: formData.contactPerson,
|
||||
contactPhone: formData.contactPhone,
|
||||
visitor: formData.visitor,
|
||||
visitTime: formData.visitTime ? formData.visitTime.format('YYYY-MM-DD') : undefined,
|
||||
customerOpinion: formData.customerOpinion,
|
||||
};
|
||||
|
||||
let res: any;
|
||||
if (isEdit.value && editId.value) {
|
||||
res = await visitRecordApi.update(editId.value, submitData);
|
||||
} else {
|
||||
res = await visitRecordApi.create(submitData);
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
message.success(isEdit.value ? '更新成功' : '创建成功');
|
||||
modalVisible.value = false;
|
||||
fetchList();
|
||||
} else {
|
||||
message.error(res.message || '操作失败');
|
||||
}
|
||||
} catch {
|
||||
message.error('操作失败');
|
||||
} finally {
|
||||
modalLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
modalVisible.value = false;
|
||||
resetForm();
|
||||
}
|
||||
|
||||
async function handleDelete(id: number) {
|
||||
try {
|
||||
const res: any = await visitRecordApi.delete(id);
|
||||
if (res.code === 200) {
|
||||
message.success('删除成功');
|
||||
fetchList();
|
||||
} else {
|
||||
message.error(res.message || '删除失败');
|
||||
}
|
||||
} catch {
|
||||
message.error('删除失败');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-management {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
:deep(.ant-card) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
:deep(.ant-card-body) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
.search-bar {
|
||||
flex-shrink: 0;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
.table-wrapper {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
:deep(.ant-table) {
|
||||
font-size: 14px;
|
||||
height: 100%;
|
||||
}
|
||||
:deep(.ant-btn-link) {
|
||||
padding: 0 4px;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user