vr-shopxo-uniapp/docs/showstart_ticketing_app.html

1243 lines
63 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!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:0012: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:0012: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:0012: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:3021: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>