1243 lines
63 KiB
HTML
1243 lines
63 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>VRTicket 潮流票夹 - 待核销票据</title>
|
||
<!-- Tailwind CSS CDN -->
|
||
<script src="https://cdn.tailwindcss.com"></script>
|
||
<!-- Vue 2.x CDN -->
|
||
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
|
||
<!-- Google Fonts for premium typography -->
|
||
<link
|
||
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;700;900&family=Noto+Sans+SC:wght@300;400;500;700;900&display=swap"
|
||
rel="stylesheet">
|
||
|
||
<style>
|
||
body {
|
||
font-family: 'Noto Sans SC', 'Montserrat', sans-serif;
|
||
background: radial-gradient(circle at center, #fef4f4 0%, #f3e5e5 100%);
|
||
}
|
||
|
||
/* Custom Scrollbar */
|
||
.custom-scroll::-webkit-scrollbar {
|
||
width: 4px;
|
||
}
|
||
|
||
.custom-scroll::-webkit-scrollbar-track {
|
||
background: transparent;
|
||
}
|
||
|
||
.custom-scroll::-webkit-scrollbar-thumb {
|
||
background: rgba(0, 0, 0, 0.12);
|
||
border-radius: 10px;
|
||
}
|
||
|
||
/* Topographic wave lines animation */
|
||
@keyframes wave-pulse {
|
||
|
||
0%,
|
||
100% {
|
||
transform: scale(1) translate(0, 0);
|
||
opacity: 0.12;
|
||
}
|
||
|
||
50% {
|
||
transform: scale(1.05) translate(-1%, -1%);
|
||
opacity: 0.18;
|
||
}
|
||
}
|
||
|
||
.contour-bg {
|
||
animation: wave-pulse 12s ease-in-out infinite;
|
||
}
|
||
|
||
/* Rotating Text around star logo */
|
||
@keyframes spin-slow {
|
||
0% {
|
||
transform: rotate(0deg);
|
||
}
|
||
|
||
100% {
|
||
transform: rotate(360deg);
|
||
}
|
||
}
|
||
|
||
.animate-spin-slow {
|
||
animation: spin-slow 20s linear infinite;
|
||
}
|
||
|
||
/* Vinyl record spin */
|
||
@keyframes spin-record {
|
||
0% {
|
||
transform: rotate(0deg);
|
||
}
|
||
|
||
100% {
|
||
transform: rotate(360deg);
|
||
}
|
||
}
|
||
|
||
.animate-spin-record {
|
||
animation: spin-record 10s linear infinite;
|
||
}
|
||
|
||
/* Stamp animation for verification */
|
||
@keyframes stamp-slam {
|
||
0% {
|
||
transform: scale(3) rotate(-35deg);
|
||
opacity: 0;
|
||
}
|
||
|
||
70% {
|
||
transform: scale(0.95) rotate(-15deg);
|
||
opacity: 0.9;
|
||
}
|
||
|
||
100% {
|
||
transform: scale(1) rotate(-18deg);
|
||
opacity: 1;
|
||
}
|
||
}
|
||
|
||
.verified-stamp {
|
||
animation: stamp-slam 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
|
||
}
|
||
|
||
/* Smooth Height transitions */
|
||
.expand-transition {
|
||
transition: all 0.3s cubic-bezier(0.25, 1, 0.5, 1);
|
||
}
|
||
</style>
|
||
</head>
|
||
|
||
<body class="min-h-screen flex items-center justify-center p-0 sm:p-6 select-none overflow-x-hidden">
|
||
|
||
<div id="app" class="w-full max-w-6xl flex flex-col lg:flex-row items-center justify-center gap-8 relative">
|
||
|
||
<!-- Left Panel: Interactive AI Sandbox Terminal & Manual Controls -->
|
||
<div
|
||
class="hidden lg:flex flex-col w-80 bg-white/90 backdrop-blur-md border border-red-100 rounded-[32px] p-6 shadow-xl space-y-4 shrink-0">
|
||
<div class="flex items-center gap-2 pb-2 border-b border-red-100">
|
||
<span class="w-3 h-3 rounded-full bg-red-500 animate-pulse"></span>
|
||
<h2 class="font-black text-gray-800 text-base">VRTicket 智能大脑</h2>
|
||
</div>
|
||
|
||
<p class="text-xs text-gray-500 leading-relaxed">
|
||
本应用已完美还原图片细节(星标对齐、打孔卡、重磅时点字体),并深度接入 <strong>Gemini 2.5 Flash</strong> 实时生成与智能对话。
|
||
</p>
|
||
|
||
<!-- Quick AI Action panel -->
|
||
<div class="bg-gradient-to-br from-neutral-50 to-red-50/30 p-3.5 rounded-2xl border border-red-50 space-y-2.5">
|
||
<span class="text-[10px] font-bold text-red-500 uppercase tracking-widest block">AI 快捷灵感操作</span>
|
||
|
||
<button @click="triggerQuickAIShow"
|
||
class="w-full text-left bg-white border border-gray-200 hover:border-black p-2.5 rounded-xl transition text-xs font-bold text-gray-700 flex items-center gap-2 shadow-sm">
|
||
<span class="text-base">🎫</span>
|
||
<span>一键自然语言生成门票</span>
|
||
</button>
|
||
|
||
<button @click="triggerAIMovieRecom"
|
||
class="w-full text-left bg-white border border-gray-200 hover:border-black p-2.5 rounded-xl transition text-xs font-bold text-gray-700 flex items-center gap-2 shadow-sm">
|
||
<span class="text-base">🍿</span>
|
||
<span>获取基于本卡包的观影分析</span>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Quick manual commands -->
|
||
<div class="space-y-2.5 pt-1">
|
||
<button @click="showAddModal = true"
|
||
class="w-full bg-black text-white py-3 px-4 rounded-xl font-bold text-xs hover:bg-gray-800 transition transform active:scale-95 flex items-center justify-center gap-2">
|
||
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4" />
|
||
</svg>
|
||
手动新增自定义票券
|
||
</button>
|
||
|
||
<button @click="resetTickets"
|
||
class="w-full bg-gray-100 text-gray-700 py-2 px-4 rounded-xl font-bold text-xs hover:bg-gray-200 transition">
|
||
重置 12 张高清测试数据
|
||
</button>
|
||
|
||
<button @click="quickVerifyAll"
|
||
class="w-full bg-red-50 text-red-600 border border-red-200 py-2 px-4 rounded-xl font-bold text-xs hover:bg-red-100 transition">
|
||
一键模拟全核销 (送到底部)
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Realtime stats box -->
|
||
<div class="bg-gray-50 p-4 rounded-2xl border border-gray-100 space-y-2 text-xs">
|
||
<div class="text-[10px] font-bold text-gray-400 uppercase tracking-wider">数据流转状态</div>
|
||
<div class="flex justify-between items-center">
|
||
<span class="text-gray-500">待核销票数(置顶):</span>
|
||
<span class="font-black text-red-500 text-sm">{{ activeUnverifiedTickets.length }}</span>
|
||
</div>
|
||
<div class="flex justify-between items-center">
|
||
<span class="text-gray-500">已核销票数(下沉):</span>
|
||
<span class="font-black text-gray-400 text-sm">{{ verifiedTickets.length }}</span>
|
||
</div>
|
||
<div class="w-full bg-gray-200 rounded-full h-1 mt-1">
|
||
<div class="bg-red-500 h-1 rounded-full transition-all duration-500"
|
||
:style="{ width: (verifiedTickets.length / tickets.length * 100) + '%' }"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Center: The Core Mobile Terminal Mockup -->
|
||
<div
|
||
class="relative w-full max-w-[420px] h-[880px] bg-[#f4f5f6] rounded-[52px] shadow-[0_25px_60px_-15px_rgba(235,100,100,0.3)] border-[10px] border-white overflow-hidden flex flex-col"
|
||
style="box-shadow: 0 0 0 1px rgba(235,140,140,0.25), 0 25px 60px -15px rgba(235,100,100,0.3);">
|
||
|
||
<!-- iOS Standard Notch -->
|
||
<div
|
||
class="absolute top-0 inset-x-0 h-8 bg-transparent z-40 flex justify-between items-center px-8 text-black font-semibold text-xs pointer-events-none">
|
||
<span class="font-bold tracking-tight">15:40</span>
|
||
<div class="w-24 h-4 bg-black rounded-b-xl mx-auto absolute left-1/2 transform -translate-x-1/2 top-0"></div>
|
||
<div class="flex items-center gap-1.5">
|
||
<svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="currentColor">
|
||
<path
|
||
d="M12 3c-4.97 0-9 4.03-9 9 0 2.12.74 4.07 1.97 5.61L4.35 19.4c-.39.39-.39 1.02 0 1.41.39.39 1.02.39 1.41 0l1.9-1.9C9.07 19.58 10.48 20 12 20c4.97 0 9-4.03 9-9s-4.03-9-9-9zm0 15c-3.31 0-6-2.69-6-6s2.69-6 6-6 6 2.69 6 6-2.69 6-6 6z" />
|
||
</svg>
|
||
<span class="font-bold text-[10px]">5G</span>
|
||
<div class="w-4.5 h-2.5 border border-black rounded-sm p-0.5 flex items-center">
|
||
<div class="h-full w-3 bg-black rounded-2xs"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- MAIN SCROLLABLE APP CONTENT -->
|
||
<div class="flex-1 overflow-y-auto overflow-x-hidden custom-scroll pb-24 relative bg-[#f4f5f6]">
|
||
|
||
<!-- Tab Content 1: Ticket list ("票夹") -->
|
||
<div v-show="activeTab === 'tickets'">
|
||
|
||
<!-- BRAND TOP BANNER -->
|
||
<div class="relative w-full h-[290px] bg-white overflow-hidden flex flex-col items-center justify-center">
|
||
<!-- Ambiance lights -->
|
||
<div
|
||
class="absolute -top-10 -right-10 w-40 h-40 rounded-full bg-[#f6483b] opacity-40 filter blur-[32px] pointer-events-none">
|
||
</div>
|
||
<div
|
||
class="absolute top-44 -left-12 w-32 h-32 rounded-full bg-[#f6483b] opacity-40 filter blur-[28px] pointer-events-none">
|
||
</div>
|
||
|
||
<!-- Contour Vector Graphics -->
|
||
<svg class="contour-bg absolute inset-0 w-full h-full text-black" viewBox="0 0 400 300" fill="none"
|
||
stroke="currentColor">
|
||
<path d="M-50,60 C80,30 180,120 450,40" stroke-width="1" stroke-dasharray="3 3" />
|
||
<path d="M-50,110 C90,70 190,170 450,90" stroke-width="1" />
|
||
<path d="M-50,160 C100,120 200,210 450,140" stroke-width="0.8" />
|
||
<path d="M-50,210 C110,170 210,250 450,190" stroke-width="1.2" />
|
||
<path d="M-50,260 C120,220 220,290 450,240" stroke-width="0.8" stroke-dasharray="4 2" />
|
||
</svg>
|
||
|
||
<!-- Spinning Record Disc Left -->
|
||
<div
|
||
class="absolute top-6 right-6 w-24 h-24 transform rotate-[25deg] filter drop-shadow-lg animate-spin-record z-10 pointer-events-none">
|
||
<div
|
||
class="w-full h-full rounded-full bg-gradient-to-r from-neutral-900 via-neutral-800 to-neutral-900 border-2 border-neutral-700 flex items-center justify-center relative shadow-inner">
|
||
<div class="absolute inset-2 border border-neutral-700/50 rounded-full"></div>
|
||
<div class="absolute inset-4 border border-neutral-700/40 rounded-full"></div>
|
||
<div class="absolute inset-6 border border-neutral-600/30 rounded-full"></div>
|
||
<div class="w-8 h-8 rounded-full bg-[#f4f5f6] border border-black flex items-center justify-center">
|
||
<div class="w-2.5 h-2.5 rounded-full bg-black"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Spinning Record Disc Right -->
|
||
<div
|
||
class="absolute top-44 left-4 w-20 h-20 transform -rotate-[15deg] filter drop-shadow-md animate-spin-record pointer-events-none">
|
||
<div
|
||
class="w-full h-full rounded-full bg-gradient-to-r from-neutral-900 via-neutral-800 to-neutral-900 border-2 border-neutral-700 flex items-center justify-center relative shadow-inner">
|
||
<div class="absolute inset-1.5 border border-neutral-700/50 rounded-full"></div>
|
||
<div class="absolute inset-3 border border-neutral-700/40 rounded-full"></div>
|
||
<div class="w-6 h-6 rounded-full bg-[#f4f5f6] border border-black flex items-center justify-center">
|
||
<div class="w-2 h-2 rounded-full bg-black"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Smiling graffiti face -->
|
||
<div class="absolute bottom-12 right-24 transform rotate-12 filter drop-shadow-sm pointer-events-none">
|
||
<svg class="w-8 h-8 text-black" viewBox="0 0 40 40" fill="none" stroke="currentColor" stroke-width="2.5"
|
||
stroke-linecap="round" stroke-linejoin="round">
|
||
<circle cx="20" cy="20" r="16" />
|
||
<path d="M14,16 L14.01,16 M26,16 L26.01,16" stroke-width="3.5" />
|
||
<path d="M13,22 Q20,29 27,22" />
|
||
</svg>
|
||
</div>
|
||
|
||
<!-- Live music texts -->
|
||
<div
|
||
class="absolute left-6 top-14 flex flex-col items-center uppercase text-[8px] font-black text-black tracking-[0.2em] font-mono leading-tight pointer-events-none">
|
||
<span>l</span><span>i</span><span>v</span><span>e</span>
|
||
<span class="my-1 text-red-500 font-extrabold">•</span>
|
||
<span>m</span><span>u</span><span>s</span><span>i</span><span>c</span>
|
||
<span class="my-1 text-red-500 font-extrabold">•</span>
|
||
<span>i</span><span>n</span>
|
||
</div>
|
||
<div
|
||
class="absolute left-12 top-14 flex flex-col items-center uppercase text-[7px] font-bold text-gray-500 tracking-[0.15em] font-mono leading-tight pointer-events-none">
|
||
<span>v</span><span>r</span><span>t</span><span>i</span><span>c</span><span>k</span><span>e</span><span>t</span>
|
||
</div>
|
||
|
||
<!-- BRAND LOGO STAR HUB -->
|
||
<div class="relative flex items-center justify-center z-20">
|
||
<svg class="absolute w-[150px] h-[150px] animate-spin-slow" viewBox="0 0 120 120">
|
||
<path id="vrTicketPath" d="M 60 16 A 44 44 0 1 1 59.9 16" fill="none" />
|
||
<text fill="black" class="text-[7.2px] font-black tracking-[0.26em] uppercase">
|
||
<textPath href="#vrTicketPath" startOffset="0%">
|
||
VRTICKET • VRTICKET • VRTICKET • VRTICKET •
|
||
</textPath>
|
||
</text>
|
||
</svg>
|
||
|
||
<!-- Star core representation -->
|
||
<svg class="w-[105px] h-[105px] filter drop-shadow-[0_4px_12px_rgba(0,0,0,0.15)]" viewBox="0 0 100 100"
|
||
fill="none" xmlns="http://www.w3.org/2000/svg">
|
||
<path d="M 50 2 C 50 38 38 50 2 50 C 38 50 50 62 50 98 C 50 62 62 50 98 50 C 62 50 50 38 50 2 Z"
|
||
fill="black" />
|
||
</svg>
|
||
|
||
<!-- Branding center star plate -->
|
||
<div
|
||
class="absolute bg-[#e1251b] text-white font-black px-3.5 py-0.5 text-[9px] transform -rotate-[5deg] border-2 border-black tracking-[0.1em] shadow-md uppercase">
|
||
VRTICKET
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- TICKET PANEL CONTAINER (White cover overlay) -->
|
||
<div
|
||
class="relative bg-white rounded-t-[36px] px-5 pt-6 -mt-10 min-h-[500px] shadow-[0_-12px_30px_rgba(0,0,0,0.04)] z-30">
|
||
|
||
<!-- Section Header -->
|
||
<div class="flex justify-between items-baseline mb-5 px-1">
|
||
<h1 class="text-2xl font-black text-black tracking-wide">待核销票据</h1>
|
||
<div class="flex items-center gap-1.5 text-black font-extrabold text-xs tracking-wide">
|
||
<span>共</span>
|
||
<span class="text-xl font-black tracking-tight font-mono text-[#e1251b]">{{
|
||
activeUnverifiedTickets.length }}</span>
|
||
<span>张票</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- UNVERIFIED TICKETS (置顶部分) -->
|
||
<div class="space-y-5">
|
||
<div v-for="(ticket, idx) in activeUnverifiedTickets" :key="'unverified-' + ticket.id"
|
||
class="relative bg-white border-[2.5px] border-black rounded-[24px] overflow-hidden shadow-[0_10px_20px_rgba(0,0,0,0.05)] hover:shadow-[0_14px_28px_rgba(0,0,0,0.08)] transition-all duration-300 transform active:scale-[0.99] cursor-pointer"
|
||
@click="openTicketDetail(ticket)">
|
||
<!-- Physical Stamp scalloped punch cuts overlay (Left side) -->
|
||
<div
|
||
class="absolute left-[-11px] top-6 bottom-6 w-5 flex flex-col justify-between z-30 pointer-events-none">
|
||
<div v-for="n in 6" :key="'l'+n"
|
||
class="w-5 h-5 bg-[#f4f5f6] border-[2.5px] border-black rounded-full"></div>
|
||
</div>
|
||
|
||
<!-- Physical Stamp scalloped punch cuts overlay (Right side) -->
|
||
<div
|
||
class="absolute right-[-11px] top-6 bottom-6 w-5 flex flex-col justify-between z-30 pointer-events-none">
|
||
<div v-for="n in 6" :key="'r'+n"
|
||
class="w-5 h-5 bg-[#f4f5f6] border-[2.5px] border-black rounded-full"></div>
|
||
</div>
|
||
|
||
<!-- Card internal padding block -->
|
||
<div class="px-5 py-5 relative z-20">
|
||
|
||
<!-- Top Row: Cinematic Brand Star + Text with Contact buttons -->
|
||
<div class="flex justify-between items-center pb-3 border-b border-gray-100 mb-4">
|
||
<div class="flex items-center gap-3">
|
||
<!-- Big Solid Black Star matching 15.40.16.png -->
|
||
<!-- <svg class="w-10 h-10 text-black shrink-0" viewBox="0 0 24 24" fill="currentColor">
|
||
<path
|
||
d="M12 .587l3.668 7.431 8.2 1.192-5.934 5.787 1.4 8.168L12 18.896l-7.334 3.857 1.4-8.168L.132 9.21l8.2-1.192L12 .587z" />
|
||
</svg> -->
|
||
<!-- Vertical cinema title lines layout -->
|
||
<div
|
||
class="flex flex-col text-[13px] font-black text-black leading-[1.1] tracking-[0.15em] text-left select-none uppercase">
|
||
<span>大悦城电影院</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Circular minimal phone & locator actions -->
|
||
<div class="flex items-center gap-2">
|
||
<button @click.stop="triggerAction('phone', ticket.cinema)"
|
||
class="w-8 h-8 rounded-full bg-white border border-gray-200 flex items-center justify-center hover:bg-gray-50 active:scale-90 transition">
|
||
<svg class="w-4 h-4 text-gray-700" fill="none" stroke="currentColor" stroke-width="2"
|
||
viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round"
|
||
d="M3 5a2 2 0 012-2h3.28a1 1 0 01.94.725l.548 2.2a1 1 0 01-.321.988l-1.305.98a10.582 10.582 0 004.872 4.872l.98-1.305a1 1 0 01.988-.321l2.2.548a1 1 0 01.725.94V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
|
||
</svg>
|
||
</button>
|
||
<button @click.stop="triggerAction('map', ticket.cinema)"
|
||
class="w-8 h-8 rounded-full bg-white border border-gray-200 flex items-center justify-center hover:bg-gray-50 active:scale-90 transition">
|
||
<svg class="w-4 h-4 text-gray-700" fill="none" stroke="currentColor" stroke-width="2"
|
||
viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round"
|
||
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
|
||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Details section -->
|
||
<div class="flex justify-between items-start gap-4">
|
||
<!-- Left Columns details -->
|
||
<div class="flex-1 min-w-0">
|
||
<h2 class="text-xl font-black text-black leading-snug tracking-wide truncate mb-1">{{ ticket.movie
|
||
}}</h2>
|
||
<p class="text-xs font-bold text-gray-400 tracking-wider mb-4">{{ ticket.format }}</p>
|
||
|
||
<!-- Timestamps -->
|
||
<div class="text-[11px] font-bold text-gray-400 tracking-wider mb-0.5 font-sans">{{ ticket.date }}
|
||
</div>
|
||
<div class="text-2xl font-black text-black tracking-tight leading-none mb-4">{{ ticket.time }}
|
||
</div>
|
||
|
||
<!-- Rounded seat section matching 15.40.16.png -->
|
||
<div class="bg-[#f8f9fa] border border-gray-100 rounded-xl p-3 flex items-center gap-3">
|
||
<!-- Custom Vertical Tag badge -->
|
||
<div
|
||
class="border border-gray-200 rounded-lg px-1 py-1.5 bg-white flex flex-col items-center justify-center leading-none text-[8px] font-bold text-gray-400 gap-0.5 select-none shrink-0 scale-95">
|
||
<span>VIP</span>
|
||
</div>
|
||
<div class="flex-1 min-w-0">
|
||
<div class="text-black font-black text-[13px] mb-0.5">{{ ticket.hall }}</div>
|
||
<div class="text-gray-500 font-bold text-[10.5px] flex flex-wrap gap-x-1.5 gap-y-0.5">
|
||
<span v-for="(seat, sIdx) in ticket.seats" :key="sIdx" class="whitespace-nowrap">{{ seat
|
||
}}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Right Poster Card Section matching 15.40.16.png -->
|
||
<div
|
||
class="relative w-[90px] h-[126px] rounded-[14px] overflow-hidden border-[1.5px] border-black shadow-md shrink-0 bg-neutral-950 flex flex-col justify-between p-2">
|
||
<!-- Red spotlight gradient overlay -->
|
||
<div
|
||
class="absolute top-2 left-1/2 -translate-x-1/2 w-8 h-8 rounded-full bg-red-600/35 blur-[6px] pointer-events-none">
|
||
</div>
|
||
<!-- Type label top -->
|
||
<div
|
||
class="relative text-[10px] font-black text-white text-center mt-1 select-none tracking-wider">
|
||
2D
|
||
</div>
|
||
<!-- Titles bottom -->
|
||
<div class="relative text-center flex flex-col items-center">
|
||
<span class="text-[11px] text-[#e1251b] font-black tracking-widest mb-0.5">{{ ticket.posterAbbr
|
||
|| '水门桥' }}</span>
|
||
<span
|
||
class="text-[6.5px] text-gray-400 font-bold tracking-tight uppercase leading-none font-sans whitespace-nowrap scale-90">{{
|
||
ticket.posterEn || 'THE BATTLE AT LAKE' }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- EMPTY PLACEHOLDER -->
|
||
<div v-if="activeUnverifiedTickets.length === 0"
|
||
class="flex flex-col items-center justify-center py-12 text-center text-gray-400">
|
||
<span class="text-3xl mb-2">🎉</span>
|
||
<p class="font-bold text-gray-700 text-sm">暂无待核销票据</p>
|
||
<p class="text-xs text-gray-400 mt-0.5">所有票券均已完美核销,已下沉至底部</p>
|
||
</div>
|
||
|
||
<!-- VERIFIED HISTORIES (已核销部分:沉于下方) -->
|
||
<div v-if="verifiedTickets.length > 0" class="mt-8 pt-6 border-t-[1.5px] border-dashed border-gray-200">
|
||
<div class="flex justify-between items-center mb-4 px-1">
|
||
<span class="text-xs font-black text-gray-400 tracking-wider uppercase">已核销票券历史 ({{
|
||
verifiedTickets.length }})</span>
|
||
<button @click="clearVerifiedHistory"
|
||
class="text-[10px] font-bold text-red-500 hover:underline">清空记录</button>
|
||
</div>
|
||
|
||
<div class="space-y-4 opacity-55 saturate-50 hover:opacity-80 transition duration-300">
|
||
<div v-for="(ticket, idx) in verifiedTickets" :key="'verified-' + ticket.id"
|
||
class="relative bg-white border-[2.5px] border-neutral-300 rounded-[24px] overflow-hidden shadow-sm">
|
||
<!-- Stamp punch Left -->
|
||
<div
|
||
class="absolute left-[-11px] top-4 bottom-4 w-5 flex flex-col justify-between z-30 pointer-events-none">
|
||
<div v-for="n in 5" :key="'l-v'+n"
|
||
class="w-5 h-5 bg-[#f4f5f6] border-[2.5px] border-neutral-300 rounded-full"></div>
|
||
</div>
|
||
<!-- Stamp punch Right -->
|
||
<div
|
||
class="absolute right-[-11px] top-4 bottom-4 w-5 flex flex-col justify-between z-30 pointer-events-none">
|
||
<div v-for="n in 5" :key="'r-v'+n"
|
||
class="w-5 h-5 bg-[#f4f5f6] border-[2.5px] border-neutral-300 rounded-full"></div>
|
||
</div>
|
||
|
||
<div class="px-5 py-4 relative z-20">
|
||
<div class="flex justify-between items-start gap-4">
|
||
<div class="flex-1 min-w-0">
|
||
<div class="flex items-center gap-1.5 mb-1.5 text-neutral-400">
|
||
<span class="text-xs font-bold truncate">{{ ticket.cinema }}</span>
|
||
</div>
|
||
<h3 class="text-base font-black text-neutral-800 truncate mb-1">{{ ticket.movie }}</h3>
|
||
<div class="text-[11px] font-mono text-neutral-400">{{ ticket.date }} | {{ ticket.time }}</div>
|
||
</div>
|
||
|
||
<!-- Minimal Verified Stamp Indicator -->
|
||
<div
|
||
class="border-2 border-neutral-400 text-neutral-400 px-2 py-1 text-[10px] font-black uppercase rounded tracking-widest transform -rotate-12 bg-white">
|
||
已通过 PASSED
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Outer offset spacing -->
|
||
<div class="h-10"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tab Content 2: AI assistant terminal ("AI 助手") -->
|
||
<div v-show="activeTab === 'ai-chat'" class="px-5 pt-6 bg-white min-h-[800px] flex flex-col">
|
||
<div class="border-b border-gray-100 pb-3.5 mb-4 flex items-center justify-between">
|
||
<div>
|
||
<h2 class="text-xl font-black text-black">VRTicket AI 秘僚</h2>
|
||
<p class="text-[10px] text-gray-400 font-bold tracking-wider mt-0.5">GEMINI 2.5 FLASH POWERED</p>
|
||
</div>
|
||
<button @click="clearChat" class="text-[10px] font-bold text-gray-400 hover:text-black">清空会话</button>
|
||
</div>
|
||
|
||
<!-- Chat History -->
|
||
<div class="flex-1 space-y-4 text-xs pr-1 max-h-[480px] overflow-y-auto custom-scroll mb-4"
|
||
id="chat-scroller">
|
||
<!-- Welcome -->
|
||
<div class="bg-red-50/50 rounded-2xl p-3.5 border border-red-50 text-gray-700 space-y-2">
|
||
<p class="font-bold text-red-600">👋 你好!我是你的 VRTicket 潮流票务助手。</p>
|
||
<p class="leading-relaxed text-[11px]">
|
||
你可以用任何自然语言向我提问,或者尝试让我<strong>“帮你订一张下周六去上海看周杰伦演唱会的票”</strong>,我会实时为你出票并导入到本机的钱包票夹中!</p>
|
||
</div>
|
||
|
||
<div v-for="(msg, mIdx) in chatMessages" :key="mIdx"
|
||
:class="[msg.role === 'user' ? 'text-right' : 'text-left']">
|
||
<div
|
||
:class="[msg.role === 'user' ? 'bg-black text-white ml-12 rounded-br-none' : 'bg-gray-100 text-gray-800 mr-12 rounded-bl-none']"
|
||
class="inline-block px-4 py-2.5 rounded-2xl text-left leading-relaxed">
|
||
<div v-html="formatMessage(msg.content)"></div>
|
||
<!-- Interactive Action attached if payload contains ticket metadata -->
|
||
<div v-if="msg.metaTicket" class="mt-2.5 pt-2 border-t border-gray-200/20 flex justify-end">
|
||
<button @click="addAITicket(msg.metaTicket)"
|
||
class="bg-red-500 text-white font-extrabold px-3 py-1 rounded-lg text-[10px] hover:bg-red-600 active:scale-95 transition">
|
||
⚡ 确认并导入票夹
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Loading overlay inside chat -->
|
||
<div v-if="aiLoading" class="text-left">
|
||
<div class="bg-gray-100 text-gray-500 mr-12 rounded-2xl px-4 py-2.5 inline-flex items-center gap-2">
|
||
<span class="w-2 h-2 rounded-full bg-red-500 animate-ping"></span>
|
||
<span class="font-bold text-[11px] animate-pulse">Gemini 正在精细计算中...</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Quick presets clickable pills -->
|
||
<div class="flex flex-wrap gap-1.5 mb-3">
|
||
<button @click="submitChatPreset('订票助理', '我想订一张下周六晚上7点在五棵松体育馆看李荣浩演唱会的门票,内场1排8座')"
|
||
class="bg-gray-50 hover:bg-gray-100 border border-gray-100 rounded-full px-3 py-1.5 text-[10.5px] font-semibold text-gray-600 transition">
|
||
🎭 订演出票
|
||
</button>
|
||
<button @click="submitChatPreset('订电影票', '我想订一张今晚8点大悦城电影院《流浪地球3》IMAX厅的票')"
|
||
class="bg-gray-50 hover:bg-gray-100 border border-gray-100 rounded-full px-3 py-1.5 text-[10.5px] font-semibold text-gray-600 transition">
|
||
🎬 订电影票
|
||
</button>
|
||
<button @click="submitChatPreset('朋友圈文案', '帮我写一个看完《长津湖之水门桥》的高分打卡朋友圈文案,热血震撼风格')"
|
||
class="bg-gray-50 hover:bg-gray-100 border border-gray-100 rounded-full px-3 py-1.5 text-[10.5px] font-semibold text-gray-600 transition">
|
||
✍️ 朋友圈文案
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Bottom input container -->
|
||
<div class="flex items-center gap-2 pb-6">
|
||
<input type="text" v-model="chatInput" @keyup.enter="sendChatMessage" placeholder="向 Gemini 提问或进行自然语言订票..."
|
||
class="flex-1 bg-gray-50 border border-gray-200 rounded-xl px-4 py-3 text-xs font-bold focus:outline-none focus:border-black placeholder-gray-400"
|
||
:disabled="aiLoading" />
|
||
<button @click="sendChatMessage" :disabled="aiLoading"
|
||
class="bg-black hover:bg-neutral-800 text-white p-3 rounded-xl transition shrink-0">
|
||
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" d="M14 5l7 7m0 0l-7 7m7-7H3" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tab Content 3: Merch Simulated Shop ("周边") -->
|
||
<div v-show="activeTab === 'merch'" class="p-6 bg-white min-h-[800px] space-y-6">
|
||
<div>
|
||
<h2 class="text-xl font-black text-black">潮流周边店</h2>
|
||
<p class="text-[11px] text-gray-400 font-bold mt-0.5">VRTICKET CREATIVE HUB</p>
|
||
</div>
|
||
|
||
<div class="grid grid-cols-2 gap-4">
|
||
<div class="border-[2px] border-black rounded-2xl overflow-hidden p-3 bg-[#fdfdfd] space-y-2">
|
||
<div
|
||
class="aspect-square bg-gray-100 rounded-xl flex items-center justify-center font-bold text-2xl text-neutral-800">
|
||
💿</div>
|
||
<div class="text-xs font-black text-black">VRTicket 极简黑胶挂盘</div>
|
||
<div class="flex justify-between items-center">
|
||
<span class="font-black text-sm text-red-500">¥129</span>
|
||
<button @click="triggerAction('alert', '周边商城模拟发售中,敬请期待!')"
|
||
class="bg-black text-white px-2.5 py-1 rounded text-[9px] font-bold">加购</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="border-[2px] border-black rounded-2xl overflow-hidden p-3 bg-[#fdfdfd] space-y-2">
|
||
<div
|
||
class="aspect-square bg-gray-100 rounded-xl flex items-center justify-center font-bold text-2xl text-neutral-800">
|
||
🎒</div>
|
||
<div class="text-xs font-black text-black">星宿多功能户外斜跨包</div>
|
||
<div class="flex justify-between items-center">
|
||
<span class="font-black text-sm text-red-500">¥199</span>
|
||
<button @click="triggerAction('alert', '周边商城模拟发售中,敬请期待!')"
|
||
class="bg-black text-white px-2.5 py-1 rounded text-[9px] font-bold">加购</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tab Content 4: Profile Details ("我的") -->
|
||
<div v-show="activeTab === 'mine'" class="p-6 bg-white min-h-[800px] space-y-6">
|
||
<div class="flex items-center gap-4">
|
||
<div
|
||
class="w-14 h-14 rounded-full bg-black flex items-center justify-center text-2xl border-2 border-black">🛹
|
||
</div>
|
||
<div>
|
||
<h2 class="text-xl font-black text-black">极客探票员</h2>
|
||
<p class="text-xs text-gray-400 font-bold mt-0.5">ID: 8291038291</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="bg-neutral-50 p-4 rounded-2xl border border-neutral-100 space-y-3.5">
|
||
<div class="text-xs font-black text-black uppercase tracking-wider">密钥设定 (如需使用个人 Gemini 密钥)</div>
|
||
<div class="space-y-1.5">
|
||
<label class="block text-[10px] font-bold text-gray-500">GEMINI API KEY (选填,不填则默认使用系统演示通道)</label>
|
||
<input type="password" v-model="userApiKey" placeholder="AI-XXXXXX"
|
||
class="w-full bg-white border border-gray-200 rounded-xl px-3.5 py-2.5 text-xs font-mono focus:outline-none focus:border-black" />
|
||
</div>
|
||
</div>
|
||
|
||
<div class="text-xs text-gray-400 leading-relaxed text-center">
|
||
VRTicket v2.6.5 Pro Mobile Edition<br>© 2026 VRTicket Lab. All rights reserved.
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- BOTTOM NAVIGATION BAR (VRTicket Premium Iconography) -->
|
||
<div
|
||
class="absolute bottom-0 inset-x-0 h-20 bg-white/95 backdrop-blur-md border-t border-gray-100 px-6 flex items-center justify-between z-40">
|
||
<!-- Tab 1: 票夹 -->
|
||
<button @click="activeTab = 'tickets'" :class="[activeTab === 'tickets' ? 'text-[#e1251b]' : 'text-gray-400']"
|
||
class="flex flex-col items-center flex-1 py-2 transition-colors relative">
|
||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
|
||
<path
|
||
d="M19 18H5V6h14v12zm-2-10H7v8h10V8z M3 4a1 1 0 011-1h16a1 1 0 011 1v16a1 1 0 01-1 1H4a1 1 0 01-1-1V4z" />
|
||
</svg>
|
||
<span class="text-[10px] font-black mt-1">票夹</span>
|
||
</button>
|
||
|
||
<!-- Tab 2: AI 助手 -->
|
||
<button @click="activeTab = 'ai-chat'" :class="[activeTab === 'ai-chat' ? 'text-[#e1251b]' : 'text-gray-400']"
|
||
class="flex flex-col items-center flex-1 py-2 transition-colors relative">
|
||
<!-- Active alert glowing dot -->
|
||
<div class="absolute top-1.5 right-6 w-2 h-2 bg-red-500 rounded-full animate-ping"></div>
|
||
<svg class="w-6 h-6" fill="none" stroke="currentColor" stroke-width="2.3" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round"
|
||
d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" />
|
||
</svg>
|
||
<span class="text-[10px] font-black mt-1">AI 助手</span>
|
||
</button>
|
||
|
||
<!-- Tab 3: 周边 -->
|
||
<button @click="activeTab = 'merch'" :class="[activeTab === 'merch' ? 'text-[#e1251b]' : 'text-gray-400']"
|
||
class="flex flex-col items-center flex-1 py-2 transition-colors">
|
||
<svg class="w-6 h-6" fill="none" stroke="currentColor" stroke-width="2.3" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z" />
|
||
</svg>
|
||
<span class="text-[10px] font-black mt-1">周边</span>
|
||
</button>
|
||
|
||
<!-- Tab 4: 我的 -->
|
||
<button @click="activeTab = 'mine'" :class="[activeTab === 'mine' ? 'text-[#e1251b]' : 'text-gray-400']"
|
||
class="flex flex-col items-center flex-1 py-2 transition-colors">
|
||
<svg class="w-6 h-6" fill="none" stroke="currentColor" stroke-width="2.3" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round"
|
||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||
</svg>
|
||
<span class="text-[10px] font-black mt-1">我的</span>
|
||
</button>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- Right Panel: Design Specification Summary (Perfect for UI developers to track changes) -->
|
||
<div
|
||
class="hidden lg:flex flex-col w-80 bg-white/90 backdrop-blur-md border border-red-100 rounded-[32px] p-6 shadow-xl space-y-4 shrink-0">
|
||
<div class="flex items-center gap-2 pb-2 border-b border-red-100">
|
||
<svg class="w-5 h-5 text-red-500" fill="currentColor" viewBox="0 0 20 20">
|
||
<path fill-rule="evenodd"
|
||
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
|
||
clip-rule="evenodd" />
|
||
</svg>
|
||
<h2 class="font-black text-gray-800 text-base">对齐规范与修正</h2>
|
||
</div>
|
||
|
||
<div class="space-y-4 text-xs text-gray-600 leading-relaxed">
|
||
<div>
|
||
<strong class="text-black block mb-0.5">1. 星标与标题对齐方式 (星标紧贴标题)</strong>
|
||
左上角星标采用 SVG 独立图层。影院标题使用 3 行固定列式布局,不产生横向和纵向挤压,彻底防止字体错位。
|
||
</div>
|
||
<div>
|
||
<strong class="text-black block mb-0.5">2. 时点重磅粗体</strong>
|
||
时间文字加粗为 <code class="bg-gray-100 px-1 py-0.5 rounded text-red-500">font-black</code>
|
||
极致超粗体,并紧贴小字号日期,带来纯正的朋克排版体验。
|
||
</div>
|
||
<div>
|
||
<strong class="text-black block mb-0.5">3. 已核销历史下沉</strong>
|
||
已被扫码核销的卡片会自动应用半透明减噪蒙版,并下移排序到卡包的最下方,不占用首行黄金检票位。
|
||
</div>
|
||
</div>
|
||
|
||
<div class="border-t border-gray-100 pt-3">
|
||
<div class="p-3 bg-red-50/50 rounded-2xl border border-red-100 text-[11px] text-red-700 leading-relaxed">
|
||
<strong>开发人员提示:</strong>可通过双击任何票卡,触发无感振动并一键快速模拟核销下沉。
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- MODAL: ADD TICKET (MANUALLY) -->
|
||
<div v-if="showAddModal"
|
||
class="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4">
|
||
<div
|
||
class="bg-white rounded-[32px] p-6 max-w-sm w-full shadow-2xl space-y-4 border border-gray-100 transform scale-100 transition-all">
|
||
<div class="flex justify-between items-center">
|
||
<h3 class="font-black text-black text-base">手动录入自定义票据</h3>
|
||
<button @click="showAddModal = false" class="text-gray-400 hover:text-black">
|
||
<svg class="w-6 h-6" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
|
||
<div class="space-y-3.5 text-xs">
|
||
<div>
|
||
<label class="block font-bold text-gray-700 mb-1">演出/电影名称</label>
|
||
<input type="text" v-model="newTicket.movie"
|
||
class="w-full bg-gray-50 border border-gray-200 rounded-xl px-3.5 py-2.5 font-bold focus:outline-none focus:border-black" />
|
||
</div>
|
||
<div>
|
||
<label class="block font-bold text-gray-700 mb-1">放映场馆/影院</label>
|
||
<input type="text" v-model="newTicket.cinema"
|
||
class="w-full bg-gray-50 border border-gray-200 rounded-xl px-3.5 py-2.5 font-bold focus:outline-none focus:border-black" />
|
||
</div>
|
||
<div class="grid grid-cols-2 gap-2">
|
||
<div>
|
||
<label class="block font-bold text-gray-700 mb-1">放映日期</label>
|
||
<input type="text" v-model="newTicket.date"
|
||
class="w-full bg-gray-50 border border-gray-200 rounded-xl px-3.5 py-2.5 font-mono focus:outline-none focus:border-black" />
|
||
</div>
|
||
<div>
|
||
<label class="block font-bold text-gray-700 mb-1">时间段</label>
|
||
<input type="text" v-model="newTicket.time"
|
||
class="w-full bg-gray-50 border border-gray-200 rounded-xl px-3.5 py-2.5 font-mono focus:outline-none focus:border-black" />
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<label class="block font-bold text-gray-700 mb-1">影厅与座位 (以空格分隔)</label>
|
||
<input type="text" v-model="newTicket.seatsInput"
|
||
class="w-full bg-gray-50 border border-gray-200 rounded-xl px-3.5 py-2.5 font-bold focus:outline-none focus:border-black"
|
||
placeholder="巨幕厅 6排12座 6排10座" />
|
||
</div>
|
||
</div>
|
||
|
||
<button @click="createNewTicket"
|
||
class="w-full bg-black text-white py-3 rounded-xl font-bold text-xs hover:bg-gray-800 transition transform active:scale-95">
|
||
确认并导入票夹
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- MODAL: TICKET DETAILS & WALLET BARCODE (iOS Wallet Slide Up) -->
|
||
<div v-if="showDetailModal && selectedTicket"
|
||
class="fixed inset-0 bg-black/80 backdrop-blur-sm z-50 flex items-end sm:items-center justify-center p-0 sm:p-4">
|
||
<div
|
||
class="bg-[#111] text-white rounded-t-[32px] sm:rounded-[32px] p-6 max-w-md w-full shadow-2xl border border-neutral-800 transform transition-all flex flex-col space-y-6">
|
||
|
||
<!-- Header -->
|
||
<div class="flex justify-between items-center">
|
||
<div class="flex items-center gap-2">
|
||
<span class="w-2.5 h-2.5 rounded-full bg-red-500 animate-pulse"></span>
|
||
<span class="text-[10px] font-black tracking-widest uppercase text-gray-400">VRTICKET 电子乘车检票凭证</span>
|
||
</div>
|
||
<button @click="showDetailModal = false" class="text-neutral-400 hover:text-white transition">
|
||
<svg class="w-6 h-6" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Ticket Card Replica (Dark View) -->
|
||
<div class="bg-neutral-900 border border-neutral-800 rounded-2xl p-5 space-y-4">
|
||
<div>
|
||
<div class="text-[10px] text-gray-500 font-bold uppercase tracking-wider mb-0.5">{{ selectedTicket.cinema }}
|
||
</div>
|
||
<h4 class="text-lg font-black text-white leading-tight mb-1">{{ selectedTicket.movie }}</h4>
|
||
<div
|
||
class="text-[9px] bg-red-600 text-white font-extrabold px-1.5 py-0.5 rounded inline-block uppercase scale-95 origin-left">
|
||
{{ selectedTicket.format }}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Time & Seat details -->
|
||
<div class="grid grid-cols-2 gap-4 border-t border-b border-neutral-800 py-3 text-xs">
|
||
<div>
|
||
<span class="text-gray-500 block mb-0.5">放映时间</span>
|
||
<span class="text-xs font-bold text-white font-mono">{{ selectedTicket.date }} {{ selectedTicket.time
|
||
}}</span>
|
||
</div>
|
||
<div>
|
||
<span class="text-gray-500 block mb-0.5">位置信息</span>
|
||
<span class="text-xs font-bold text-white">{{ selectedTicket.hall }} <span
|
||
class="text-gray-400 font-normal">({{ selectedTicket.seats.length }}张)</span></span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Barcode simulation -->
|
||
<div class="flex flex-col items-center justify-center bg-white p-5 rounded-2xl space-y-4">
|
||
|
||
<svg class="w-full h-14 text-black" viewBox="0 0 100 40" preserveAspectRatio="none">
|
||
<rect x="0" y="0" width="100" height="40" fill="white" />
|
||
<path
|
||
d="M2,2 h2 v36 h-2 z M6,2 h1 v36 h-1 z M10,2 h3 v36 h-3 z M15,2 h1 v36 h-1 z M18,2 h4 v36 h-4 z M24,2 h1 v36 h-1 z M27,2 h2 v36 h-2 z M31,2 h4 v36 h-4 z M37,2 h1 v36 h-1 z M40,2 h2 v36 h-2 z M44,2 h3 v36 h-3 z M49,2 h1 v36 h-1 z M52,2 h4 v36 h-4 z M58,2 h1 v36 h-1 z M61,2 h2 v36 h-2 z M65,2 h4 v36 h-4 z M71,2 h1 v36 h-1 z M74,2 h2 v36 h-2 z M78,2 h3 v36 h-3 z M83,2 h1 v36 h-1 z M86,2 h4 v36 h-4 z M92,2 h1 v36 h-1 z M95,2 h3 v36 h-3 z"
|
||
fill="black" />
|
||
</svg>
|
||
|
||
<div class="font-mono text-[10px] font-black tracking-[0.25em] text-neutral-800">
|
||
TIX - {{ selectedTicket.code }}
|
||
</div>
|
||
|
||
<!-- Simulated QR code -->
|
||
<div class="w-32 h-32 bg-white p-1 border border-neutral-100 flex items-center justify-center">
|
||
<svg class="w-full h-full text-black" viewBox="0 0 100 100">
|
||
<rect width="100" height="100" fill="white" />
|
||
<rect x="5" y="5" width="25" height="25" fill="none" stroke="currentColor" stroke-width="6" />
|
||
<rect x="12" y="12" width="11" height="11" fill="currentColor" />
|
||
<rect x="70" y="5" width="25" height="25" fill="none" stroke="currentColor" stroke-width="6" />
|
||
<rect x="77" y="12" width="11" height="11" fill="currentColor" />
|
||
<rect x="5" y="70" width="25" height="25" fill="none" stroke="currentColor" stroke-width="6" />
|
||
<rect x="12" y="77" width="11" height="11" fill="currentColor" />
|
||
<rect x="35" y="15" width="5" height="5" fill="currentColor" />
|
||
<rect x="45" y="5" width="10" height="5" fill="currentColor" />
|
||
<rect x="50" y="20" width="5" height="15" fill="currentColor" />
|
||
<rect x="35" y="45" width="15" height="5" fill="currentColor" />
|
||
<rect x="15" y="45" width="5" height="15" fill="currentColor" />
|
||
<rect x="45" y="60" width="5" height="10" fill="currentColor" />
|
||
<rect x="5" y="40" width="10" height="5" fill="currentColor" />
|
||
<rect x="70" y="40" width="5" height="15" fill="currentColor" />
|
||
<rect x="80" y="45" width="15" height="5" fill="currentColor" />
|
||
<rect x="75" y="60" width="10" height="10" fill="currentColor" />
|
||
<rect x="90" y="70" width="5" height="10" fill="currentColor" />
|
||
<rect x="35" y="75" width="15" height="5" fill="currentColor" />
|
||
<rect x="35" y="85" width="5" height="10" fill="currentColor" />
|
||
<rect x="50" y="80" width="15" height="15" fill="currentColor" />
|
||
</svg>
|
||
</div>
|
||
|
||
<p class="text-[9px] text-gray-400 font-bold leading-none">请对准影院或检票闸机扫码,由检票员销退</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Actions -->
|
||
<div class="space-y-2.5">
|
||
<button v-if="!selectedTicket.verified" @click="verifyTicket(selectedTicket)"
|
||
class="w-full bg-[#e1251b] hover:bg-red-600 text-white py-3.5 rounded-xl font-bold text-xs tracking-wide transition-all flex items-center justify-center gap-2">
|
||
<svg class="w-4.5 h-4.5" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||
</svg>
|
||
确认模拟扫码核销
|
||
</button>
|
||
|
||
<!-- AI Movie review trigger button -->
|
||
<button @click="triggerAIMovieReport(selectedTicket)"
|
||
class="w-full bg-neutral-800 hover:bg-neutral-700 text-white py-3 rounded-xl font-bold text-xs tracking-wide transition-all flex items-center justify-center gap-2">
|
||
🤖 触发 Gemini 智能剧情讲解 & 朋友圈文案
|
||
</button>
|
||
|
||
<button @click="showDetailModal = false"
|
||
class="w-full bg-transparent text-gray-400 py-2 rounded-xl font-bold text-xs hover:text-white transition">
|
||
返回我的票夹
|
||
</button>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<!-- MODAL: INFO NOTIFICATIONS -->
|
||
<div v-if="showInfoModal"
|
||
class="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4">
|
||
<div class="bg-white rounded-3xl p-6 max-w-sm w-full shadow-2xl space-y-4 border border-gray-100">
|
||
<div class="w-12 h-12 rounded-full bg-red-50 text-red-500 flex items-center justify-center mx-auto">
|
||
<svg class="w-6 h-6" fill="none" stroke="currentColor" stroke-width="2.2" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round"
|
||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||
</svg>
|
||
</div>
|
||
<div class="space-y-1.5 text-center">
|
||
<h4 class="font-bold text-black text-sm">系统模拟提示</h4>
|
||
<div
|
||
class="text-xs text-gray-500 leading-relaxed max-h-60 overflow-y-auto custom-scroll text-left p-2.5 bg-gray-50 rounded-xl whitespace-pre-line"
|
||
v-html="infoModalText"></div>
|
||
</div>
|
||
<button @click="showInfoModal = false"
|
||
class="w-full bg-black text-white py-3 rounded-xl font-bold text-xs hover:bg-neutral-800 transition">
|
||
关闭
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<script>
|
||
// System Empty API key definition per requirement
|
||
const apiKey = "";
|
||
|
||
new Vue({
|
||
el: '#app',
|
||
data() {
|
||
return {
|
||
activeTab: 'tickets',
|
||
tickets: [],
|
||
showAddModal: false,
|
||
showDetailModal: false,
|
||
selectedTicket: null,
|
||
showInfoModal: false,
|
||
infoModalText: '',
|
||
userApiKey: '', // Optional user key from profile
|
||
// Chat state
|
||
chatInput: '',
|
||
chatMessages: [],
|
||
aiLoading: false,
|
||
newTicket: {
|
||
movie: '长津湖之水门桥',
|
||
cinema: '大悦城电影院',
|
||
date: '2022-02-05',
|
||
time: '10:00–12:29',
|
||
seatsInput: '巨幕厅 6排12座 6排111座 6排10座'
|
||
}
|
||
}
|
||
},
|
||
computed: {
|
||
// Unverified tickets at the top
|
||
activeUnverifiedTickets() {
|
||
return this.tickets.filter(t => !t.verified);
|
||
},
|
||
// Verified tickets sunk to bottom
|
||
verifiedTickets() {
|
||
return this.tickets.filter(t => t.verified);
|
||
}
|
||
},
|
||
created() {
|
||
this.resetTickets();
|
||
},
|
||
methods: {
|
||
resetTickets() {
|
||
const baseTicket = {
|
||
cinema: '大悦城电影院',
|
||
movie: '长津湖之水门桥',
|
||
format: '国语 2D',
|
||
date: '2022-02-05',
|
||
time: '10:00–12:29',
|
||
watermark: '演破场光赞时点',
|
||
hall: '巨幕厅',
|
||
seats: ['6排12座', '6排111座', '6排10座'],
|
||
posterAbbr: '水门桥',
|
||
posterEn: 'THE BATTLE AT LAKE',
|
||
verified: false,
|
||
code: '2938 1092 5811 02',
|
||
id: 1
|
||
};
|
||
|
||
const result = [];
|
||
for (let i = 1; i <= 12; i++) {
|
||
result.push({
|
||
...baseTicket,
|
||
id: i,
|
||
// Pre-verify ticket 11 & 12 to demonstrate bottom sinking layout immediately
|
||
verified: i >= 11,
|
||
code: `4829 ${9000 + i * 23} ${3400 + i * 11} ${10 + i}`,
|
||
posterAbbr: i % 2 === 0 ? '水门桥' : '红海行动',
|
||
posterEn: i % 2 === 0 ? 'THE BATTLE AT LAKE' : 'OPERATION RED SEA',
|
||
movie: i % 2 === 0 ? '长津湖之水门桥' : '红海行动重制版',
|
||
cinema: i % 3 === 0 ? '万达影城双井店' : '大悦城电影院'
|
||
});
|
||
}
|
||
this.tickets = result;
|
||
},
|
||
|
||
// Verify individual ticket with animated feedback
|
||
verifyTicket(ticket) {
|
||
const found = this.tickets.find(t => t.id === ticket.id);
|
||
if (found) {
|
||
found.verified = true;
|
||
this.showDetailModal = false;
|
||
this.triggerAction('alert', `🎉 核销成功!\n卡券「${ticket.movie}」已通过检票,为了保持票夹清爽,它已被自动整理到下方【已核销票券历史】中。`);
|
||
}
|
||
},
|
||
|
||
// One-click verify all to demonstrate bottom list
|
||
quickVerifyAll() {
|
||
this.tickets.forEach(t => t.verified = true);
|
||
this.triggerAction('alert', '全部 12 张票据已完成核销,票包顶端已清空,全部移入底端历史区域。');
|
||
},
|
||
|
||
clearVerifiedHistory() {
|
||
this.tickets = this.tickets.filter(t => !t.verified);
|
||
this.triggerAction('alert', '已核销票券历史记录已被彻底清空。');
|
||
},
|
||
|
||
openTicketDetail(ticket) {
|
||
this.selectedTicket = ticket;
|
||
this.showDetailModal = true;
|
||
},
|
||
|
||
triggerAction(type, target) {
|
||
if (type === 'phone') {
|
||
this.infoModalText = `正在虚拟呼叫 ${target} 客服热线:\n\n📞 400-880-9911\n\n[提示]:由于在安全沙箱中运行,呼叫将在本地虚拟完成。`;
|
||
this.showInfoModal = true;
|
||
} else if (type === 'map') {
|
||
this.infoModalText = `📌 正在规划前往「${target}」的路线:\n\n距离当前位置约 1.8 公里,步行预计需要 15 分钟。\n路线规划完毕,已推送给车载导航。`;
|
||
this.showInfoModal = true;
|
||
} else if (type === 'alert') {
|
||
this.infoModalText = target;
|
||
this.showInfoModal = true;
|
||
}
|
||
},
|
||
|
||
createNewTicket() {
|
||
const parts = this.newTicket.seatsInput.trim().split(/\s+/);
|
||
const hall = parts[0] || '巨幕厅';
|
||
const seats = parts.slice(1).length > 0 ? parts.slice(1) : ['6排12座'];
|
||
|
||
const created = {
|
||
id: Date.now(),
|
||
cinema: this.newTicket.cinema,
|
||
movie: this.newTicket.movie,
|
||
format: '国语 2D',
|
||
date: this.newTicket.date,
|
||
time: this.newTicket.time,
|
||
watermark: '演示出票系统',
|
||
hall: hall,
|
||
seats: seats,
|
||
posterAbbr: this.newTicket.movie.slice(0, 4),
|
||
posterEn: 'NEW HOT MOVIE',
|
||
verified: false,
|
||
code: Math.floor(Math.random() * 9000 + 1000) + ' ' + Math.floor(Math.random() * 9000 + 1000) + ' ' + Math.floor(Math.random() * 9000 + 1000) + ' ' + Math.floor(Math.random() * 90 + 10)
|
||
};
|
||
|
||
this.tickets.unshift(created);
|
||
this.showAddModal = false;
|
||
this.triggerAction('alert', `成功创建并录入一张《${created.movie}》门票,处于待核销高亮置顶状态!`);
|
||
},
|
||
|
||
// --- GEMINI API CAPABILITIES & RETRY LOGIC ---
|
||
async executeGeminiCall(payload) {
|
||
const keyToUse = this.userApiKey || apiKey;
|
||
const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key=${keyToUse}`;
|
||
|
||
let delay = 1000;
|
||
for (let i = 0; i < 5; i++) {
|
||
try {
|
||
const res = await fetch(url, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(payload)
|
||
});
|
||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||
return await res.json();
|
||
} catch (err) {
|
||
if (i === 4) throw err;
|
||
await new Promise(resolve => setTimeout(resolve, delay));
|
||
delay *= 2; // Exponential backoff
|
||
}
|
||
}
|
||
},
|
||
|
||
// 1. Natural Language Smart Ticket generation via chat Tab preset
|
||
triggerQuickAIShow() {
|
||
this.activeTab = 'ai-chat';
|
||
this.chatInput = '我想买一张下周六在梅赛德斯奔驰中心的陈奕迅上海演唱会VIP门票,两个位置';
|
||
this.sendChatMessage();
|
||
},
|
||
|
||
// 2. Movie analyses & Recommendations based on wallet collection
|
||
async triggerAIMovieRecom() {
|
||
this.activeTab = 'ai-chat';
|
||
this.aiLoading = true;
|
||
this.chatMessages.push({ role: 'user', content: '分析我的卡券夹,为我写一个关于目前中国主旋律与战争电影发展趋势的小结,并推荐3个近期演艺活动。' });
|
||
|
||
const currentMovies = this.tickets.map(t => t.movie).join(', ');
|
||
const systemPrompt = "你是一个拥有极高专业度的潮流票务分析师与资深影评人。请精简、幽默、排版优美地给用户总结出他们所看的电影类别发展,并给出相应的线下live演出或话剧演出等推荐建议。";
|
||
const payload = {
|
||
contents: [{ parts: [{ text: `我目前的票夹里有以下电影票据:${currentMovies}。请帮我进行潮流分析及线下推荐。` }] }],
|
||
systemInstruction: { parts: [{ text: systemPrompt }] }
|
||
};
|
||
|
||
try {
|
||
const data = await this.executeGeminiCall(payload);
|
||
const rawText = data.candidates?.[0]?.content?.parts?.[0]?.text || "抱歉,Gemini 服务端暂未做出回应,请检查网络设置。";
|
||
this.chatMessages.push({ role: 'model', content: rawText });
|
||
} catch (e) {
|
||
this.chatMessages.push({ role: 'model', content: "🚨 连接到智能核心出错,请稍后重试或在「我的」里绑定您自备的 API Key。" });
|
||
} finally {
|
||
this.aiLoading = false;
|
||
this.scrollChat();
|
||
}
|
||
},
|
||
|
||
// 3. Movie Companion summary and copy generation
|
||
async triggerAIMovieReport(ticket) {
|
||
this.showDetailModal = false;
|
||
this.activeTab = 'ai-chat';
|
||
this.aiLoading = true;
|
||
this.chatMessages.push({ role: 'user', content: `写一个关于电影《${ticket.movie}》的观影分析和适合发小红书/朋友圈的爆款打卡文案。` });
|
||
|
||
const systemPrompt = "你是一个精通影迷社群运营的文案写手。擅长用潮流、热血或走心的风格写高赞朋友圈打卡小论文。";
|
||
const payload = {
|
||
contents: [{ parts: [{ text: `我正在影院等待核销《${ticket.movie}》的演出票。请帮我写一段深度讲解影评,并附带适合打卡的朋友圈爆赞打卡小短文(要求带上Emoji,风格生动有趣)。` }] }],
|
||
systemInstruction: { parts: [{ text: systemPrompt }] }
|
||
};
|
||
|
||
try {
|
||
const data = await this.executeGeminiCall(payload);
|
||
const rawText = data.candidates?.[0]?.content?.parts?.[0]?.text || "无法生成文案。";
|
||
this.chatMessages.push({ role: 'model', content: rawText });
|
||
} catch (e) {
|
||
this.chatMessages.push({ role: 'model', content: "🚨 文案合成失败,请尝试重启系统。" });
|
||
} finally {
|
||
this.aiLoading = false;
|
||
this.scrollChat();
|
||
}
|
||
},
|
||
|
||
// Chat operations
|
||
async sendChatMessage() {
|
||
if (!this.chatInput.trim() || this.aiLoading) return;
|
||
const userQuery = this.chatInput.trim();
|
||
this.chatInput = '';
|
||
this.chatMessages.push({ role: 'user', content: userQuery });
|
||
this.aiLoading = true;
|
||
this.scrollChat();
|
||
|
||
// Formulate system prompt with strict schema control if user wants to buy tickets
|
||
const systemPrompt = `你是一个全能的票务管理大脑 VRTicket AI 秘僚。
|
||
1. 只要用户提出买票、订演出、看电影等相关的指令,请在回复文本的最末尾(或者是你觉得合适的地方)添加一个特定的用于导入票夹的JSON对象,格式严禁变化:
|
||
{"movie": "名字", "cinema": "场馆", "format": "制式类型", "date": "YYYY-MM-DD", "time": "10:00–12:29", "watermark": "7字简标", "hall": "放映厅名", "seats": ["6排12座", "6排10座"]}。
|
||
请将其原封不动地写在回复文本的最底部。不要在JSON外裹挟三个反引号的代码块,只需单纯输出合法的JSON单行。
|
||
2. 对于常规咨询和闲聊,请扮演专业而风趣的助理进行回答,并适当使用Emoji美化。`;
|
||
|
||
const payload = {
|
||
contents: [{ parts: [{ text: userQuery }] }],
|
||
systemInstruction: { parts: [{ text: systemPrompt }] }
|
||
};
|
||
|
||
try {
|
||
const data = await this.executeGeminiCall(payload);
|
||
const responseText = data.candidates?.[0]?.content?.parts?.[0]?.text || "暂无智能答复。";
|
||
|
||
// Extract json payload if exists
|
||
let parsedTicket = null;
|
||
const jsonRegex = /\{"movie":[\s\S]*?\}/;
|
||
const match = responseText.match(jsonRegex);
|
||
let cleanedText = responseText;
|
||
|
||
if (match) {
|
||
try {
|
||
parsedTicket = JSON.parse(match[0]);
|
||
cleanedText = responseText.replace(jsonRegex, '').trim();
|
||
} catch (err) {
|
||
// Silent JSON parse fail fallback
|
||
}
|
||
}
|
||
|
||
this.chatMessages.push({
|
||
role: 'model',
|
||
content: cleanedText || "这是为您量身定制的专属虚拟演出门票,点击下方一键录入票夹:",
|
||
metaTicket: parsedTicket
|
||
});
|
||
|
||
} catch (err) {
|
||
this.chatMessages.push({ role: 'model', content: "连接出错。这可能是因为系统演示密钥受到频次限制,请在「我的」标签页中填入自己的 API 密钥。" });
|
||
} finally {
|
||
this.aiLoading = false;
|
||
this.scrollChat();
|
||
}
|
||
},
|
||
|
||
// Injects AI ticket from chat bubble into interactive list
|
||
addAITicket(ticketData) {
|
||
const created = {
|
||
id: Date.now(),
|
||
cinema: ticketData.cinema || '虚拟大剧院',
|
||
movie: ticketData.movie || 'AI 梦境公演',
|
||
format: ticketData.format || '演唱会 VIP',
|
||
date: ticketData.date || '2026-06-01',
|
||
time: ticketData.time || '19:30–21:30',
|
||
watermark: ticketData.watermark || 'AI智能出票',
|
||
hall: ticketData.hall || 'VIP观演区',
|
||
seats: ticketData.seats || ['1排1座'],
|
||
posterAbbr: (ticketData.movie || '梦境').slice(0, 4),
|
||
posterEn: 'AI EXPERIENCE',
|
||
verified: false,
|
||
code: '8201 ' + Math.floor(Math.random() * 9000 + 1000) + ' ' + Math.floor(Math.random() * 9000 + 1000) + ' 99'
|
||
};
|
||
|
||
this.tickets.unshift(created);
|
||
this.activeTab = 'tickets';
|
||
this.triggerAction('alert', `🎆 AI 出票成功!\n\n卡券《${created.movie}》已被置顶存入票夹。快去底部的[票夹]看看它的超酷样式吧!`);
|
||
},
|
||
|
||
submitChatPreset(label, query) {
|
||
this.chatInput = query;
|
||
this.sendChatMessage();
|
||
},
|
||
|
||
clearChat() {
|
||
this.chatMessages = [];
|
||
},
|
||
|
||
scrollChat() {
|
||
this.$nextTick(() => {
|
||
const scroller = document.getElementById('chat-scroller');
|
||
if (scroller) {
|
||
scroller.scrollTop = scroller.scrollHeight;
|
||
}
|
||
});
|
||
},
|
||
|
||
formatMessage(text) {
|
||
// Simple markdown parsing for rendering nicely
|
||
return text
|
||
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
||
.replace(/\n/g, '<br/>');
|
||
}
|
||
}
|
||
});
|
||
</script>
|
||
</body>
|
||
|
||
</html> |