12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478 |
- <!DOCTYPE html>
- <html lang="en" data-theme="light">
- <head>
- <meta charset="UTF-8" />
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
- <title>POG Airdrop</title>
- <link
- rel="icon"
- type="image/png"
- href="https://portal.telgather.com/h/imgs/icon_tog.png"
- />
- <script src="https://cdn.tailwindcss.com"></script>
- <link
- href="https://cdn.bootcdn.net/ajax/libs/daisyui/4.12.23/full.css"
- rel="stylesheet"
- type="text/css"
- />
- <script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.13/vue.global.min.js"></script>
- <link
- href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"
- rel="stylesheet"
- />
- <style>
- @import url("https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap");
- * {
- font-family: "Inter", sans-serif;
- }
- .gradient-bg {
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- }
- .gradient-bg-2 {
- background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
- }
- .gradient-bg-3 {
- background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
- }
- .card-hover {
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
- }
- .card-hover:hover {
- transform: translateY(-4px);
- box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
- }
- .scrollbar-thin {
- scrollbar-width: thin;
- scrollbar-color: #cbd5e0 #f7fafc;
- }
- .scrollbar-thin::-webkit-scrollbar {
- width: 6px;
- }
- .scrollbar-thin::-webkit-scrollbar-track {
- background: #f7fafc;
- }
- .scrollbar-thin::-webkit-scrollbar-thumb {
- background: #cbd5e0;
- border-radius: 3px;
- }
- .animate-pulse-slow {
- animation: pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite;
- }
- .animate-bounce-slow {
- animation: bounce 2s infinite;
- }
- .glass-effect {
- background: rgba(255, 255, 255, 0.25);
- backdrop-filter: blur(10px);
- border: 1px solid rgba(255, 255, 255, 0.18);
- }
- .neon-glow {
- box-shadow: 0 0 20px rgba(102, 126, 234, 0.5);
- }
- .floating {
- animation: floating 3s ease-in-out infinite;
- }
- @keyframes floating {
- 0% {
- transform: translateY(0px);
- }
- 50% {
- transform: translateY(-10px);
- }
- 100% {
- transform: translateY(0px);
- }
- }
- .stat-card {
- background: linear-gradient(
- 135deg,
- rgba(255, 255, 255, 0.1) 0%,
- rgba(255, 255, 255, 0.05) 100%
- );
- backdrop-filter: blur(10px);
- border: 1px solid rgba(255, 255, 255, 0.2);
- }
- .reward-item {
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- color: white;
- transition: all 0.3s ease;
- }
- .reward-item:hover {
- transform: scale(1.05);
- box-shadow: 0 10px 25px rgba(102, 126, 234, 0.3);
- }
- .address-item {
- transition: all 0.3s ease;
- }
- .address-item:hover {
- background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
- color: white;
- transform: translateX(5px);
- }
- .btn-glow {
- position: relative;
- overflow: hidden;
- }
- .btn-glow::before {
- content: "";
- position: absolute;
- top: 0;
- left: -100%;
- width: 100%;
- height: 100%;
- background: linear-gradient(
- 90deg,
- transparent,
- rgba(255, 255, 255, 0.4),
- transparent
- );
- transition: left 0.5s;
- }
- .btn-glow:hover::before {
- left: 100%;
- }
- .item-icon {
- width: 100%;
- height: 100%;
- object-fit: contain;
- transition: transform 0.3s ease;
- }
- .item-icon:hover {
- transform: scale(1.1);
- }
- .icon-container {
- display: flex;
- align-items: center;
- justify-content: center;
- background: rgba(255, 255, 255, 0.1);
- border: 1px solid rgba(255, 255, 255, 0.2);
- border-radius: 50%;
- overflow: hidden;
- transition: all 0.3s ease;
- }
- .icon-container:hover {
- background: rgba(255, 255, 255, 0.2);
- border-color: rgba(255, 255, 255, 0.4);
- }
- /* Toast Animation */
- .toast-enter-active,
- .toast-leave-active {
- transition: all 0.3s ease;
- }
- .toast-enter-from {
- opacity: 0;
- transform: translateX(100%);
- }
- .toast-leave-to {
- opacity: 0;
- transform: translateX(100%);
- }
- .toast-enter-to,
- .toast-leave-from {
- opacity: 1;
- transform: translateX(0);
- }
- .animate-slide-in {
- animation: slideIn 0.3s ease-out;
- }
- @keyframes slideIn {
- from {
- opacity: 0;
- transform: translateX(100%);
- }
- to {
- opacity: 1;
- transform: translateX(0);
- }
- }
- </style>
- </head>
- <body
- class="min-h-screen bg-gradient-to-br from-indigo-900 via-purple-900 to-pink-900"
- >
- <div id="app" class="container mx-auto p-4 md:p-8">
- <!-- Animated Background -->
- <div class="fixed inset-0 overflow-hidden pointer-events-none">
- <div
- class="absolute -top-40 -right-40 w-80 h-80 bg-purple-500 rounded-full mix-blend-multiply filter blur-xl opacity-20 animate-bounce-slow"
- ></div>
- <div
- class="absolute -bottom-40 -left-40 w-80 h-80 bg-pink-500 rounded-full mix-blend-multiply filter blur-xl opacity-20 animate-bounce-slow"
- style="animation-delay: 1s"
- ></div>
- <div
- class="absolute top-40 left-40 w-80 h-80 bg-blue-500 rounded-full mix-blend-multiply filter blur-xl opacity-20 animate-bounce-slow"
- style="animation-delay: 2s"
- ></div>
- </div>
- <!-- Header -->
- <div class="text-center mb-12 relative z-10">
- <div class="floating mb-4">
- <i class="fas fa-rocket text-6xl text-white mb-4"></i>
- </div>
- <h1 class="text-5xl font-bold text-white mb-4">POG Airdrop</h1>
- <p class="text-xl text-purple-200">Mass Reward Distribution System</p>
- <div class="flex justify-center mt-6 space-x-4">
- <div class="stat-card rounded-lg p-4 text-white">
- <div class="text-2xl font-bold">{{ addressList.length }}</div>
- <div class="text-sm opacity-80">Addresses</div>
- </div>
- <div class="stat-card rounded-lg p-4 text-white">
- <div class="text-2xl font-bold">{{ totalRewardCount }}</div>
- <div class="text-sm opacity-80">Rewards</div>
- </div>
- <div class="stat-card rounded-lg p-4 text-white">
- <div class="text-2xl font-bold">
- {{ totalRewardCount * addressList.length }}
- </div>
- <div class="text-sm opacity-80">Total</div>
- </div>
- </div>
- <!-- History Button -->
- <div class="mt-6">
- <button
- class="btn btn-secondary btn-lg gradient-bg border-0 text-white shadow-lg btn-glow"
- @click="showHistoryDialog = true"
- >
- <i class="fas fa-history mr-2"></i>
- View History
- </button>
- </div>
- </div>
- <div class="space-y-8 relative z-10">
- <!-- Step 1: Address Management -->
- <div class="glass-effect rounded-2xl shadow-2xl card-hover">
- <div class="p-6">
- <div class="flex items-center mb-6">
- <div
- class="w-12 h-12 bg-gradient-to-r from-blue-500 to-purple-600 rounded-full flex items-center justify-center text-white font-bold text-xl mr-4"
- >
- 1
- </div>
- <h2 class="text-3xl font-bold text-white">
- <i class="fas fa-users text-blue-300 mr-3"></i>
- Step 1: Add Wallet Addresses
- </h2>
- </div>
- <div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
- <!-- Address Input -->
- <div>
- <div class="form-control">
- <label class="label">
- <span class="label-text font-semibold text-white text-lg"
- >Wallet Address List</span
- >
- <span class="label-text-alt text-purple-300"
- >{{ addressList.length }} addresses</span
- >
- </label>
- <textarea
- v-model="waitInputAddresses"
- rows="10"
- placeholder="Enter wallet addresses, one per line Example: 0x1234567890abcdef... 0xabcdef1234567890..."
- class="textarea textarea-bordered w-full font-mono text-sm bg-white/10 border-white/30 text-white placeholder-white/50"
- @input="parseAddresses"
- ></textarea>
- <label class="label">
- <span class="label-text-alt text-purple-200"
- >Support batch paste, one address per line</span
- >
- </label>
- </div>
- <!-- Quick Actions -->
- <div class="flex flex-wrap gap-2 mt-4">
- <button
- class="btn btn-sm btn-outline border-white/30 text-white hover:bg-white/10"
- @click="clearAddresses"
- >
- <i class="fas fa-trash mr-1"></i> Clear
- </button>
- <button
- class="btn btn-sm btn-outline border-white/30 text-white hover:bg-white/10"
- @click="exportAddresses"
- >
- <i class="fas fa-download mr-1"></i> Export
- </button>
- <button
- class="btn btn-sm btn-outline border-white/30 text-white hover:bg-white/10"
- @click="importAddresses"
- >
- <i class="fas fa-upload mr-1"></i> Import
- </button>
- </div>
- </div>
- <!-- Address List -->
- <div>
- <div class="flex justify-between items-center mb-4">
- <h3 class="text-xl font-semibold text-white">
- <i class="fas fa-list text-orange-300 mr-2"></i>
- Address List
- </h3>
- <div class="flex items-center space-x-3">
- <input
- type="text"
- v-model="searchAddress"
- placeholder="Search addresses..."
- class="input input-bordered input-sm w-48 bg-white/10 border-white/30 text-white placeholder-white/50"
- />
- <div class="badge badge-outline border-white/30 text-white">
- {{ filteredAddresses.length }}/{{ addressList.length }}
- </div>
- </div>
- </div>
- <div v-if="addressList.length === 0" class="text-center py-8">
- <div class="text-6xl mb-4 animate-bounce-slow">👤</div>
- <p class="text-purple-200 text-lg">No addresses yet</p>
- <p class="text-purple-300 text-sm mt-2">
- Please enter addresses on the left
- </p>
- </div>
- <div v-else class="max-h-80 overflow-y-auto scrollbar-thin">
- <div
- v-for="(address, index) in filteredAddresses"
- :key="index"
- class="address-item flex justify-between items-center p-3 rounded-lg mb-2 bg-white/5 backdrop-blur-sm"
- >
- <div class="flex items-center space-x-4">
- <div
- class="w-8 h-8 bg-gradient-to-r from-purple-400 to-pink-400 rounded-full flex items-center justify-center text-xs font-bold text-white"
- >
- {{ index + 1 }}
- </div>
- <div class="font-mono text-sm text-white">
- {{ address }}
- </div>
- </div>
- <button
- class="btn btn-circle btn-sm bg-red-500 border-0 hover:bg-red-600"
- @click="removeAddress(index)"
- >
- <i class="fas fa-times"></i>
- </button>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- <!-- Step 2: Reward Management -->
- <div class="glass-effect rounded-2xl shadow-2xl card-hover">
- <div class="p-6">
- <div class="flex items-center mb-6">
- <div
- class="w-12 h-12 bg-gradient-to-r from-purple-500 to-pink-600 rounded-full flex items-center justify-center text-white font-bold text-xl mr-4"
- >
- 2
- </div>
- <h2 class="text-3xl font-bold text-white">
- <i class="fas fa-gift text-purple-300 mr-3"></i>
- Step 2: Select Rewards
- </h2>
- <div class="badge badge-primary badge-lg animate-pulse-slow ml-4">
- {{ waitAddRewardList.length }} Types
- </div>
- </div>
- <div
- v-if="waitAddRewardList.length === 0"
- class="text-center py-12"
- >
- <div class="text-8xl mb-6 animate-bounce-slow">🎁</div>
- <p class="text-purple-200 text-lg">No rewards added yet</p>
- <p class="text-purple-300 text-sm mt-2">
- Click the button below to add rewards
- </p>
- </div>
- <div
- v-else
- class="space-y-4 max-h-80 overflow-y-auto scrollbar-thin"
- >
- <div
- v-for="reward in waitAddRewardList"
- :key="reward.id"
- class="reward-item rounded-xl p-4 border border-white/20"
- >
- <div class="flex justify-between items-center">
- <div class="flex items-center space-x-4">
- <div class="w-12 h-12 icon-container backdrop-blur-sm">
- <img
- :src="getItemIcon(reward.id)"
- :alt="getItemName(reward.id)"
- class="w-8 h-8 item-icon"
- @error="handleImageError"
- />
- </div>
- <div>
- <div class="font-semibold text-lg">
- {{ getItemName(reward.id) }}
- </div>
- <div class="text-sm opacity-80">
- {{ reward.count }} per address
- </div>
- </div>
- </div>
- <div class="flex items-center space-x-3">
- <input
- type="number"
- v-model="reward.count"
- min="1"
- class="input input-bordered input-sm w-20 text-center bg-white/20 border-white/30 text-white placeholder-white/50"
- @change="updateRewardCount(reward.id, reward.count)"
- />
- <button
- class="btn btn-circle btn-sm bg-red-500 border-0 hover:bg-red-600"
- @click="removeWaitReward(reward.id)"
- >
- <i class="fas fa-times"></i>
- </button>
- </div>
- </div>
- </div>
- </div>
- <div class="text-center mt-6">
- <button
- class="btn btn-primary btn-lg gradient-bg-2 border-0 text-white shadow-lg btn-glow"
- @click="showAddRewardDialog = true"
- >
- <i class="fas fa-plus mr-2"></i>
- Add Reward
- </button>
- </div>
- </div>
- </div>
- <!-- Step 3: Send Preview -->
- <div class="glass-effect rounded-2xl shadow-2xl card-hover">
- <div class="p-6">
- <div class="flex items-center mb-6">
- <div
- class="w-12 h-12 bg-gradient-to-r from-green-500 to-blue-600 rounded-full flex items-center justify-center text-white font-bold text-xl mr-4"
- >
- 3
- </div>
- <h2 class="text-3xl font-bold text-white">
- <i class="fas fa-chart-line text-green-300 mr-3"></i>
- Step 3: Review Summary
- </h2>
- </div>
- <!-- Statistics -->
- <div class="grid grid-cols-3 gap-6 mb-8">
- <div class="stat-card rounded-xl p-6 text-center">
- <div class="text-4xl font-bold text-white mb-2">
- {{ addressList.length }}
- </div>
- <div class="text-lg text-purple-200">Addresses</div>
- </div>
- <div class="stat-card rounded-xl p-6 text-center">
- <div class="text-4xl font-bold text-white mb-2">
- {{ totalRewardCount }}
- </div>
- <div class="text-lg text-purple-200">Rewards</div>
- </div>
- <div class="stat-card rounded-xl p-6 text-center">
- <div class="text-4xl font-bold text-white mb-2">
- {{ totalRewardCount * addressList.length }}
- </div>
- <div class="text-lg text-purple-200">Total</div>
- </div>
- </div>
- <!-- Item Summary -->
- <div v-if="waitAddRewardList.length > 0">
- <h3 class="text-xl font-semibold text-white mb-4">
- Item Summary
- </h3>
- <div
- class="grid grid-cols-1 md:grid-cols-2 gap-4 max-h-60 overflow-y-auto scrollbar-thin"
- >
- <div
- v-for="reward in waitAddRewardList"
- :key="reward.id"
- class="flex justify-between items-center p-4 bg-white/10 rounded-lg backdrop-blur-sm"
- >
- <div class="flex items-center space-x-3">
- <div class="w-10 h-10 icon-container">
- <img
- :src="getItemIcon(reward.id)"
- :alt="getItemName(reward.id)"
- class="w-8 h-8 item-icon"
- @error="handleImageError"
- />
- </div>
- <span class="font-medium text-white text-lg"
- >{{ getItemName(reward.id) }}</span
- >
- </div>
- <div class="flex items-center space-x-2">
- <span class="text-sm text-purple-200"
- >{{ reward.count }} × {{ addressList.length }}</span
- >
- <span class="font-bold text-yellow-300 text-lg"
- >= {{ reward.count * addressList.length }}</span
- >
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- <!-- Step 4: Launch Airdrop -->
- <div class="glass-effect rounded-2xl shadow-2xl card-hover">
- <div class="p-6">
- <div class="flex items-center mb-6">
- <div
- class="w-12 h-12 bg-gradient-to-r from-red-500 to-orange-600 rounded-full flex items-center justify-center text-white font-bold text-xl mr-4"
- >
- 4
- </div>
- <h2 class="text-3xl font-bold text-white">
- <i class="fas fa-rocket text-red-300 mr-3"></i>
- Step 4: Launch Airdrop
- </h2>
- </div>
- <div class="text-center">
- <div class="mb-8">
- <p class="text-xl text-purple-200 mb-4">
- Ready to send rewards to {{ addressList.length }} addresses
- </p>
- <p class="text-lg text-purple-300">
- Total rewards to distribute: {{ totalRewardCount *
- addressList.length }}
- </p>
- </div>
- <button
- class="btn btn-primary btn-xl gradient-bg-3 border-0 text-white shadow-2xl btn-glow"
- @click="sendAllRewards"
- :disabled="addressList.length === 0 || waitAddRewardList.length === 0"
- >
- <i class="fas fa-rocket mr-3 animate-bounce-slow"></i>
- Launch Airdrop ({{ totalRewardCount * addressList.length }}
- rewards)
- </button>
- </div>
- </div>
- </div>
- </div>
- <!-- Add Reward Modal -->
- <div class="modal" :class="{ 'modal-open': showAddRewardDialog }">
- <div
- class="modal-box w-11/12 max-w-4xl bg-gradient-to-br from-gray-900/95 to-black/95 backdrop-blur-xl border border-white/20 shadow-2xl"
- >
- <h3 class="font-bold text-3xl mb-8 text-white text-center">
- <i class="fas fa-gift text-purple-300 mr-2"></i>
- Add Reward
- </h3>
- <div class="space-y-6 max-h-[80vh] overflow-y-auto">
- <div class="form-control">
- <label class="label">
- <span class="label-text font-semibold text-white text-lg"
- >Select Item</span
- >
- </label>
- <div
- class="grid grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4 max-h-96 overflow-y-auto scrollbar-thin"
- >
- <div
- v-for="item in staticGoods"
- :key="item.id"
- @click="selectedReward = item.id"
- :class="[
- 'p-4 rounded-xl border-2 cursor-pointer transition-all duration-200 hover:scale-105',
- selectedReward == item.id
- ? 'border-purple-400 bg-purple-500/20 shadow-lg shadow-purple-500/30'
- : 'border-white/20 bg-white/5 hover:bg-white/10 hover:border-white/40'
- ]"
- >
- <div class="flex flex-col items-center space-y-3 text-center">
- <div class="w-16 h-16 icon-container">
- <img
- :src="getItemIcon(item.id)"
- :alt="item.name"
- class="w-12 h-12 item-icon"
- @error="handleImageError"
- />
- </div>
- <div>
- <div class="font-semibold text-white text-base">
- {{ item.name }}
- </div>
- <div class="text-sm text-purple-200 mt-1">
- Stock: {{ item.count }}
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- <div class="form-control">
- <label class="label">
- <span class="label-text font-semibold text-white text-lg"
- >Quantity per Address</span
- >
- </label>
- <input
- type="number"
- v-model="rewardQuantity"
- min="1"
- class="input input-bordered w-full bg-white/10 border-white/30 text-white placeholder-white/50 text-lg text-center"
- placeholder="Enter quantity"
- />
- </div>
- </div>
- <div class="modal-action">
- <button
- class="btn btn-primary btn-lg gradient-bg-2 border-0 text-white"
- @click="addRewardConfirm"
- :disabled="!selectedReward || !rewardQuantity || rewardQuantity <= 0"
- >
- <i class="fas fa-check mr-2"></i>
- Add Reward
- </button>
- <button class="btn btn-ghost btn-lg text-white" @click="closeModal">
- Cancel
- </button>
- </div>
- </div>
- <div
- class="modal-backdrop bg-black/50 backdrop-blur-sm"
- @click="closeModal"
- ></div>
- </div>
- <!-- Password Verification Modal -->
- <div class="modal" :class="{ 'modal-open': showPasswordDialog }">
- <div
- class="modal-box max-w-md bg-gradient-to-br from-gray-900/95 to-black/95 backdrop-blur-xl border border-white/20 shadow-2xl"
- >
- <h3 class="font-bold text-2xl mb-6 text-white text-center">
- <i class="fas fa-shield-alt text-red-300 mr-2"></i>
- Final Confirmation
- </h3>
- <div class="space-y-6">
- <div class="text-center">
- <div class="text-6xl mb-4 animate-bounce-slow">🔐</div>
- <p class="text-purple-200 text-lg mb-2">
- Confirm Airdrop Operation
- </p>
- <p class="text-purple-300 text-sm">
- This action will send {{ totalRewardCount * addressList.length
- }} rewards to {{ addressList.length }} addresses
- </p>
- </div>
- <div class="grid grid-cols-1 gap-6">
- <div class="form-control">
- <label class="label">
- <span class="label-text font-semibold text-white text-lg"
- ><i class="fas fa-tag text-blue-300 mr-2"></i>Activity
- Name</span
- >
- </label>
- <input
- type="text"
- v-model="reason"
- class="input input-bordered w-full bg-white/10 border-white/30 text-white placeholder-white/50 text-lg text-center"
- placeholder="Enter activity name (e.g., Spring Festival Event)"
- @keyup.enter="verifyPassword"
- />
- <label class="label">
- <span class="label-text-alt text-purple-200"
- >This will be used to identify the airdrop activity</span
- >
- </label>
- </div>
- <div class="form-control">
- <label class="label">
- <span class="label-text font-semibold text-white text-lg"
- ><i class="fas fa-lock text-red-300 mr-2"></i>Admin
- Password</span
- >
- </label>
- <input
- type="password"
- v-model="password"
- class="input input-bordered w-full bg-white/10 border-white/30 text-white placeholder-white/50 text-lg text-center"
- placeholder="Enter password"
- @keyup.enter="verifyPassword"
- />
- <label class="label">
- <span class="label-text-alt text-purple-200"
- >Press Enter to confirm</span
- >
- </label>
- </div>
- </div>
- </div>
- <div class="modal-action">
- <button
- class="btn btn-primary btn-lg gradient-bg-3 border-0 text-white"
- @click="verifyPassword"
- :disabled="!password.trim() || !reason.trim() || isVerifying"
- >
- <i v-if="isVerifying" class="fas fa-spinner fa-spin mr-2"></i>
- <i v-else class="fas fa-rocket mr-2"></i>
- {{ isVerifying ? 'Verifying...' : 'Launch Airdrop' }}
- </button>
- <button
- class="btn btn-ghost btn-lg text-white"
- @click="closePasswordModal"
- >
- Cancel
- </button>
- </div>
- </div>
- <div
- class="modal-backdrop bg-black/50 backdrop-blur-sm"
- @click="closePasswordModal"
- ></div>
- </div>
- <!-- History Modal -->
- <div class="modal" :class="{ 'modal-open': showHistoryDialog }">
- <div
- class="modal-box w-11/12 max-w-5xl bg-gradient-to-br from-gray-900/95 to-black/95 backdrop-blur-xl border border-white/20 shadow-2xl"
- >
- <div class="flex justify-between items-center mb-4">
- <h3 class="font-bold text-2xl text-white">
- <i class="fas fa-history text-blue-300 mr-2"></i>
- Airdrop History
- </h3>
- <button
- class="btn btn-circle btn-sm bg-white/20 border-0 hover:bg-white/30"
- @click="closeHistoryModal"
- >
- <i class="fas fa-times text-white"></i>
- </button>
- </div>
- <!-- Loading State -->
- <div v-if="isLoadingHistory" class="text-center py-8">
- <div class="text-4xl mb-2 animate-bounce-slow">⏳</div>
- <p class="text-purple-200">Loading history...</p>
- </div>
- <!-- History Content -->
- <div
- v-else-if="historyData.length > 0"
- class="space-y-3 max-h-[75vh] overflow-y-auto"
- >
- <div
- v-for="item in historyData"
- :key="item.id"
- class="bg-white/10 rounded-lg p-4 backdrop-blur-sm border border-white/20"
- >
- <!-- Header Row -->
- <div class="flex justify-between items-center mb-3">
- <div class="flex items-center space-x-3">
- <h4 class="text-lg font-bold text-white">
- {{ item.reason }}
- </h4>
- <div
- :class="[
- 'badge badge-xs',
- item.status === 1 ? 'badge-success' : 'badge-error'
- ]"
- >
- {{ item.status === 1 ? '✓' : '✗' }}
- </div>
- </div>
- <div
- class="flex items-center space-x-3 text-xs text-purple-300"
- >
- <span>
- <i class="fas fa-calendar mr-1"></i>
- {{ formatDate(item.createTime) }}
- </span>
- <span class="text-purple-400">#{{ item.id.slice(-8) }}</span>
- </div>
- </div>
- <!-- Rewards List - Compact -->
- <div class="flex flex-wrap gap-2">
- <div
- v-for="reward in parseGoodList(item.goodList)"
- :key="reward.id"
- class="flex items-center space-x-2 px-3 py-2 bg-white/10 rounded-lg border border-white/20"
- >
- <div class="w-6 h-6 icon-container">
- <img
- :src="getItemIcon(reward.id)"
- :alt="getItemName(reward.id)"
- class="w-5 h-5 item-icon"
- @error="handleImageError"
- />
- </div>
- <span class="text-sm font-medium text-white">
- {{ getItemName(reward.id) }}
- </span>
- <span
- class="text-xs text-purple-200 bg-purple-500/20 px-2 py-1 rounded"
- >
- ×{{ reward.count }}
- </span>
- </div>
- </div>
- </div>
- </div>
- <!-- Empty State -->
- <div v-else class="text-center py-8">
- <div class="text-6xl mb-3 animate-bounce-slow">📜</div>
- <p class="text-purple-200">No history found</p>
- <p class="text-purple-300 text-sm mt-1">
- Start your first airdrop to see history here
- </p>
- </div>
- <!-- Pagination - Compact -->
- <div
- v-if="historyData.length > 0"
- class="flex justify-center items-center space-x-3 mt-4 pt-3 border-t border-white/20"
- >
- <button
- class="btn btn-sm btn-outline border-white/30 text-white hover:bg-white/10"
- @click="changePage(currentPage - 1)"
- :disabled="currentPage <= 1"
- >
- <i class="fas fa-chevron-left"></i>
- </button>
- <div class="flex items-center space-x-2">
- <span class="text-sm text-white">Page</span>
- <input
- type="number"
- v-model.number="currentPage"
- min="1"
- class="input input-bordered input-xs w-16 text-center bg-white/10 border-white/30 text-white"
- @change="loadHistory"
- />
- <span class="text-sm text-white">/ {{ totalPages }}</span>
- </div>
- <button
- class="btn btn-sm btn-outline border-white/30 text-white hover:bg-white/10"
- @click="changePage(currentPage + 1)"
- :disabled="currentPage >= totalPages"
- >
- <i class="fas fa-chevron-right"></i>
- </button>
- </div>
- </div>
- <div
- class="modal-backdrop bg-black/50 backdrop-blur-sm"
- @click="closeHistoryModal"
- ></div>
- </div>
- <!-- Toast Container -->
- <div class="fixed top-4 right-4 z-50">
- <div
- v-if="toast.show"
- :class="['alert', toast.type, 'glass-effect', 'shadow-2xl', 'min-w-80', 'max-w-md', 'animate-slide-in']"
- >
- <div class="flex items-center justify-between w-full">
- <div class="flex items-center">
- <i
- v-if="toast.type === 'alert-success'"
- class="fas fa-check-circle text-green-400 mr-3"
- ></i>
- <i
- v-else-if="toast.type === 'alert-error'"
- class="fas fa-exclamation-circle text-red-400 mr-3"
- ></i>
- <i v-else class="fas fa-info-circle text-blue-400 mr-3"></i>
- <span class="text-white font-medium">{{ toast.message }}</span>
- </div>
- <button
- @click="toast.show = false"
- class="btn btn-circle btn-sm bg-white/20 border-0 hover:bg-white/30 ml-3"
- >
- <i class="fas fa-times text-white"></i>
- </button>
- </div>
- </div>
- </div>
- </div>
- <script>
- const { createApp } = Vue;
- const API_BASE_URL = "https://zombies.telgather.com/platform";
- const Toast = {
- template: `
- <div class="fixed top-4 right-4 z-50">
- <div v-if="show" :class="['alert', type, 'glass-effect', 'shadow-2xl', 'min-w-80', 'max-w-md', 'animate-slide-in']">
- <div class="flex items-center justify-between w-full">
- <div class="flex items-center">
- <i v-if="type === 'alert-success'" class="fas fa-check-circle text-green-400 mr-3"></i>
- <i v-else-if="type === 'alert-error'" class="fas fa-exclamation-circle text-red-400 mr-3"></i>
- <i v-else class="fas fa-info-circle text-blue-400 mr-3"></i>
- <span class="text-white font-medium">{{ message }}</span>
- </div>
- <button
- @click="$emit('close')"
- class="btn btn-circle btn-sm bg-white/20 border-0 hover:bg-white/30 ml-3"
- >
- <i class="fas fa-times text-white"></i>
- </button>
- </div>
- </div>
- </div>
- `,
- props: ["show", "type", "message"],
- emits: ["close"],
- };
- createApp({
- data() {
- return {
- reason: "",
- waitInputAddresses: "",
- addressList: [],
- waitAddRewardList: [],
- showAddRewardDialog: false,
- showPasswordDialog: false,
- selectedReward: "",
- rewardQuantity: "",
- password: "",
- isVerifying: false,
- searchAddress: "",
- staticGoods: [],
- toast: {
- show: false,
- type: "alert-info",
- message: "",
- },
- showHistoryDialog: false,
- historyData: [],
- currentPage: 1,
- totalPages: 1,
- isLoadingHistory: false,
- };
- },
- computed: {
- totalRewardCount() {
- return this.waitAddRewardList.reduce(
- (total, reward) => total + reward.count,
- 0
- );
- },
- filteredAddresses() {
- if (!this.searchAddress) return this.addressList;
- return this.addressList.filter((addr) =>
- addr.toLowerCase().includes(this.searchAddress.toLowerCase())
- );
- },
- },
- components: {
- Toast,
- },
- watch: {
- showHistoryDialog(newVal) {
- if (newVal) {
- this.loadHistory();
- }
- },
- },
- methods: {
- // Parse addresses
- parseAddresses() {
- if (this.waitInputAddresses.trim()) {
- const addresses = this.waitInputAddresses
- .split("\n")
- .map((addr) => addr.trim())
- .filter((addr) => addr && addr.length > 0);
- this.addressList = addresses;
- } else {
- this.addressList = [];
- }
- },
- // Remove address
- removeAddress(index) {
- this.addressList.splice(index, 1);
- this.updateAddressInput();
- this.showToast(`Address removed`, "alert-info");
- },
- // Update address input
- updateAddressInput() {
- this.waitInputAddresses = this.addressList.join("\n");
- },
- // Clear addresses
- clearAddresses() {
- this.addressList = [];
- this.waitInputAddresses = "";
- this.showToast("All addresses cleared", "alert-info");
- },
- // Export addresses
- exportAddresses() {
- if (this.addressList.length === 0) {
- this.showToast("No addresses to export", "alert-error");
- return;
- }
- const text = this.addressList.join("\n");
- const blob = new Blob([text], { type: "text/plain" });
- const url = URL.createObjectURL(blob);
- const a = document.createElement("a");
- a.href = url;
- a.download = `addresses_${
- new Date().toISOString().split("T")[0]
- }.txt`;
- a.click();
- URL.revokeObjectURL(url);
- this.showToast(
- `Exported ${this.addressList.length} addresses`,
- "alert-success"
- );
- },
- // Import addresses
- importAddresses() {
- const input = document.createElement("input");
- input.type = "file";
- input.accept = ".txt,.csv";
- input.onchange = (e) => {
- const file = e.target.files[0];
- if (file) {
- const reader = new FileReader();
- reader.onload = (e) => {
- const content = e.target.result;
- this.waitInputAddresses = content;
- this.parseAddresses();
- this.showToast(
- `Imported ${this.addressList.length} addresses`,
- "alert-success"
- );
- };
- reader.readAsText(file);
- }
- };
- input.click();
- },
- // Add reward confirmation
- addRewardConfirm() {
- if (
- this.selectedReward &&
- this.rewardQuantity &&
- this.rewardQuantity > 0
- ) {
- const itemId = parseInt(this.selectedReward);
- const count = parseInt(this.rewardQuantity);
- // Check if item already exists
- const existingIndex = this.waitAddRewardList.findIndex(
- (item) => item.id === itemId
- );
- if (existingIndex > -1) {
- // If exists, increase quantity
- this.waitAddRewardList[existingIndex].count += count;
- this.showToast(
- `Added ${count} more ${this.getItemName(itemId)}`,
- "alert-success"
- );
- } else {
- // If not exists, add new item
- this.waitAddRewardList.push({
- id: itemId,
- count: count,
- });
- this.showToast(
- `Added ${count} ${this.getItemName(itemId)}`,
- "alert-success"
- );
- }
- this.closeModal();
- }
- },
- // Update reward count
- updateRewardCount(id, count) {
- const reward = this.waitAddRewardList.find(
- (item) => item.id === id
- );
- if (reward && count > 0) {
- reward.count = parseInt(count);
- this.showToast(
- `Updated ${this.getItemName(id)} quantity to ${count}`,
- "alert-info"
- );
- }
- },
- // Remove reward
- removeWaitReward(id) {
- const index = this.waitAddRewardList.findIndex(
- (item) => item.id === id
- );
- if (index > -1) {
- const removedReward = this.waitAddRewardList.splice(index, 1)[0];
- this.showToast(
- `Removed ${removedReward.count} ${this.getItemName(id)}`,
- "alert-info"
- );
- }
- },
- // Get item name
- getItemName(id) {
- const item = this.staticGoods.find((item) => item.id == id);
- return item ? item.name : `Unknown Item (${id})`;
- },
- // Get item icon
- getItemIcon(id) {
- const item = this.staticGoods.find((item) => item.id == id);
- if (item && item.icon) {
- const iconPath = `imgs/${item.icon}.png`;
- console.log(`Loading icon for item ${id}: ${iconPath}`);
- return iconPath;
- }
- console.log(`No icon found for item ${id}, using default`);
- return "imgs/default_icon.png";
- },
- // Handle image error
- handleImageError(event) {
- console.log("Image load error:", event.target.src);
- // Replace with fallback icon
- const parent = event.target.parentElement;
- if (parent) {
- parent.innerHTML =
- '<i class="fas fa-question text-white/50 text-xl"></i>';
- }
- },
- // Send all rewards
- sendAllRewards() {
- if (this.addressList.length === 0) {
- this.showToast(
- "Please add wallet addresses first",
- "alert-error"
- );
- return;
- }
- if (this.waitAddRewardList.length === 0) {
- this.showToast("Please add rewards first", "alert-error");
- return;
- }
- // Show password verification dialog
- this.showPasswordDialog = true;
- this.password = "";
- this.reason = "";
- },
- // Verify password and proceed with airdrop
- async verifyPassword() {
- if (!this.password.trim()) {
- this.showToast("Please enter password", "alert-error");
- return;
- }
- if (!this.reason.trim()) {
- this.showToast("Please enter reason", "alert-error");
- return;
- }
- this.isVerifying = true;
- let goodList = [];
- for (const item of this.waitAddRewardList) {
- goodList.push({
- id: item.id,
- count: item.count,
- });
- }
- try {
- // Build send data
- const sendData = {
- reason: this.reason,
- walletList: this.addressList,
- goodList: goodList,
- password: this.password,
- };
- console.log("Sending data to server:", sendData);
- this.showToast(
- `Verifying password and preparing airdrop...`,
- "alert-info"
- );
- // Call server API for password verification and airdrop
- const response = await fetch(
- `${API_BASE_URL}/pog-service/callback/add/good`,
- {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify(sendData),
- }
- );
- const result = await response.json();
- if (response.ok) {
- this.closePasswordModal();
- this.showToast(
- "Airdrop completed successfully!",
- "alert-success"
- );
- console.log("Airdrop result:", result);
- } else {
- this.showToast(
- result.message ||
- "Airdrop failed. Please check your password and try again.",
- "alert-error"
- );
- this.password = "";
- }
- } catch (error) {
- console.error("Airdrop error:", error);
- this.showToast(
- "Network error. Please check your connection and try again.",
- "alert-error"
- );
- this.password = "";
- } finally {
- this.isVerifying = false;
- }
- },
- // Close password modal
- closePasswordModal() {
- this.showPasswordDialog = false;
- this.password = "";
- this.reason = "";
- this.isVerifying = false;
- },
- closeModal() {
- this.showAddRewardDialog = false;
- this.selectedReward = "";
- this.rewardQuantity = "";
- },
- showToast(message, type = "alert-info") {
- // Hide existing toast first
- this.toast.show = false;
- // Show new toast after a brief delay
- setTimeout(() => {
- this.toast = {
- show: true,
- type: type,
- message: message,
- };
- // Auto hide after 4 seconds
- setTimeout(() => {
- this.toast.show = false;
- }, 4000);
- }, 100);
- },
- // Load history data
- async loadHistory() {
- this.isLoadingHistory = true;
- try {
- const response = await fetch(
- `${API_BASE_URL}/pog-service/callback/add/good/page?page=${this.currentPage}`,
- {
- method: "GET",
- headers: {
- "Content-Type": "application/json",
- },
- }
- );
- const result = await response.json();
- if (response.ok && result.success) {
- this.historyData = result.data.content || [];
- // Assuming the API returns total pages or we can calculate it
- // For now, we'll set a default total pages
- this.totalPages = Math.max(
- 1,
- Math.ceil(this.historyData.length / 10)
- );
- this.showToast("History loaded successfully", "alert-success");
- } else {
- this.showToast(
- result.message || "Failed to load history",
- "alert-error"
- );
- }
- } catch (error) {
- console.error("History loading error:", error);
- this.showToast(
- "Network error. Please check your connection and try again.",
- "alert-error"
- );
- } finally {
- this.isLoadingHistory = false;
- }
- },
- // Change page
- changePage(newPage) {
- if (newPage >= 1 && newPage <= this.totalPages) {
- this.currentPage = newPage;
- this.loadHistory();
- }
- },
- // Parse good list from JSON string
- parseGoodList(goodListString) {
- try {
- return JSON.parse(goodListString);
- } catch (error) {
- console.error("Error parsing good list:", error);
- return [];
- }
- },
- // Format date - Display UTC time directly
- formatDate(dateString) {
- try {
- const date = new Date(dateString);
- // Format as UTC time: YYYY-MM-DD HH:mm:ss
- const year = date.getUTCFullYear();
- const month = String(date.getUTCMonth() + 1).padStart(2, "0");
- const day = String(date.getUTCDate()).padStart(2, "0");
- const hours = String(date.getUTCHours()).padStart(2, "0");
- const minutes = String(date.getUTCMinutes()).padStart(2, "0");
- const seconds = String(date.getUTCSeconds()).padStart(2, "0");
- return `${year}-${month}-${day} ${hours}:${minutes}:${seconds} UTC`;
- } catch (error) {
- return dateString;
- }
- },
- // Close history modal
- closeHistoryModal() {
- this.showHistoryDialog = false;
- this.historyData = [];
- this.currentPage = 1;
- this.totalPages = 1;
- },
- },
- mounted() {
- const m = {
- 1: {
- name: "POG",
- count: 100,
- icon: "icon_pog",
- },
- 2: {
- name: "Diamond",
- count: 100,
- icon: "icon_gem",
- },
- 3: {
- name: "GAME_SHARD",
- count: 100,
- icon: "icon_puzzle",
- },
- 5: {
- name: "CRIT_CARD",
- count: 100,
- icon: "icon_crit",
- },
- 6: {
- name: "FREE_ITEM_BOX",
- count: 100,
- icon: "icon_item_box",
- },
- 7: {
- name: "ITEM_BOX",
- count: 100,
- icon: "icon_item_box",
- },
- 8: {
- name: "FREE_POG_BOX",
- count: 100,
- icon: "icon_pog_box",
- },
- 9: {
- name: "POG_BOX",
- count: 100,
- icon: "icon_pog_box",
- },
- };
- this.staticGoods = [];
- for (const key in m) {
- this.staticGoods.push({
- id: key,
- name: m[key].name,
- count: m[key].count,
- icon: m[key].icon,
- });
- }
- console.log(this.staticGoods);
- },
- }).mount("#app");
- </script>
- </body>
- </html>
|