feat: 新增会议管理菜单

This commit is contained in:
fyy 2025-06-19 16:54:36 +08:00
parent a2ca8c2d09
commit f31c90cb61
9 changed files with 847 additions and 0 deletions

View File

@ -0,0 +1,5 @@
<template>
<div>会议室增值服务</div>
</template>
<script></script>
<style lang="scss"></style>

View File

@ -0,0 +1,270 @@
import type { FormSchemaGetter } from '#/adapter/form';
import type { VxeGridProps } from '#/adapter/vxe-table';
import { getDictOptions } from '#/utils/dict';
import { renderDict } from '#/utils/render';
export const querySchema: FormSchemaGetter = () => [
{
component: 'Input',
fieldName: 'roomName',
label: '会议室名称',
},
{
component: 'Input',
fieldName: 'bookingName',
label: '会议预订人',
},
{
component: 'Select',
componentProps: {
// 可选从DictEnum中获取 DictEnum.WY_YYZT 便于维护
options: getDictOptions('wy_yyzt'),
},
fieldName: 'bookingStatus',
label: '预约状态',
},
];
// 需要使用i18n注意这里要改成getter形式 否则切换语言不会刷新
// export const columns: () => VxeGridProps['columns'] = () => [
export const columns: VxeGridProps['columns'] = [
{ type: 'checkbox', width: 60 },
{
title: '会议室id',
field: 'tbConferenceId',
},
{
title: '预约状态',
field: 'bookingStatus',
slots: {
default: ({ row }) => {
// 可选从DictEnum中获取 DictEnum.WY_YYZT 便于维护
return renderDict(row.bookingStatus, 'wy_yyzt');
},
},
},
{
title: '审核状态',
field: 'reviewStatus',
slots: {
default: ({ row }) => {
// 可选从DictEnum中获取 DictEnum.WY_SHZT 便于维护
return renderDict(row.reviewStatus, 'wy_shzt');
},
},
},
{
title: '会议预订人',
field: 'bookingName',
},
{
title: '使用单位',
field: 'userUnit',
},
{
title: '会议主题',
field: 'conferenceTheme',
},
{
title: '预约日期',
field: 'appointmentDate',
},
{
title: '预约开始时段',
field: 'appointmentBeginTime',
},
{
title: '预约结束时段',
field: 'appointmentEndTime',
},
{
title: '参会人员',
field: 'attendeesName',
},
{
title: '参会人数',
field: 'approverCount',
},
{
title: '签到开始时间',
field: 'checkInStartTime',
},
{
title: '签到结束时间',
field: 'checkInEndTime',
},
{
title: '评价',
field: 'evaluate',
},
{
title: '备注',
field: 'remark',
},
{
title: '是否需要增值服务',
field: 'addServices',
slots: {
default: ({ row }) => {
// 可选从DictEnum中获取 DictEnum.WY_SF 便于维护
return renderDict(row.addServices, 'wy_sf');
},
},
},
{
field: 'action',
fixed: 'right',
slots: { default: 'action' },
title: '操作',
width: 180,
},
];
export const modalSchema: FormSchemaGetter = () => [
{
label: 'id',
fieldName: 'id',
component: 'Input',
dependencies: {
show: () => false,
triggerFields: [''],
},
},
{
label: '会议室id',
fieldName: 'tbConferenceId',
component: 'Input',
rules: 'required',
},
{
label: '预约状态',
fieldName: 'bookingStatus',
component: 'Select',
componentProps: {
// 可选从DictEnum中获取 DictEnum.WY_YYZT 便于维护
options: getDictOptions('wy_yyzt'),
},
rules: 'selectRequired',
},
{
label: '审核状态',
fieldName: 'reviewStatus',
component: 'Select',
componentProps: {
// 可选从DictEnum中获取 DictEnum.WY_SHZT 便于维护
options: getDictOptions('wy_shzt'),
},
rules: 'selectRequired',
},
{
label: '会议预订人',
fieldName: 'bookingName',
component: 'Input',
rules: 'required',
},
{
label: '使用单位',
fieldName: 'userUnit',
component: 'Input',
rules: 'required',
},
{
label: '会议主题',
fieldName: 'conferenceTheme',
component: 'Input',
rules: 'required',
},
{
label: '预约日期',
fieldName: 'appointmentDate',
component: 'DatePicker',
componentProps: {
showTime: true,
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
},
rules: 'required',
},
{
label: '预约开始时段',
fieldName: 'appointmentBeginTime',
component: 'DatePicker',
componentProps: {
showTime: true,
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
},
rules: 'required',
},
{
label: '预约结束时段',
fieldName: 'appointmentEndTime',
component: 'DatePicker',
componentProps: {
showTime: true,
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
},
rules: 'required',
},
{
label: '参会人员',
fieldName: 'attendeesName',
component: 'Input',
rules: 'required',
},
{
label: '参会人数',
fieldName: 'approverCount',
component: 'Input',
rules: 'required',
},
{
label: '签到开始时间',
fieldName: 'checkInStartTime',
component: 'DatePicker',
componentProps: {
showTime: true,
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
},
rules: 'required',
},
{
label: '签到结束时间',
fieldName: 'checkInEndTime',
component: 'DatePicker',
componentProps: {
showTime: true,
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
},
rules: 'required',
},
{
label: '评价',
fieldName: 'evaluate',
component: 'Input',
rules: 'required',
},
{
label: '备注',
fieldName: 'remark',
component: 'Input',
rules: 'required',
},
{
label: '是否需要增值服务',
fieldName: 'addServices',
component: 'RadioGroup',
componentProps: {
// 可选从DictEnum中获取 DictEnum.WY_SF 便于维护
options: getDictOptions('wy_sf'),
buttonStyle: 'solid',
optionType: 'button',
},
rules: 'selectRequired',
},
];

