<template>
|
<view class="meeting-list">
|
<PageHeader title="会议列表"
|
@back="goBack" />
|
<!-- 查询表单 -->
|
<view class="search-section">
|
<view class="search-bar">
|
<view class="search-input">
|
<up-input class="search-text"
|
placeholder="查询日期"
|
@click.stop="showDatePicker"
|
v-model="queryForm.meetingDate"
|
clearable />
|
</view>
|
<view class="filter-button"
|
@click="clearDate">
|
<u-icon name="close-circle-fill"
|
size="24"
|
color="#999"></u-icon>
|
</view>
|
</view>
|
</view>
|
<!-- 日期选择器 -->
|
<up-datetime-picker v-model="datePickerValue"
|
mode="date"
|
:show="showDatePickerDialog"
|
@confirm="handleDateConfirm"
|
@cancel="showDatePickerDialog = false"
|
format="YYYY-MM-DD"
|
value-format="YYYY-MM-DD" />
|
<!-- 会议室使用情况 -->
|
<view class="table-container">
|
<scroll-view scroll-x="true"
|
style="width: 100%;">
|
<view class="time-table">
|
<!-- 表头 -->
|
<view class="table-header">
|
<view class="header-cell room-header">会议室</view>
|
<view v-for="timeSlot in timeSlots"
|
:key="timeSlot.value"
|
class="header-cell time-header">
|
{{ timeSlot.label }}
|
</view>
|
</view>
|
<!-- 表格内容 -->
|
<view class="table-body">
|
<view v-for="room in roomUsage"
|
:key="room.id"
|
class="table-row">
|
<view class="cell room-cell">{{ room.name }}</view>
|
<view class="cells-container">
|
<template v-for="(cell, index) in generateMeetingCells(room)"
|
:key="index">
|
<view class="cell content-cell"
|
:class="[cell.type, `status-${cell.meeting?.status || '0'}`]"
|
:style="{ width: `${cell.span * 120}rpx` }"
|
@click="viewMeetingDetails(cell)">
|
<view v-if="cell.type === 'meeting'"
|
class="meeting-content">
|
<view class="meeting-title">{{ cell.meeting.title }}</view>
|
<view class="meeting-time">{{ cell.startTime }}-{{ cell.endTime }}</view>
|
</view>
|
<view v-else
|
class="free-content">
|
空闲
|
</view>
|
</view>
|
</template>
|
</view>
|
</view>
|
</view>
|
</view>
|
</scroll-view>
|
</view>
|
<!-- 会议详情对话框 -->
|
<u-popup :show="detailDialogVisible"
|
mode="center"
|
customStyle="width: 80%;"
|
:round="10">
|
<view class="dialog-content">
|
<view class="dialog-header">
|
<text class="dialog-title">会议详情</text>
|
<up-icon name="close"
|
@click="detailDialogVisible = false"
|
class="close-icon"></up-icon>
|
</view>
|
<view v-if="currentMeeting"
|
class="dialog-body">
|
<view class="detail-item">
|
<text class="detail-label">会议主题</text>
|
<text class="detail-value">{{ currentMeeting.title }}</text>
|
</view>
|
<view class="detail-item">
|
<text class="detail-label">会议室</text>
|
<text class="detail-value">{{ currentMeeting.room }}</text>
|
</view>
|
<view class="detail-item">
|
<text class="detail-label">会议时间</text>
|
<text class="detail-value">{{ currentMeeting.time }}</text>
|
</view>
|
<view class="detail-item">
|
<text class="detail-label">主持人</text>
|
<text class="detail-value">{{ currentMeeting.host }}</text>
|
</view>
|
<view class="detail-item">
|
<text class="detail-label">参会人数</text>
|
<text class="detail-value">{{ currentMeeting.participants }}人</text>
|
</view>
|
<view class="detail-item">
|
<text class="detail-label">会议说明</text>
|
<text class="detail-value">{{ currentMeeting.description }}</text>
|
</view>
|
</view>
|
<view class="dialog-footer">
|
<u-button @click="detailDialogVisible = false">关闭</u-button>
|
</view>
|
</view>
|
</u-popup>
|
</view>
|
</template>
|
|
<script setup>
|
import { ref, reactive, onMounted } from "vue";
|
import PageHeader from "@/components/PageHeader.vue";
|
import { getMeetingUseList } from "@/api/managementMeetings/meeting.js";
|
import dayjs from "dayjs";
|
|
// 查询表单
|
const queryForm = reactive({
|
meetingDate: dayjs().format("YYYY-MM-DD"),
|
});
|
|
const loading = ref(false);
|
const timeSlots = ref([]);
|
const roomUsage = ref([]);
|
const currentMeeting = ref(null);
|
const detailDialogVisible = ref(false);
|
const showDatePickerDialog = ref(false);
|
const datePickerValue = ref(Date.now());
|
|
// 返回上一页
|
const goBack = () => {
|
uni.navigateBack();
|
};
|
|
// 清空日期
|
const clearDate = () => {
|
datePickerValue.value = Date.now();
|
queryForm.meetingDate = dayjs().format("YYYY-MM-DD");
|
getList();
|
};
|
|
// 显示日期选择器
|
const showDatePicker = () => {
|
if (queryForm.meetingDate) {
|
datePickerValue.value = new Date(queryForm.meetingDate).getTime();
|
} else {
|
datePickerValue.value = Date.now();
|
}
|
console.log(datePickerValue.value, "datePickerValue.value");
|
showDatePickerDialog.value = true;
|
};
|
|
// 处理日期选择确认
|
const handleDateConfirm = value => {
|
// dayjs().format("YYYY-MM-DD")
|
console.log(value, "value");
|
|
queryForm.meetingDate = dayjs(value.value).format("YYYY-MM-DD");
|
showDatePickerDialog.value = false;
|
getList();
|
};
|
|
// 获取列表数据
|
const getList = () => {
|
handleSearch();
|
};
|
|
// 初始化时间槽(以半小时为间隔,从8:00到17:30)
|
const initTimeSlots = () => {
|
const slots = [];
|
// 生成8:00到17:00的时间段
|
for (let hour = 8; hour <= 17; hour++) {
|
// 添加整点
|
slots.push({
|
label: `${hour.toString().padStart(2, "0")}:00`,
|
value: `${hour.toString().padStart(2, "0")}:00`,
|
});
|
|
// 添加半点,直到17:30
|
if (hour <= 17) {
|
slots.push({
|
label: `${hour.toString().padStart(2, "0")}:30`,
|
value: `${hour.toString().padStart(2, "0")}:30`,
|
});
|
}
|
}
|
// 移除最后一个18:00的时间段
|
if (slots.length > 0 && slots[slots.length - 1].value === "18:00") {
|
slots.pop();
|
}
|
timeSlots.value = slots;
|
console.log(timeSlots.value, "timeSlots.value");
|
};
|
|
// 生成会议室的时间单元格
|
const generateMeetingCells = room => {
|
const cells = [];
|
const meetings = room.meetings || [];
|
const occupiedSlots = new Set();
|
|
// 处理每个会议
|
for (const meeting of meetings) {
|
const startIdx = timeSlots.value.findIndex(
|
slot => slot.value === meeting.startTime
|
);
|
let endIdx = timeSlots.value.findIndex(
|
slot => slot.value === meeting.endTime
|
);
|
if (endIdx === -1) {
|
endIdx = timeSlots.value.length;
|
}
|
|
if (startIdx !== -1) {
|
// 标记被占用的时间段
|
for (let i = startIdx; i < endIdx; i++) {
|
if (timeSlots.value[i]) {
|
occupiedSlots.add(timeSlots.value[i].value);
|
}
|
}
|
|
// 创建会议单元格
|
cells.push({
|
type: "meeting",
|
meeting: meeting,
|
span: endIdx - startIdx,
|
startTime: meeting.startTime,
|
endTime: meeting.endTime,
|
});
|
}
|
}
|
|
// 处理空闲时间段
|
for (let i = 0; i < timeSlots.value.length; i++) {
|
const slot = timeSlots.value[i];
|
if (!occupiedSlots.has(slot.value)) {
|
// 查找连续的空闲时间段
|
let span = 1;
|
while (
|
i + span < timeSlots.value.length &&
|
!occupiedSlots.has(timeSlots.value[i + span].value)
|
) {
|
occupiedSlots.add(timeSlots.value[i + span].value);
|
span++;
|
}
|
|
cells.push({
|
type: "free",
|
span: span,
|
time: slot.value,
|
});
|
}
|
}
|
|
// 按时间排序
|
cells.sort((a, b) => {
|
const timeA = a.startTime || a.time;
|
const timeB = b.startTime || b.time;
|
return (
|
timeSlots.value.findIndex(s => s.value === timeA) -
|
timeSlots.value.findIndex(s => s.value === timeB)
|
);
|
});
|
console.log(cells, "cells");
|
return cells;
|
};
|
|
// 查看会议详情
|
const viewMeetingDetails = cell => {
|
console.log(cell, "cell");
|
|
if (cell && cell.type === "meeting") {
|
currentMeeting.value = cell.meeting;
|
detailDialogVisible.value = true;
|
} else {
|
uni.showToast({
|
title: "该时间段会议室空闲",
|
icon: "info",
|
});
|
}
|
};
|
|
// 查询按钮操作
|
const handleSearch = async () => {
|
loading.value = true;
|
try {
|
const resp = await getMeetingUseList({ ...queryForm });
|
roomUsage.value = resp.data;
|
} catch (error) {
|
uni.showToast({
|
title: "获取数据失败",
|
icon: "error",
|
});
|
} finally {
|
loading.value = false;
|
}
|
};
|
|
// 重置搜索表单
|
const resetSearch = () => {
|
queryForm.meetingDate = dayjs().format("YYYY-MM-DD");
|
};
|
|
// 页面加载时获取数据
|
onMounted(() => {
|
// 初始化时间槽
|
initTimeSlots();
|
|
// 默认查询今天的数据
|
handleSearch();
|
});
|
</script>
|
|
<style scoped lang="scss">
|
@import "../../../styles/sales-common.scss";
|
.meeting-list {
|
min-height: 100vh;
|
background: #f8f9fa;
|
padding: 16rpx;
|
}
|
|
.search-section {
|
background: #fff;
|
padding: 16rpx;
|
border-radius: 8rpx;
|
margin-bottom: 16rpx;
|
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
}
|
|
.search-bar {
|
display: flex;
|
align-items: center;
|
gap: 12rpx;
|
}
|
|
.search-input {
|
flex: 1;
|
}
|
|
.filter-button {
|
padding: 12rpx 16rpx;
|
background: #f5f7fa;
|
border-radius: 4rpx;
|
}
|
|
.form-buttons {
|
display: flex;
|
gap: 12rpx;
|
margin-top: 16rpx;
|
justify-content: center;
|
}
|
|
.table-container {
|
margin-top: 16rpx;
|
overflow-x: auto;
|
}
|
|
.time-table {
|
width: 100%;
|
}
|
|
.table-header {
|
display: flex;
|
border: 1rpx solid #e4e7ed;
|
background: #f5f7fa;
|
}
|
|
.header-cell {
|
padding: 12rpx 8rpx;
|
text-align: center;
|
font-weight: bold;
|
border-right: 1rpx solid #e4e7ed;
|
}
|
|
.room-header {
|
width: 120rpx;
|
flex-shrink: 0;
|
}
|
|
.time-header {
|
width: 120rpx;
|
flex-shrink: 0;
|
}
|
|
.table-body {
|
border: 1rpx solid #e4e7ed;
|
border-top: none;
|
}
|
|
.table-row {
|
display: flex;
|
border-top: 1rpx solid #e4e7ed;
|
}
|
|
.table-row:first-child {
|
border-top: none;
|
}
|
|
.cell {
|
padding: 16rpx 8rpx;
|
text-align: center;
|
border-right: 1rpx solid #e4e7ed;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
word-break: break-word;
|
line-height: 1.2;
|
}
|
|
.room-cell {
|
width: 120rpx;
|
font-weight: bold;
|
flex-shrink: 0;
|
background: #f9fafc;
|
}
|
|
.cells-container {
|
display: flex;
|
}
|
|
.content-cell {
|
min-height: 120rpx;
|
cursor: pointer;
|
transition: all 0.3s;
|
display: flex;
|
flex-direction: column;
|
justify-content: center;
|
padding: 16rpx;
|
box-sizing: border-box;
|
}
|
|
.content-cell:active {
|
opacity: 0.8;
|
}
|
|
.free {
|
color: #f56c6c;
|
}
|
|
.meeting {
|
display: flex;
|
flex-direction: column;
|
justify-content: center;
|
}
|
|
.status-1 {
|
background-color: #fef0f0;
|
color: #d14646;
|
}
|
|
.status-0 {
|
background-color: #ecf5ff;
|
color: #409eff;
|
}
|
|
.meeting-content {
|
width: 100%;
|
}
|
|
.meeting-title {
|
font-weight: bold;
|
margin-bottom: 8rpx;
|
font-size: 24rpx;
|
}
|
|
.meeting-time {
|
font-size: 20rpx;
|
opacity: 0.8;
|
}
|
|
.free-content {
|
color: #909399;
|
}
|
|
/* 对话框样式 */
|
.dialog-content {
|
padding: 24rpx;
|
}
|
|
.dialog-header {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
margin-bottom: 24rpx;
|
padding-bottom: 16rpx;
|
border-bottom: 1rpx solid #e4e7ed;
|
}
|
|
.dialog-title {
|
font-size: 32rpx;
|
font-weight: bold;
|
color: #303133;
|
}
|
|
.close-icon {
|
font-size: 32rpx;
|
color: #909399;
|
}
|
|
.dialog-body {
|
margin-bottom: 24rpx;
|
}
|
|
.detail-item {
|
display: flex;
|
margin-bottom: 16rpx;
|
padding: 8rpx 0;
|
border-bottom: 1rpx solid #f0f0f0;
|
}
|
|
.detail-label {
|
width: 140rpx;
|
font-weight: bold;
|
color: #606266;
|
}
|
|
.detail-value {
|
flex: 1;
|
color: #303133;
|
}
|
|
.dialog-footer {
|
display: flex;
|
justify-content: center;
|
margin-top: 16rpx;
|
}
|
</style>
|