AI实验室前端

This commit is contained in:
BBIT-Kai
2026-02-04 13:56:52 +08:00
parent 892cb2494e
commit f9536dd0b4
24 changed files with 2987 additions and 63 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 291 KiB

@@ -0,0 +1,703 @@
<script setup>
import { nextTick, onMounted, reactive, ref, watch } from 'vue';
import * as api from '#/api';
const items = reactive([]);
const showModal = ref(false);
const selected = ref(null);
// 画布 refs & confetti state
const confettiCanvas = ref(null);
let confettiAnim = null;
const openItem = async (item) => {
try {
// 调用接口更新数据库
await api.openLotteryItem(item.id);
// 本地状态更新
item.is_opened = true;
selected.value = item;
await nextTick();
showModal.value = true;
} catch (error) {
console.error('标记奖品已开启失败', error);
}
};
const handleClick = (item) => {
if (item.is_opened) {
selected.value = item;
showModal.value = true;
nextTick(() => startConfetti());
} else {
openItem(item);
}
};
const closeModal = () => {
showModal.value = false;
stopConfetti();
selected.value = null;
};
const startConfetti = () => {
const canvas = confettiCanvas.value;
if (!canvas) return;
const ctx = canvas.getContext('2d');
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
const W = canvas.width;
const H = canvas.height;
const particles = [];
const colors = ['#ffd400', '#ff4d4f', '#ff8a00', '#ffe29a', '#b21f2d'];
for (let i = 0; i < 120; i++) {
particles.push({
x: Math.random() * W,
y: Math.random() * H - H,
vx: (Math.random() - 0.5) * 6,
vy: Math.random() * 6 + 2,
size: Math.random() * 6 + 4,
color: colors[Math.floor(Math.random() * colors.length)],
tilt: Math.random() * 0.3,
angle: Math.random() * Math.PI * 2,
});
}
let last = performance.now();
const loop = (t) => {
const dt = (t - last) / 1000;
last = t;
ctx.clearRect(0, 0, W, H);
for (const p of particles) {
p.x += p.vx * dt * 60;
p.y += p.vy * dt * 60;
p.angle += p.tilt * 0.2;
ctx.save();
ctx.translate(p.x, p.y);
ctx.rotate(p.angle);
ctx.fillStyle = p.color;
ctx.fillRect(-p.size / 2, -p.size / 2, p.size, p.size * 0.6);
ctx.restore();
if (p.y > H + 50) {
p.y = -20;
p.x = Math.random() * W;
}
}
confettiAnim = requestAnimationFrame(loop);
};
if (confettiAnim) cancelAnimationFrame(confettiAnim);
confettiAnim = requestAnimationFrame(loop);
};
const stopConfetti = () => {
if (confettiAnim) cancelAnimationFrame(confettiAnim);
confettiAnim = null;
const canvas = confettiCanvas.value;
if (canvas) {
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
};
watch(showModal, (v) => {
if (v) setTimeout(() => startConfetti(), 200);
else stopConfetti();
});
onMounted(async () => {
try {
const res = await api.getLotteryList();
if (Array.isArray(res)) {
items.splice(0, items.length, ...res);
}
} catch (error) {
console.error('获取礼品列表失败', error);
}
});
</script>
<template>
<div>
<div class="lucky-page">
<!-- 顶部标题 -->
<div style="background-color: #e32c2c; width: 120%; margin-bottom: 20px">
<header class="title">
<h1 class="art-title">2025奥立年会暨主干信息十周年庆典</h1>
</header>
</div>
<!-- 网格容器 -->
<main class="grid-wrap">
<div class="grid">
<div
v-for="item in items"
:key="item.id"
class="cell"
:class="{ opened: item.is_opened }"
@click="handleClick(item)"
role="button"
:aria-pressed="item.is_opened"
>
<!-- 门面未开状态 -->
<div class="red-envelope" v-if="!item.is_opened">
<div class="envelope-top"></div>
<div class="envelope-body"></div>
<div class="label">
<div class="id">{{ item.sort }}</div>
</div>
</div>
<!-- 打开状态显示奖品 -->
<div class="prize" v-else>
<img :src="item.oss_url" :alt="item.name" />
<div class="prize-name">{{ item.name }}</div>
<div class="tag">已开</div>
</div>
</div>
</div>
</main>
<!-- 自定义模态窗 -->
<transition name="modal-fade">
<div class="modal-overlay" v-if="showModal" @click.self="closeModal">
<div class="modal celebratory">
<div class="modal-body">
<div class="modal-left">
<img
:src="selected?.oss_url"
:alt="selected?.name"
class="prize-img"
/>
<canvas ref="confettiCanvas" class="confetti-canvas"></canvas>
</div>
<div class="modal-right">
<h2 class="modal-title">恭喜抽中</h2>
<h3 class="prize-title">{{ selected?.name }}</h3>
<p class="modal-msg">请上台领奖</p>
</div>
</div>
<!-- 礼花画布 -->
<canvas ref="confettiCanvas" class="confetti-canvas"></canvas>
</div>
</div>
</transition>
</div>
</div>
</template>
<style scoped>
/* 配色变量(传统中国喜庆) */
:root {
--red-deep: #b21f2d;
--red: #c8161d;
--gold: #ffde00;
--warm: #ffb94a;
--bg1: #c8161d;
--bg2: #ffd400;
--glass: rgba(255, 255, 255, 0.06);
}
/* 页面整体背景:红黄渐变 */
.lucky-page {
/* 主题变量 */
--red-deep: #b21f2d;
--red: #c8161d;
--gold: #ffde00;
--warm: #ffb94a;
--bg1: #c8161d;
--bg2: #ffd400;
--bg3: #ff6a00;
min-height: 100vh;
padding: 28px;
box-sizing: border-box;
/* 多层混合渐变 */
background: linear-gradient(
120deg,
var(--bg1),
var(--bg3),
var(--bg2),
var(--bg1)
);
background-size: 400% 400%;
animation: luckyGradientFlow 25s ease-in-out infinite;
display: flex;
flex-direction: column;
align-items: center;
}
@keyframes luckyGradientFlow {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
/* 标题区 */
.title {
text-align: center;
margin-bottom: 5px;
margin-top: 5px;
filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.35));
}
.title h1 {
font-size: 33px;
margin: 0;
letter-spacing: 1px;
color: #fff;
}
.subtitle {
margin: 6px 0 0;
font-size: 13px;
color: rgba(255, 255, 255, 0.92);
}
/* 内容主区容器(限制宽度使整体更高级) */
.grid-wrap {
width: 100%;
max-width: 1200px;
flex: 1 1 auto;
display: flex;
align-items: flex-start;
justify-content: center;
padding: 12px;
box-sizing: border-box;
}
/* Grid:自动填充,方格自适应,保持方形(aspect-ratio */
.grid {
width: 100%;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(84px, 1fr));
gap: 12px;
}
/* 单个方格样式 */
.cell {
position: relative;
aspect-ratio: 1 / 1;
border-radius: 8px;
overflow: hidden;
cursor: pointer;
transition:
transform 220ms cubic-bezier(0.2, 0.9, 0.3, 1),
box-shadow 220ms;
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.22);
background: linear-gradient(
180deg,
rgba(255, 255, 255, 0.02),
rgba(0, 0, 0, 0.03)
);
display: flex;
align-items: center;
justify-content: center;
}
/* 点击反馈 */
.cell:active {
transform: translateY(1px) scale(0.997);
}
/* 门面未开状态 */
.door {
width: 100%;
height: 100%;
position: relative;
}
/* 左右门板 */
.door-left,
.door-right {
position: absolute;
top: 0;
bottom: 0;
width: 50%;
background: linear-gradient(
180deg,
rgba(255, 255, 255, 0.02),
rgba(0, 0, 0, 0.06)
);
border: 2px solid rgba(255, 255, 255, 0.06);
box-sizing: border-box;
transition:
transform 700ms cubic-bezier(0.2, 0.9, 0.3, 1),
box-shadow 700ms;
transform-origin: left center;
backface-visibility: hidden;
}
/* 右侧以右为原点 */
.door-right {
right: 0;
left: auto;
transform-origin: right center;
}
/* 给门板加装饰纹理(竖纹)和金边 */
.door-left::after,
.door-right::after {
content: '';
position: absolute;
inset: 0;
background-image: linear-gradient(
90deg,
rgba(255, 255, 255, 0.01) 0.5px,
transparent 0.5px
);
opacity: 0.12;
}
/* 门面标签 */
.label {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
pointer-events: none;
}
.label .id {
font-weight: 700;
color: var(--gold);
background: linear-gradient(
90deg,
rgba(255, 255, 255, 0.04),
rgba(255, 255, 255, 0.02)
);
padding: 6px 10px;
border-radius: 6px;
font-size: 14px;
letter-spacing: 0.6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25) inset;
}
.label .hint {
font-size: 11px;
margin-top: 6px;
color: rgba(255, 255, 255, 0.82);
}
/* 打开状态:门板旋转(做成类似两扇门打开) */
.cell.opened .door-left {
transform: perspective(600px) rotateY(-100deg) translateZ(0);
}
.cell.opened .door-right {
transform: perspective(600px) rotateY(100deg) translateZ(0);
}
/* 奖品显示样式 */
.prize {
width: 100%;
height: 100%;
background: linear-gradient(
180deg,
rgba(255, 242, 225, 0.18),
rgba(255, 232, 200, 0.06)
);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
padding: 10px;
box-sizing: border-box;
}
.prize img {
width: 68%;
height: auto;
max-height: 58%;
object-fit: contain;
border-radius: 8px;
border: 2px solid rgba(255, 255, 255, 0.06);
background: rgba(255, 255, 255, 0.02);
}
.prize-name {
color: #fff;
font-size: 12px;
text-align: center;
word-break: break-word;
}
.tag {
position: absolute;
left: 8px;
top: 8px;
background: linear-gradient(90deg, var(--gold), #ffd77a);
color: var(--red-deep);
font-weight: 700;
padding: 4px 6px;
border-radius: 6px;
font-size: 11px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
}
/* 模态窗与动画 */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.42);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
padding: 20px;
box-sizing: border-box;
}
.modal-fade-enter-active,
.modal-fade-leave-active {
transition: opacity 240ms ease;
}
.modal-fade-enter-from,
.modal-fade-leave-to {
opacity: 0;
}
/* 模态体(动态金黄渐变) */
.modal {
width: min(920px, 96%);
background: linear-gradient(
45deg,
#c8161d,
/* 主红色 */ #e32c2c,
/* 高光红 */ #b21f2d,
/* 深红阴影 */ #ff4d4f,
/* 活泼红点 */ #c8161d
);
background-size: 300% 300%;
animation: modalGradientFlow 4s ease-in-out infinite;
border-radius: 12px;
overflow: hidden;
position: relative;
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.45);
transform-origin: center;
padding: 14px;
border: 1px solid rgba(255, 255, 255, 0.08); /* 金色微边框增强质感 */
}
/* 动态渐变动画 */
@keyframes modalGradientFlow {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
/* 内容网格 */
.modal-body {
display: grid;
grid-template-columns: 46% 54%;
gap: 14px;
align-items: center;
}
.modal-left {
position: relative;
display: flex;
align-items: center;
justify-content: center;
padding: 8px;
border-radius: 10px;
background: linear-gradient(
180deg,
rgba(255, 255, 255, 0.02),
rgba(0, 0, 0, 0.02)
);
}
.modal-left img {
max-width: 100%;
max-height: 320px;
border-radius: 8px;
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.35) inset;
}
.modal-right {
padding: 6px 10px;
color: #fff;
}
.modal-title {
margin: 0;
font-size: 35px;
color: var(--gold);
letter-spacing: 0.6px;
}
.prize-title {
margin: 10px 0 6px;
font-size: 22px;
color: #fff;
}
.modal-msg {
margin: 8px 0 18px;
color: rgba(255, 255, 255, 0.86);
font-size: 13px;
}
/* 按钮 */
.confirm {
background: linear-gradient(90deg, var(--gold), #ffd77a);
color: var(--red-deep);
border: none;
padding: 10px 18px;
border-radius: 8px;
font-weight: 700;
cursor: pointer;
box-shadow: 0 6px 18px rgba(178, 31, 45, 0.12);
}
.close-btn {
position: absolute;
right: 8px;
top: 8px;
background: transparent;
border: none;
color: rgba(255, 255, 255, 0.9);
font-size: 22px;
cursor: pointer;
}
/* 礼花画布覆盖在模态内 */
.confetti-canvas {
position: absolute;
inset: 0;
pointer-events: none;
z-index: 10;
width: 100%;
height: 100%;
}
/* 响应处理:小屏时 modal 调整为单列 */
@media (max-width: 720px) {
.modal-body {
grid-template-columns: 1fr;
}
.modal-left img {
max-height: 220px;
}
}
/* 大气艺术字体,金黄渐变动画 */
.art-title {
font-family:
'SimHei', 'Heiti SC', 'PingFang SC', 'Microsoft YaHei', sans-serif; /* 中文黑体风格 */
font-size: 33px; /* 根据页面宽度可调 */
font-weight: 700;
text-align: center;
background: linear-gradient(
45deg,
#ffe29a,
#ffb94a,
#ffe29a,
#ffde00,
#ffd400
);
background-size: 300% 300%;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
animation: gradientFlow 6s ease-in-out infinite;
letter-spacing: 2px;
line-height: 2.2;
filter: drop-shadow(2px 2px 10px rgba(0, 0, 0, 1));
}
/* 渐变流动动画 */
@keyframes gradientFlow {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
/* 小屏幕适配 */
@media (max-width: 720px) {
.art-title {
font-size: 35px;
letter-spacing: 1px;
}
}
/* 红包整体 */
.red-envelope {
width: 100%;
height: 100%;
position: relative;
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
perspective: 600px; /* 用于开合动画 */
}
/* 红包封口 */
.envelope-top {
width: 80%;
height: 20%;
background: linear-gradient(135deg, #e32c2c, #b21f2d);
border-radius: 8px 8px 0 0;
transform-origin: top center;
transition: transform 2s cubic-bezier(0.2, 0.9, 0.3, 1);
z-index: 2;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
}
/* 红包主体 */
.envelope-body {
width: 80%;
flex: 1;
background: linear-gradient(180deg, #ff4d4f, #c8161d);
border-radius: 0 0 10px 10px;
box-shadow: inset 0 4px 12px rgba(0, 0, 0, 0.2);
position: relative;
}
/* 打开动画 */
.cell.opened .envelope-top {
transform: rotateX(-120deg) translateY(-10%);
}
/* 红包上的数字标签 */
.label {
position: absolute;
top: 28%;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
}
.label .id {
color: #ffd400;
font-weight: 700;
font-size: 16px;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
background: rgba(255, 255, 255, 0.08);
padding: 4px 8px;
border-radius: 6px;
}
</style>