View File

@ -0,0 +1,134 @@
<template>
<div class="conference-list">
<a-list
:grid="{ gutter: 16, xs: 1, sm: 2, md: 2, lg: 3, xl: 4, xxl: 4 }"
:data-source="conferenceList"
:loading="loading"
class="conference-list-content"
v-infinite-scroll="loadMore"
:infinite-scroll-disabled="disabled"
:infinite-scroll-distance="10"
>
<template #renderItem="{ item }">
<a-list-item>
<a-card :title="item.name" :bordered="false" class="conference-card">
<template #cover>
<img
alt="会议室图片"
:src="item.image || '/placeholder-image.jpg'"
style="height: 200px; object-fit: cover;"
/>
</template>
<a-card-meta :title="item.name">
<template #description>
<div class="conference-info">
<p>容纳人数{{ item.capacity }}</p>
<p>位置{{ item.location }}</p>
<p>设备{{ item.facilities }}</p>
</div>
</template>
</a-card-meta>
<div class="card-actions">
<a-button type="primary" @click="handleReservation(item)">
去预约
</a-button>
</div>
</a-card>
</a-list-item>
</template>
</a-list>
</div>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue';
import { requestClient } from '../../../../api/request';
interface ConferenceRoom {
id: string;
name: string;
capacity: number;
location: string;
facilities: string;
image?: string;
}
const pageSize = 20;
const currentPage = ref(1);
const loading = ref(false);
const conferenceList = ref<ConferenceRoom[]>([]);
const noMore = ref(false);
const disabled = computed(() => loading.value || noMore.value);
const fetchConferenceList = async (page: number) => {
try {
loading.value = true;
const res = await requestClient.get<{
list: ConferenceRoom[];
total: number;
}>('/property/conference/list', {
params: {
pageSize,
pageNum: page,
},
});
if (page === 1) {
conferenceList.value = res.list;
} else {
conferenceList.value = [...conferenceList.value, ...res.list];
}
noMore.value = conferenceList.value.length >= res.total;
} finally {
loading.value = false;
}
};
const loadMore = async () => {
if (disabled.value) return;
currentPage.value++;
await fetchConferenceList(currentPage.value);
};
const handleReservation = (room: ConferenceRoom) => {
// TODO:
console.log('预约会议室:', room);
};
//
fetchConferenceList(1);
</script>
<style lang="scss" scoped>
.conference-list {
padding: 16px;
height: 100%;
overflow: hidden;
.conference-list-content {
height: calc(100vh - 120px);
overflow-y: auto;
}
.conference-card {
transition: all 0.3s;
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.conference-info {
p {
margin-bottom: 8px;
}
}
.card-actions {
margin-top: 16px;
text-align: right;
}
}
}
</style>

View File

@ -0,0 +1,7 @@
<template>
<div>会议室设置
</div>
</template>
<script></script>
<style lang="scss"></style>

View File

