You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
486 lines
12 KiB
486 lines
12 KiB
<template>
|
|
<div class="seat-selector">
|
|
<div class="seats-container">
|
|
<div v-if="rows.length === 0" class="no-seats-message">
|
|
没有可用座位,请确认场地容量设置
|
|
</div>
|
|
<div v-else v-for="row in rows" :key="row" class="seat-row">
|
|
<div
|
|
v-for="col in cols"
|
|
:key="`${row}-${col}`"
|
|
class="seat-container"
|
|
>
|
|
<div
|
|
class="seat"
|
|
:class="{
|
|
'seat-available': isSeatAvailable(row, col),
|
|
'seat-occupied': !isSeatAvailable(row, col) && isSeatValid(row, col),
|
|
'seat-selected': isSelected(row, col),
|
|
'seat-hidden': !isSeatValid(row, col)
|
|
}"
|
|
@click="toggleSeat(row, col)"
|
|
>
|
|
{{ col }}
|
|
</div>
|
|
<div v-if="isSelected(row, col)" class="seat-name">
|
|
{{ getSeatObject(row, col)?.name || '' }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="seat-legend">
|
|
<div class="legend-item">
|
|
<div class="legend-box seat-available"></div>
|
|
<span>可选</span>
|
|
</div>
|
|
<div class="legend-item">
|
|
<div class="legend-box seat-occupied"></div>
|
|
<span>已占</span>
|
|
</div>
|
|
<div class="legend-item">
|
|
<div class="legend-box seat-selected"></div>
|
|
<span>已选</span>
|
|
</div>
|
|
</div>
|
|
<div class="seat-info">
|
|
<span>已选: {{ selectedSeats.length }}/{{ props.maxSelections === Infinity ? '不限' : props.maxSelections }}</span>
|
|
<span>剩余空位: {{ availableSeats.length }}</span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, watch, onMounted } from 'vue';
|
|
import { getClassroompeople } from '@/app/api/classroom';
|
|
import { getAllVenueList } from '@/app/api/venue';
|
|
|
|
const props = defineProps({
|
|
venueId: {
|
|
type: [Number, String],
|
|
default: null
|
|
},
|
|
capacity: {
|
|
type: Number,
|
|
default: 0
|
|
},
|
|
occupiedSeats: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
selectedSeatIds: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
maxSelections: {
|
|
type: Number,
|
|
default: Infinity
|
|
}
|
|
});
|
|
|
|
const emit = defineEmits(['update:selectedSeats', 'change', 'update:selectedSeatIds']);
|
|
|
|
// 位置布局配置
|
|
const rows = ref([]);
|
|
const cols = ref([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); // 固定每行10个座位
|
|
const selectedSeats = ref([]);
|
|
const occupiedSeatsData = ref([]);
|
|
const selectedSeatObjects = ref([]);
|
|
const totalSeats = ref(0);
|
|
|
|
// 调整位置行列数以匹配容量 - 不再使用该函数,因为我们在onMounted和watch中直接设置了
|
|
const adjustLayout = (capacityValue) => {
|
|
console.log('已废弃的adjustLayout调用,使用值:', capacityValue);
|
|
};
|
|
|
|
// 加载场地信息和已占用位置数据
|
|
const loadVenueInfoAndOccupiedSeats = async () => {
|
|
if (!props.venueId) return;
|
|
|
|
try {
|
|
// 如果有传入的已占用位置,直接使用
|
|
if (props.occupiedSeats && props.occupiedSeats.length) {
|
|
occupiedSeatsData.value = props.occupiedSeats;
|
|
} else {
|
|
// 否则尝试从API获取
|
|
const response = await getClassroompeople(props.venueId);
|
|
if (response.data && Array.isArray(response.data)) {
|
|
// 假设API返回的是已占用位置的数组
|
|
occupiedSeatsData.value = response.data;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('获取已占用位置失败:', error);
|
|
// 模拟一些随机占用的位置
|
|
occupiedSeatsData.value = generateRandomOccupiedSeats();
|
|
}
|
|
};
|
|
|
|
// 生成一些随机占用的位置(用于演示)
|
|
const generateRandomOccupiedSeats = () => {
|
|
const occupied = [];
|
|
const totalSeats = rows.value.length * cols.value.length;
|
|
const occupiedCount = Math.floor(totalSeats * 0.3); // 30%的座位被占用
|
|
|
|
for (let i = 0; i < occupiedCount; i++) {
|
|
const row = rows.value[Math.floor(Math.random() * rows.value.length)];
|
|
const col = cols.value[Math.floor(Math.random() * cols.value.length)];
|
|
const seat = `${row}-${col}`;
|
|
if (!occupied.includes(seat)) {
|
|
occupied.push(seat);
|
|
}
|
|
}
|
|
|
|
return occupied;
|
|
};
|
|
|
|
// 计算所有有效座位
|
|
const allValidSeats = computed(() => {
|
|
const seats = [];
|
|
if (rows.value.length === 0) return seats;
|
|
|
|
rows.value.forEach(row => {
|
|
cols.value.forEach(col => {
|
|
if (isSeatValid(row, col)) {
|
|
const seatId = `${row}-${col}`;
|
|
seats.push(seatId);
|
|
}
|
|
});
|
|
});
|
|
|
|
return seats;
|
|
});
|
|
|
|
// 计算可用座位(未被占用的有效座位)
|
|
const availableSeats = computed(() => {
|
|
const seats = [];
|
|
|
|
allValidSeats.value.forEach(seatId => {
|
|
if (!occupiedSeatsData.value.includes(seatId)) {
|
|
seats.push(seatId);
|
|
}
|
|
});
|
|
|
|
console.log('有效座位总数:', allValidSeats.value.length);
|
|
console.log('已占用座位数:', occupiedSeatsData.value.length);
|
|
console.log('可用座位数:', seats.length);
|
|
console.log('已选座位数:', selectedSeats.value.length);
|
|
|
|
return seats;
|
|
});
|
|
|
|
// 检查座位是否有效(在总座位数范围内)
|
|
const isSeatValid = (row, col) => {
|
|
const seatIndex = (row.charCodeAt(0) - 65) * 10 + (col - 1);
|
|
return seatIndex < totalSeats.value;
|
|
};
|
|
|
|
// 检查座位是否可用(未被占用且有效)
|
|
const isSeatAvailable = (row, col) => {
|
|
// 先检查座位是否有效
|
|
if (!isSeatValid(row, col)) {
|
|
return false;
|
|
}
|
|
|
|
const seatId = `${row}-${col}`;
|
|
return !occupiedSeatsData.value.includes(seatId);
|
|
};
|
|
|
|
// 检查座位是否被选中
|
|
const isSelected = (row, col) => {
|
|
const seatId = `${row}-${col}`;
|
|
return selectedSeats.value.includes(seatId);
|
|
};
|
|
|
|
// 获取座位对应的选中对象
|
|
const getSeatObject = (row, col) => {
|
|
const seatId = `${row}-${col}`;
|
|
return selectedSeatObjects.value.find(obj => obj.id === seatId);
|
|
};
|
|
|
|
// 切换座位选择状态
|
|
const toggleSeat = (row, col) => {
|
|
console.log('点击座位:', row, col);
|
|
|
|
// 检查座位是否有效
|
|
if (!isSeatValid(row, col)) {
|
|
console.log('座位无效,不能选择');
|
|
return;
|
|
}
|
|
|
|
const seatId = `${row}-${col}`;
|
|
|
|
// 检查座位是否可用
|
|
if (!isSeatAvailable(row, col)) {
|
|
console.log('座位已占用,不能选择');
|
|
return;
|
|
}
|
|
|
|
const index = selectedSeats.value.indexOf(seatId);
|
|
if (index === -1) {
|
|
// 检查是否已达到最大选择数量
|
|
if (selectedSeats.value.length >= props.maxSelections) {
|
|
console.log('已达到最大选择数量:', props.maxSelections);
|
|
return;
|
|
}
|
|
|
|
console.log('添加座位:', seatId);
|
|
selectedSeats.value.push(seatId);
|
|
|
|
// 检查是否有对应的外部选中对象,没有则创建一个新的
|
|
if (!selectedSeatObjects.value.some(obj => obj.id === seatId)) {
|
|
selectedSeatObjects.value.push({ id: seatId, name: '' });
|
|
}
|
|
} else {
|
|
console.log('移除座位:', seatId);
|
|
selectedSeats.value.splice(index, 1);
|
|
|
|
// 移除对应的外部选中对象
|
|
const objIndex = selectedSeatObjects.value.findIndex(obj => obj.id === seatId);
|
|
if (objIndex !== -1) {
|
|
selectedSeatObjects.value.splice(objIndex, 1);
|
|
}
|
|
}
|
|
|
|
console.log('选中座位:', selectedSeats.value);
|
|
emit('update:selectedSeats', selectedSeats.value);
|
|
emit('update:selectedSeatIds', selectedSeatObjects.value);
|
|
emit('change', selectedSeats.value);
|
|
};
|
|
|
|
|
|
|
|
// 添加场地列表数据
|
|
const venueList = ref([]);
|
|
|
|
// 加载场地列表
|
|
const loadVenueList = async () => {
|
|
try {
|
|
const response = await getAllVenueList({});
|
|
if (response.data && Array.isArray(response.data)) {
|
|
venueList.value = response.data;
|
|
}
|
|
} catch (error) {
|
|
console.error('获取场地列表失败:', error);
|
|
}
|
|
};
|
|
|
|
// 获取当前场地信息
|
|
const getCurrentVenueInfo = () => {
|
|
const venue = venueList.value.find(venue => venue.id === props.venueId) || null;
|
|
console.log('获取场地信息:', venue, '场地ID:', props.venueId, '场地列表:', venueList.value);
|
|
return venue;
|
|
};
|
|
|
|
// 监听属性变化
|
|
watch(() => props.venueId, async () => {
|
|
console.log('监听venueId变化:', props.venueId);
|
|
selectedSeats.value = [];
|
|
selectedSeatObjects.value = [];
|
|
|
|
// 重新设置座位布局
|
|
if (props.capacity) {
|
|
console.log('使用props容量:', props.capacity);
|
|
totalSeats.value = props.capacity;
|
|
const rowCount = Math.ceil(props.capacity / 10);
|
|
rows.value = Array.from({ length: rowCount }, (_, i) =>
|
|
String.fromCharCode(65 + i)
|
|
).slice(0, 26);
|
|
} else {
|
|
// 设置默认值
|
|
totalSeats.value = 30;
|
|
const rowCount = 3;
|
|
rows.value = Array.from({ length: rowCount }, (_, i) =>
|
|
String.fromCharCode(65 + i)
|
|
);
|
|
}
|
|
|
|
console.log('更新布局 - 总座位数:', totalSeats.value, '行数:', rows.value.length);
|
|
|
|
// 加载已占用座位
|
|
loadVenueInfoAndOccupiedSeats();
|
|
});
|
|
|
|
// 监听容量变化
|
|
watch(() => props.capacity, (newCapacity) => {
|
|
console.log('监听capacity变化:', newCapacity);
|
|
|
|
// 更新座位布局
|
|
if (newCapacity || newCapacity === 0) {
|
|
totalSeats.value = newCapacity;
|
|
const rowCount = Math.ceil(newCapacity / 10);
|
|
rows.value = Array.from({ length: rowCount }, (_, i) =>
|
|
String.fromCharCode(65 + i)
|
|
).slice(0, 26);
|
|
console.log('根据新容量更新布局 - 总座位数:', totalSeats.value, '行数:', rows.value.length);
|
|
}
|
|
});
|
|
|
|
watch(() => props.occupiedSeats, () => {
|
|
occupiedSeatsData.value = props.occupiedSeats;
|
|
});
|
|
|
|
// 监听外部传入的selectedSeatIds变化
|
|
watch(() => props.selectedSeatIds, (newValue) => {
|
|
if (newValue && Array.isArray(newValue)) {
|
|
selectedSeatObjects.value = [...newValue];
|
|
selectedSeats.value = newValue.map(item => item.id);
|
|
}
|
|
}, { immediate: true });
|
|
|
|
// 组件挂载时初始化
|
|
onMounted(async () => {
|
|
console.log('组件挂载 - venueId:', props.venueId, 'capacity:', props.capacity);
|
|
|
|
// 确保有默认值
|
|
if (!props.capacity && props.capacity !== 0) {
|
|
totalSeats.value = 30;
|
|
// 生成默认行数
|
|
const rowCount = Math.ceil(totalSeats.value / 10);
|
|
rows.value = Array.from({ length: rowCount }, (_, i) =>
|
|
String.fromCharCode(65 + i)
|
|
);
|
|
console.log('使用默认设置 - 总座位数:', totalSeats.value, '行数:', rows.value.length);
|
|
} else {
|
|
console.log('使用props容量:', props.capacity);
|
|
totalSeats.value = props.capacity;
|
|
// 计算需要的行数
|
|
const rowCount = Math.ceil(props.capacity / 10);
|
|
// 生成行标识
|
|
rows.value = Array.from({ length: rowCount }, (_, i) =>
|
|
String.fromCharCode(65 + i)
|
|
).slice(0, 26);
|
|
console.log('根据容量设置 - 总座位数:', totalSeats.value, '行数:', rows.value.length);
|
|
}
|
|
|
|
// 加载已占用座位
|
|
loadVenueInfoAndOccupiedSeats();
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
.seat-selector {
|
|
width: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
padding: 20px;
|
|
}
|
|
|
|
.screen {
|
|
width: 80%;
|
|
height: 30px;
|
|
background: linear-gradient(to bottom, #ccc, #f5f5f5);
|
|
margin-bottom: 30px;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
border-radius: 5px;
|
|
}
|
|
|
|
.screen-text {
|
|
font-size: 14px;
|
|
color: #666;
|
|
}
|
|
|
|
.seats-container {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.seat-row {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 10px;
|
|
}
|
|
|
|
.row-label {
|
|
width: 20px;
|
|
text-align: center;
|
|
font-weight: bold;
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.seat-container {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 3px;
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
.seat {
|
|
width: 30px;
|
|
height: 30px;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
border-radius: 5px;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.seat-name {
|
|
font-size: 10px;
|
|
max-width: 40px;
|
|
text-align: center;
|
|
word-break: break-all;
|
|
color: #333;
|
|
height: 24px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.seat-available {
|
|
background-color: #67c23a;
|
|
color: white;
|
|
}
|
|
|
|
.seat-occupied {
|
|
background-color: #909399;
|
|
color: white;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.seat-selected {
|
|
background-color: #409eff;
|
|
color: white;
|
|
}
|
|
|
|
.seat-hidden {
|
|
visibility: hidden;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.seat-legend {
|
|
display: flex;
|
|
gap: 20px;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.legend-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
}
|
|
|
|
.legend-box {
|
|
width: 15px;
|
|
height: 15px;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.seat-info {
|
|
display: flex;
|
|
gap: 20px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.no-seats-message {
|
|
font-size: 14px;
|
|
color: #666;
|
|
padding: 20px;
|
|
text-align: center;
|
|
}
|
|
</style>
|