本次提交新增了客户拜访、客户建筑、客户房间的完整业务模块,同时补充了项目勘察、项目方案、合同管理、项目验收、回访记录的全项目流程相关功能,包括: 1. 新增对应业务的实体类、DTO、控制器、服务层代码 2. 完善RBAC权限映射配置 3. 添加前端API、路由及页面菜单配置 4. 更新数据库种子数据补充菜单和权限绑定
432 lines
11 KiB
Vue
432 lines
11 KiB
Vue
<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> |