@ -0,0 +1,330 @@
<script setup lang="ts">
import { ref, computed, onMounted, h } from 'vue';
import { Page } from '@vben/common-ui';
import dayjs from 'dayjs';
import type { TableColumnType } from 'ant-design-vue';
import { Radio, Select, Button, Table } from 'ant-design-vue';
import { conferenceList } from '#/api/property/conference';
import { roomBookingList } from '#/api/property/roomBooking';
import type { RadioChangeEvent } from 'ant-design-vue';
import type { SelectValue } from 'ant-design-vue/es/select';
//
interface ConferenceRoom {
id: string;
name: string;
}
interface ConferenceBooking {
id: string | number;
tbConferenceId: string | number; // ID
bookingName: string; //
deptName: string; //
subject: string; //
startTime: string; //
endTime: string; //
bookingDate: string; //
status: number; //
}
//
const viewMode = ref<'date' | 'room'>('date');
//
const roomList = ref<ConferenceRoom[]>([]);
//
const selectedRoom = ref<string>('');
//
const selectedDate = ref<string>('');
//
const weekDates = ref<string[]>([]);
//
const bookings = ref<ConferenceBooking[]>([]);
//
const timeSlots = [
'8:00', '8:30', '9:00', '9:30', '10:00', '10:30',
'11:00', '11:30', '12:00', '12:30', '13:00', '13:30',
'14:00', '14:30', '15:00', '15:30', '16:00', '16:30',
'17:00', '17:30'
];
//
function generateWeekDates(): void {
const today = dayjs();
const dates = Array.from({ length: 7 }, (_, i) => {
return today.add(i, 'day').format('YYYY-MM-DD');
});
weekDates.value = dates;
selectedDate.value = dates[0] ?? '';
}
//
async function fetchBookings(): Promise<void> {
try {
if (viewMode.value === 'date') {
//
const roomRes = await conferenceList();
console.log(roomRes);
// roomList.value = roomRes.rows || [];
// //
// const bookingPromises = roomList.value.map(room =>
// roomBookingList({
// appointmentDate: selectedDate.value,
// bookingStatus: 2,
// reviewStatus: 1,
// tbConferenceId: room.id
// })
// );
// const bookingResults = await Promise.all(bookingPromises);
// //
// bookings.value = bookingResults.flatMap(res => res.rows || []);
} else {
//
const res = await roomBookingList({
appointmentDate: selectedDate.value,
bookingStatus: 2,
reviewStatus: 1,
tbConferenceId: selectedRoom.value
});
bookings.value = res.rows || [];
}
} catch (error) {
console.error('获取预约数据失败:', error);
}
}
//
function handleViewModeChange(e: RadioChangeEvent): void {
viewMode.value = e.target.value;
if (viewMode.value === 'date') {
selectedDate.value = weekDates.value[0] ?? '';
} else {
selectedRoom.value = roomList.value[0]?.id ?? '';
}
fetchBookings();
}
//
function handleDateChange(date: string): void {
selectedDate.value = date;
fetchBookings();
}
//
function handleRoomChange(value: SelectValue): void {
selectedRoom.value = value as string;
fetchBookings();
}
//
function getCellBooking(col: string, time: string): ConferenceBooking | undefined {
if (viewMode.value === 'date') {
const room = roomList.value.find(r => r.name === col);
return bookings.value.find(b =>
b.tbConferenceId === room?.id &&
dayjs(b.startTime).format('HH:mm') === time
);
} else {
return bookings.value.find(b =>
dayjs(b.bookingDate).format('YYYY-MM-DD') === col &&
dayjs(b.startTime).format('HH:mm') === time
);
}
}
//
function renderBookingCell(booking: ConferenceBooking) {
return h('div',
{
class: 'booking-cell'
},
[
h('span',
{ class: 'booking-user' },
`预约人:${booking.bookingName}`
),
h('span',
{ class: 'booking-dept' },
`单位:${booking.deptName}`
),
h('span',
{ class: 'booking-subject' },
`主题:${booking.subject}`
)
]
);
}
//
interface TableRecord {
time: string;
key: string;
[key: string]: any;
}
const columns = computed<TableColumnType<TableRecord>[]>(() => {
const baseColumns: TableColumnType<TableRecord>[] = [{
title: '时间',
dataIndex: 'time',
width: 100,
fixed: 'left' as const
}];
const dynamicColumns: TableColumnType<TableRecord>[] = viewMode.value === 'date'
? roomList.value.map(room => ({
title: room.name,
dataIndex: room.id,
width: 200,
customRender: ({ record }) => {
const booking = getCellBooking(room.name, record.time);
return booking ? renderBookingCell(booking) : null;
}
}))
: weekDates.value.map(date => ({
title: dayjs(date).format('MM-DD'),
dataIndex: date,
width: 200,
customRender: ({ record }) => {
const booking = getCellBooking(date, record.time);
return booking ? renderBookingCell(booking) : null;
}
}));
return [...baseColumns, ...dynamicColumns];
});
//
const tableData = computed<TableRecord[]>(() => {
return timeSlots.map(time => ({
time,
key: time,
}));
});
onMounted(() => {
generateWeekDates();
fetchBookings();
});
</script>
<template>
<Page>
<div class="conference-view">
<!-- 顶部控制区 -->
<div class="control-panel">
<Radio.Group v-model:value="viewMode" @change="handleViewModeChange">
<Radio.Button value="date">按日期</Radio.Button>
<Radio.Button value="room">按会议室</Radio.Button>
</Radio.Group>
<!-- 按会议室视图的下拉选择 -->
<Select
v-if="viewMode === 'room'"
v-model:value="selectedRoom"
class="room-select"
placeholder="请选择会议室"
@change="handleRoomChange"
>
<Select.Option
v-for="room in roomList"
:key="room.id"
:value="room.id"
>
{{ room.name }}
</Select.Option>
</Select>
<!-- 按日期视图的日期选择 -->
<div v-if="viewMode === 'date'" class="date-buttons">
<Button
v-for="date in weekDates"
:key="date"
:type="date === selectedDate ? 'primary' : 'default'"
@click="handleDateChange(date)"
>
{{ dayjs(date).format('MM-DD') }}
</Button>
</div>
</div>
<!-- 日程表格 -->
<Table
:columns="columns"
:data-source="tableData"
:pagination="false"
bordered
:scroll="{ x: 'max-content' }"
/>
</div>
</Page>
</template>
<style lang="scss" scoped>
.conference-view {
padding: 16px;
.control-panel {
margin-bottom: 16px;
display: flex;
align-items: center;
.room-select {
width: 200px;
margin-left: 16px;
}
}
.date-buttons {
margin-top: 16px;
:deep(.ant-btn) {
margin-right: 8px;
&:last-child {
margin-right: 0;
}
}
}
.booking-cell {
background: #e6f7ff;
padding: 8px;
border-radius: 4px;
min-height: 60px;
display: flex;
flex-direction: column;
gap: 4px;
span {
display: block;
font-size: 12px;
line-height: 1.5;
&.booking-user {
color: #1890ff;
font-weight: 500;
}
&.booking-dept {
color: #666;
}
&.booking-subject {
color: #333;
}
}
&:hover {
background: #bae7ff;
}
}
}
</style>

