feat: 完成点名页面和首页

frontend/dev
Spark 1 month ago
parent 884c22c0f4
commit 858e06b1e1

@ -0,0 +1,27 @@
<template>
<div class="home">
<h1>欢迎使用 <span class="logo-text">YaYa点名</span></h1>
<p>请从左侧菜单选择功能</p>
</div>
</template>
<script setup>
</script>
<style scoped>
.home {
text-align: center;
padding: 20px;
}
h1 {
color: #303133;
margin-bottom: 20px;
}
.logo-text {
font-family: YaYa点名, sans-serif;
color: black;
font-size: 25px;
}
</style>

@ -0,0 +1,561 @@
<script setup>
import {onMounted, ref, watch} from 'vue'
import {ElMessage, ElNotification} from 'element-plus'
import {listStudent, rollCall, updatePoints} from "@/api/student.js";
//
const loading = ref(false)
const cards = ref([])
const canSelect = ref(true)
const isResetting = ref(false)
const dialogVisible = ref(false)
const cardResult = ref({})
const studentTable = ref([])
const avatarUrl = ref('https://api.aspark.cc')
const avatarKey = ref(0)
const pointChange = ref(0)
//
const shuffle = (array, count) => {
//
const shuffled = [...array];
// Fisher-Yates
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; //
}
// count count
if (shuffled.length >= count) {
return shuffled.slice(0, count);
}
//
const result = [...shuffled];
while (result.length < count) {
const randomIndex = Math.floor(Math.random() * array.length);
result.push(array[randomIndex]); //
}
return result;
};
const initCards = async () => {
const response = await rollCall()
pointChange.value = 0
cardResult.value = response.data
const totalCards = 27
const shuffledResult = shuffle(studentTable.value, totalCards)
// console.log(shuffledResult)
cards.value = shuffledResult.map(({name}) => ({name, isFlipped: false}))
avatarKey.value = Date.now()
const img = new Image()
img.src = `${avatarUrl.value}/random/${avatarKey.value}`
img.onerror = () => {
console.error('Failed to load avatar')
avatarUrl.value = 'path/to/default/avatar.png'
}
}
const selectCard = (index) => {
if (!canSelect.value || cards.value[index].isFlipped) return
cards.value[index] = {
name: cardResult.value.name,
isFlipped: true
}
canSelect.value = false
ElMessage.success(`恭喜你 ${cards.value[index].name}`)
setTimeout(() => {
cards.value.forEach((card, i) => {
if (i !== index) {
card.isFlipped = true
}
})
dialogVisible.value = true
}, 1000)
}
const handleRestart = () => {
dialogVisible.value = false
if (isResetting.value) return
isResetting.value = true
canSelect.value = false
cards.value.forEach(card => {
card.isFlipped = false
})
setTimeout(async () => {
await initCards()
canSelect.value = true
isResetting.value = false
}, 600)
}
onMounted(async () => {
loading.value = true
const response = await listStudent()
studentTable.value = response.data
await initCards()
loading.value = false
ElNotification({
title: '提示',
message: '从下方 27 张卡片中任意抽取一张吧!',
type: 'info',
})
})
const formatTooltip = (value) => {
return value > 0 ? `+${value}` : value.toString();
};
const updateSliderColor = () => {
const slider = document.querySelector('.el-slider__runway');
const sliderButton = document.querySelector('.el-slider__button');
if (!slider || !sliderButton) return;
//
if (pointChange.value < 0) {
sliderButton.style.borderColor = '#f56c6c';
} else if (pointChange.value > 0) {
sliderButton.style.borderColor = '#67c23a';
} else {
sliderButton.style.borderColor = '#409EFF'; // 0
}
//
const oldBars = slider.querySelectorAll('.custom-slider-bar');
oldBars.forEach(bar => bar.remove());
// 绿
const negativeBar = document.createElement('div');
const positiveBar = document.createElement('div');
negativeBar.className = 'custom-slider-bar negative';
positiveBar.className = 'custom-slider-bar positive';
slider.appendChild(negativeBar);
slider.appendChild(positiveBar);
const zeroPosition = 50; // 050%
const currentPosition = pointChange.value * 8 * 2 + 50;
if (pointChange.value < 0) {
// 0
negativeBar.style.left = `${currentPosition}%`;
negativeBar.style.width = `${zeroPosition - currentPosition}%`;
positiveBar.style.width = '0';
} else if (pointChange.value > 0) {
// 绿0
positiveBar.style.left = `${zeroPosition}%`;
positiveBar.style.width = `${currentPosition - zeroPosition}%`;
negativeBar.style.width = '0';
} else {
// 0
negativeBar.style.width = '0';
positiveBar.style.width = '0';
}
};
const decrementPoints = () => {
if (pointChange.value > -5) {
pointChange.value -= 0.5;
}
};
const incrementPoints = () => {
if (pointChange.value < 5) {
pointChange.value += 0.5;
}
};
watch(pointChange, updateSliderColor);
onMounted(() => {
updateSliderColor();
});
const handlePointsChange = async () => {
if (pointChange.value === 0) {
dialogVisible.value = false
return
}
const response = await updatePoints(cardResult.value.id, pointChange.value)
if (response.code === 0) {
ElMessage.error('失败,原因:' + response.msg)
}
dialogVisible.value = false
}
</script>
<template>
<el-card v-loading="loading">
<div class="lottery-container">
<div class="cards-grid">
<div
v-for="(card, index) in cards"
:key="index"
class="card"
:class="{ 'flipped': card.isFlipped }"
@click="selectCard(index)"
>
<div class="card-inner">
<div class="card-front"></div>
<div class="card-back">
<div class="card-content">
<!-- <div class="text-column">-->
<!-- <span class="vertical-text student-no">{{ card.no }}</span>-->
<!-- </div>-->
<div class="text-column">
<span class="vertical-text student-name">{{ card.name }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</el-card>
<el-dialog
v-model="dialogVisible"
width="400px"
:align-center="true"
class="profile-dialog"
@close="handleRestart"
>
<div class="profile-content">
<el-avatar
:size="100"
:src="avatarUrl + '/random/' + avatarKey"
class="profile-avatar"
/>
<h2 class="profile-name">{{ cardResult.no }}&nbsp;{{ cardResult.name }}</h2>
<h3 class="profile-points">当前积分{{ cardResult.points }}</h3>
<div class="slider-demo-block">
<div class="slider-controls">
<el-button
@click="decrementPoints"
size="small"
class="slider-button decrease"
>-</el-button>
<div class="slider-wrapper">
<el-slider
v-model="pointChange"
:min="-3"
:max="3"
:step="0.5"
show-stops
:format-tooltip="formatTooltip"
/>
</div>
<el-button
@click="incrementPoints"
size="small"
class="slider-button increase"
>+</el-button>
</div>
<div class="point-change-label">
<span>积分变动</span>
<span :class="{'positive': pointChange > 0, 'negative': pointChange < 0}">
{{ pointChange > 0 ? '+' : ''}}{{ pointChange }}
</span>
</div>
</div>
<div class="button-container">
<el-button
type="danger"
@click="handleRestart"
class="round-button"
>
<i class="iconfont yaya-xmark" style="font-size: 24px;"/>
</el-button>
<el-button
type="success"
@click="handlePointsChange"
class="round-button"
>
<i class="iconfont yaya-duigou" style="font-size: 24px;" />
</el-button>
</div>
<p class="profile-bio">Coding the World.</p>
</div>
</el-dialog>
</template>
<style>
body .el-dialog {
--el-dialog-border-radius: 20px;
}
</style>
<style scoped>
.lottery-container {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
box-sizing: border-box;
}
.cards-grid {
display: grid;
grid-template-columns: repeat(9, 1fr);
grid-template-rows: repeat(3, 1fr);
gap: 10px;
width: 100%;
max-width: 1400px;
aspect-ratio: 2.7/1;
margin: 0 auto;
}
.card {
perspective: 1000px;
cursor: pointer;
aspect-ratio: 0.7/1;
}
.card-inner {
position: relative;
width: 100%;
height: 100%;
transition: transform 0.6s;
transform-style: preserve-3d;
}
.card.flipped .card-inner {
transform: rotateY(180deg);
}
.card-front,
.card-back {
position: absolute;
width: 100%;
height: 100%;
backface-visibility: hidden;
display: flex;
justify-content: center;
align-items: center;
border-radius: 5px;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
.card-front {
background-image: url('../assets/card.webp');
}
.card-back {
background-image: url('../assets/card2.webp');
border: 1px solid #ccc;
color: black;
transform: rotateY(180deg);
font-family: '隶书', '隶书-online', '微软雅黑', sans-serif;
display: flex;
justify-content: center;
align-items: center;
}
.card-content {
display: flex;
justify-content: center;
gap: 2px;
width: 90%;
height: 90%;
}
.text-column {
display: flex;
justify-content: center;
align-items: center;
}
.vertical-text {
writing-mode: vertical-rl;
text-orientation: upright;
white-space: nowrap;
line-height: 1.2;
max-height: 100%;
overflow: hidden;
}
.text-column .student-name {
font-size: clamp(1px, 3vw, 24px);
}
@media (max-width: 768px) {
.text-column .student-name {
font-size: clamp(1px, 2.5vw, 14px);
}
.card-content {
gap: 1px;
}
}
.profile-dialog :deep(.el-dialog) {
background-color: #f5f7f5;
border-radius: 16px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.profile-content {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
}
.profile-avatar {
margin-bottom: 20px;
}
.profile-name {
color: #2c3e50;
font-size: 1.8em;
margin-bottom: 8px;
font-weight: 600;
}
.profile-points {
color: #67c23a;
font-size: 1.4em;
margin-bottom: 25px;
font-weight: 500;
}
.profile-bio {
color: #5c6b77;
font-size: 1.1em;
margin-top: 20px;
font-family: 'Nautilus-pompilius', sans-serif;
font-style: italic;
}
.slider-demo-block {
width: 100%;
margin-bottom: 40px;
}
.slider-controls {
display: flex;
align-items: center;
margin-bottom: 12px;
}
.slider-wrapper {
flex: 1;
margin: 0 15px;
}
.slider-button {
width: 32px;
height: 32px;
border: none;
color: white;
font-size: 18px;
cursor: pointer;
border-radius: 50%;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
}
.slider-button.decrease {
background-color: #f56c6c;
}
.slider-button.increase {
background-color: #67c23a;
}
.slider-button:hover {
transform: scale(1.05);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
}
.point-change-label {
text-align: center;
font-size: 16px;
color: #606266;
}
.point-change-label .positive {
color: #67c23a;
font-weight: 500;
}
.point-change-label .negative {
color: #f56c6c;
font-weight: 500;
}
:deep(.el-slider__runway) {
margin: 0;
background-color: #e4e7ed;
position: relative;
}
:deep(.custom-slider-bar) {
position: absolute;
height: 6px;
transition: all 0.3s ease;
}
:deep(.custom-slider-bar.negative) {
background-color: #f56c6c;
}
:deep(.custom-slider-bar.positive) {
background-color: #67c23a;
}
:deep(.el-slider__bar) {
display: none; /* 隐藏原始的滑块条 */
}
:deep(.el-slider__button) {
width: 20px;
height: 20px;
border: 2px solid; /* 移除固定的边框颜色 */
background-color: white;
transition: all 0.3s ease;
z-index: 3;
}
:deep(.el-slider__button:hover) {
transform: scale(1.1);
}
:deep(.el-slider__stop) {
background-color: transparent;
}
.button-container {
display: flex;
justify-content: center;
gap: 20px; /* 可根据需要调整间距 */
}
.round-button {
border-radius: 50%;
width: 50px; /* 根据需要调整大小 */
height: 50px; /* 根据需要调整大小 */
display: flex;
align-items: center;
justify-content: center;
}
</style>
Loading…
Cancel
Save