|
@@ -0,0 +1,1478 @@
|
|
|
|
+<!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 >=0 ? 'badge-success' : 'badge-error'
|
|
|
|
+ ]"
|
|
|
|
+ >
|
|
|
|
+ {{ item.status >=0 ? '✓' : '✗' }}
|
|
|
|
+ </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>
|