View File

@ -0,0 +1,101 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { cloneDeep } from '@vben/utils';
import { useVbenForm } from '#/adapter/form';
import { roomBookingAdd, roomBookingInfo, roomBookingUpdate } from '#/api/property/roomBooking';
import { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';
import { modalSchema } from './data';
const emit = defineEmits<{ reload: [] }>();
const isUpdate = ref(false);
const title = computed(() => {
return isUpdate.value ? $t('pages.common.edit') : $t('pages.common.add');
});
const [BasicForm, formApi] = useVbenForm({
commonConfig: {
//
formItemClass: 'col-span-2',
// label px
labelWidth: 80,
//
componentProps: {
class: 'w-full',
}
},
schema: modalSchema(),
showDefaultActions: false,
wrapperClass: 'grid-cols-2',
});
const { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(
{
initializedGetter: defaultFormValueGetter(formApi),
currentGetter: defaultFormValueGetter(formApi),
},
);
const [BasicModal, modalApi] = useVbenModal({
//
class: 'w-[550px]',
fullscreenButton: false,
onBeforeClose,
onClosed: handleClosed,
onConfirm: handleConfirm,
onOpenChange: async (isOpen) => {
if (!isOpen) {
return null;
}
modalApi.modalLoading(true);
const { id } = modalApi.getData() as { id?: number | string };
isUpdate.value = !!id;
if (isUpdate.value && id) {
const record = await roomBookingInfo(id);
await formApi.setValues(record);
}
await markInitialized();
modalApi.modalLoading(false);
},
});
async function handleConfirm() {
try {
modalApi.lock(true);
const { valid } = await formApi.validate();
if (!valid) {
return;
}
// getValuesreadonly
const data = cloneDeep(await formApi.getValues());
await (isUpdate.value ? roomBookingUpdate(data) : roomBookingAdd(data));
resetInitialized();
emit('reload');
modalApi.close();
} catch (error) {
console.error(error);
} finally {
modalApi.lock(false);
}
}
async function handleClosed() {
await formApi.resetForm();
resetInitialized();
}
</script>
<template>
<BasicModal :title="title">
<BasicForm />
</BasicModal>
</template>