|
|
@@ -0,0 +1,395 @@
|
|
|
+<!DOCTYPE html>
|
|
|
+<html lang="zh-CN">
|
|
|
+<head>
|
|
|
+ <meta charset="UTF-8">
|
|
|
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
+ <title>GPT账户充值</title>
|
|
|
+ <style>
|
|
|
+ * {
|
|
|
+ margin: 0;
|
|
|
+ padding: 0;
|
|
|
+ box-sizing: border-box;
|
|
|
+ }
|
|
|
+
|
|
|
+ body {
|
|
|
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
|
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
+ min-height: 100vh;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ padding: 20px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .container {
|
|
|
+ background: white;
|
|
|
+ border-radius: 16px;
|
|
|
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
|
|
+ padding: 40px;
|
|
|
+ width: 100%;
|
|
|
+ max-width: 500px;
|
|
|
+ text-align: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .title {
|
|
|
+ font-size: 28px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #333;
|
|
|
+ margin-bottom: 30px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .step {
|
|
|
+ display: none;
|
|
|
+ }
|
|
|
+
|
|
|
+ .step.active {
|
|
|
+ display: block;
|
|
|
+ }
|
|
|
+
|
|
|
+ .input-group {
|
|
|
+ margin-bottom: 20px;
|
|
|
+ text-align: left;
|
|
|
+ }
|
|
|
+
|
|
|
+ .input-group label {
|
|
|
+ display: block;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ font-weight: 500;
|
|
|
+ color: #555;
|
|
|
+ }
|
|
|
+
|
|
|
+ .input-group input, .input-group textarea {
|
|
|
+ width: 100%;
|
|
|
+ padding: 12px 16px;
|
|
|
+ border: 2px solid #e1e5e9;
|
|
|
+ border-radius: 8px;
|
|
|
+ font-size: 16px;
|
|
|
+ transition: border-color 0.3s;
|
|
|
+ }
|
|
|
+
|
|
|
+ .input-group input:focus, .input-group textarea:focus {
|
|
|
+ outline: none;
|
|
|
+ border-color: #667eea;
|
|
|
+ }
|
|
|
+
|
|
|
+ .input-group textarea {
|
|
|
+ height: 120px;
|
|
|
+ resize: vertical;
|
|
|
+ }
|
|
|
+
|
|
|
+ .btn {
|
|
|
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
+ color: white;
|
|
|
+ border: none;
|
|
|
+ padding: 14px 28px;
|
|
|
+ border-radius: 8px;
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 600;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: transform 0.2s, box-shadow 0.2s;
|
|
|
+ margin: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .btn:hover {
|
|
|
+ transform: translateY(-2px);
|
|
|
+ box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
|
|
|
+ }
|
|
|
+
|
|
|
+ .btn:disabled {
|
|
|
+ background: #ccc;
|
|
|
+ cursor: not-allowed;
|
|
|
+ transform: none;
|
|
|
+ box-shadow: none;
|
|
|
+ }
|
|
|
+
|
|
|
+ .btn-secondary {
|
|
|
+ background: #6c757d;
|
|
|
+ }
|
|
|
+
|
|
|
+ .btn-secondary:hover {
|
|
|
+ box-shadow: 0 8px 25px rgba(108, 117, 125, 0.4);
|
|
|
+ }
|
|
|
+
|
|
|
+ .info-box {
|
|
|
+ background: #f8f9fa;
|
|
|
+ border: 1px solid #e9ecef;
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 20px;
|
|
|
+ margin: 20px 0;
|
|
|
+ text-align: left;
|
|
|
+ }
|
|
|
+
|
|
|
+ .info-box h4 {
|
|
|
+ color: #495057;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .info-box p {
|
|
|
+ color: #6c757d;
|
|
|
+ line-height: 1.5;
|
|
|
+ }
|
|
|
+
|
|
|
+ .status {
|
|
|
+ display: inline-block;
|
|
|
+ padding: 6px 12px;
|
|
|
+ border-radius: 20px;
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 500;
|
|
|
+ margin: 10px 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .status.processing {
|
|
|
+ background: #fff3cd;
|
|
|
+ color: #856404;
|
|
|
+ }
|
|
|
+
|
|
|
+ .status.completed {
|
|
|
+ background: #d4edda;
|
|
|
+ color: #155724;
|
|
|
+ }
|
|
|
+
|
|
|
+ .loading {
|
|
|
+ display: inline-block;
|
|
|
+ width: 20px;
|
|
|
+ height: 20px;
|
|
|
+ border: 3px solid #f3f3f3;
|
|
|
+ border-top: 3px solid #667eea;
|
|
|
+ border-radius: 50%;
|
|
|
+ animation: spin 1s linear infinite;
|
|
|
+ margin-left: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ @keyframes spin {
|
|
|
+ 0% { transform: rotate(0deg); }
|
|
|
+ 100% { transform: rotate(360deg); }
|
|
|
+ }
|
|
|
+
|
|
|
+ .success-message {
|
|
|
+ background: #d4edda;
|
|
|
+ color: #155724;
|
|
|
+ padding: 20px;
|
|
|
+ border-radius: 8px;
|
|
|
+ margin: 20px 0;
|
|
|
+ font-size: 18px;
|
|
|
+ font-weight: 600;
|
|
|
+ }
|
|
|
+
|
|
|
+ .error-message {
|
|
|
+ background: #f8d7da;
|
|
|
+ color: #721c24;
|
|
|
+ padding: 15px;
|
|
|
+ border-radius: 8px;
|
|
|
+ margin: 20px 0;
|
|
|
+ }
|
|
|
+ </style>
|
|
|
+</head>
|
|
|
+<body>
|
|
|
+ <div class="container">
|
|
|
+ <h1 class="title">GPT账户充值</h1>
|
|
|
+
|
|
|
+ <!-- 步骤1: 验证卡密 -->
|
|
|
+ <div id="step1" class="step active">
|
|
|
+ <div class="input-group">
|
|
|
+ <label for="cardKey">请输入卡密:</label>
|
|
|
+ <input type="text" id="cardKey" placeholder="输入卡密" />
|
|
|
+ </div>
|
|
|
+ <button class="btn" onclick="validateCard()">验证卡密</button>
|
|
|
+ <div id="cardError" class="error-message" style="display: none;"></div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 步骤2: 验证凭证 -->
|
|
|
+ <div id="step2" class="step">
|
|
|
+ <div class="input-group">
|
|
|
+ <label for="accessToken">请输入Access Token:</label>
|
|
|
+ <textarea id="accessToken" placeholder="粘贴您的Access Token"></textarea>
|
|
|
+ </div>
|
|
|
+ <button class="btn" onclick="parseToken()">验证凭证</button>
|
|
|
+ <button class="btn btn-secondary" onclick="goBack(1)">返回</button>
|
|
|
+ <div id="tokenError" class="error-message" style="display: none;"></div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 步骤3: 确认信息 -->
|
|
|
+ <div id="step3" class="step">
|
|
|
+ <div class="info-box">
|
|
|
+ <h4>账户信息</h4>
|
|
|
+ <p>邮箱: <span id="userEmail"></span></p>
|
|
|
+ <p>卡密: <span id="confirmCardKey"></span></p>
|
|
|
+ </div>
|
|
|
+ <p>请确认以上信息无误后点击确认充值</p>
|
|
|
+ <button class="btn" onclick="submitTask()">确认充值</button>
|
|
|
+ <button class="btn btn-secondary" onclick="goBack(2)">取消</button>
|
|
|
+ <div id="submitError" class="error-message" style="display: none;"></div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 步骤4: 充值进度 -->
|
|
|
+ <div id="step4" class="step">
|
|
|
+ <h3>充值进行中...</h3>
|
|
|
+ <div id="taskStatus" class="status processing">处理中<span class="loading"></span></div>
|
|
|
+ <div id="taskResult"></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <script>
|
|
|
+ const API_BASE = '/api/gpt-recharge';
|
|
|
+ let currentCardKey = '';
|
|
|
+ let currentAccessToken = '';
|
|
|
+ let currentTaskId = '';
|
|
|
+ let pollingInterval = null;
|
|
|
+
|
|
|
+ // 显示指定步骤
|
|
|
+ function showStep(stepNumber) {
|
|
|
+ document.querySelectorAll('.step').forEach(step => {
|
|
|
+ step.classList.remove('active');
|
|
|
+ });
|
|
|
+ document.getElementById(`step${stepNumber}`).classList.add('active');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 返回上一步
|
|
|
+ function goBack(stepNumber) {
|
|
|
+ showStep(stepNumber);
|
|
|
+ // 清理轮询
|
|
|
+ if (pollingInterval) {
|
|
|
+ clearInterval(pollingInterval);
|
|
|
+ pollingInterval = null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 显示错误信息
|
|
|
+ function showError(elementId, message) {
|
|
|
+ const errorElement = document.getElementById(elementId);
|
|
|
+ errorElement.textContent = message;
|
|
|
+ errorElement.style.display = 'block';
|
|
|
+ setTimeout(() => {
|
|
|
+ errorElement.style.display = 'none';
|
|
|
+ }, 5000);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 验证卡密
|
|
|
+ async function validateCard() {
|
|
|
+ const cardKey = document.getElementById('cardKey').value.trim();
|
|
|
+ if (!cardKey) {
|
|
|
+ showError('cardError', '请输入卡密');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const response = await fetch(`${API_BASE}/validate-card`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'application/json',
|
|
|
+ },
|
|
|
+ body: JSON.stringify({ cardKey: cardKey })
|
|
|
+ });
|
|
|
+
|
|
|
+ const result = await response.json();
|
|
|
+
|
|
|
+ if (result.available) {
|
|
|
+ currentCardKey = cardKey;
|
|
|
+ showStep(2);
|
|
|
+ } else {
|
|
|
+ showError('cardError', '卡密无效或已使用');
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ showError('cardError', '验证失败,请稍后重试');
|
|
|
+ console.error('验证卡密失败:', error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 验证凭证
|
|
|
+ async function parseToken() {
|
|
|
+ const accessToken = document.getElementById('accessToken').value.trim();
|
|
|
+ if (!accessToken) {
|
|
|
+ showError('tokenError', '请输入Access Token');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const response = await fetch(`${API_BASE}/parse-token`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'application/json',
|
|
|
+ },
|
|
|
+ body: JSON.stringify({ accessToken: accessToken })
|
|
|
+ });
|
|
|
+
|
|
|
+ const result = await response.json();
|
|
|
+
|
|
|
+ if (result.success) {
|
|
|
+ currentAccessToken = accessToken;
|
|
|
+ document.getElementById('userEmail').textContent = result.message;
|
|
|
+ document.getElementById('confirmCardKey').textContent = currentCardKey;
|
|
|
+ showStep(3);
|
|
|
+ } else {
|
|
|
+ showError('tokenError', result.message || '凭证验证失败');
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ showError('tokenError', '验证失败,请稍后重试');
|
|
|
+ console.error('验证凭证失败:', error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 提交任务
|
|
|
+ async function submitTask() {
|
|
|
+ try {
|
|
|
+ const response = await fetch(`${API_BASE}/submit-task`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'application/json',
|
|
|
+ },
|
|
|
+ body: JSON.stringify({
|
|
|
+ cardKey: currentCardKey,
|
|
|
+ accessToken: currentAccessToken,
|
|
|
+ idp: 'auth0'
|
|
|
+ })
|
|
|
+ });
|
|
|
+
|
|
|
+ const result = await response.json();
|
|
|
+
|
|
|
+ if (result.success && result.taskId) {
|
|
|
+ currentTaskId = result.taskId;
|
|
|
+ showStep(4);
|
|
|
+ startPolling();
|
|
|
+ } else {
|
|
|
+ showError('submitError', '提交任务失败');
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ showError('submitError', '提交失败,请稍后重试');
|
|
|
+ console.error('提交任务失败:', error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 开始轮询任务状态
|
|
|
+ function startPolling() {
|
|
|
+ pollingInterval = setInterval(async () => {
|
|
|
+ try {
|
|
|
+ const response = await fetch(`${API_BASE}/task-status/${currentTaskId}`);
|
|
|
+ const result = await response.json();
|
|
|
+
|
|
|
+ const statusElement = document.getElementById('taskStatus');
|
|
|
+ const resultElement = document.getElementById('taskResult');
|
|
|
+
|
|
|
+ if (result.status === 'processing') {
|
|
|
+ statusElement.className = 'status processing';
|
|
|
+ statusElement.innerHTML = '处理中<span class="loading"></span>';
|
|
|
+ } else if (result.status === 'completed') {
|
|
|
+ statusElement.className = 'status completed';
|
|
|
+ statusElement.innerHTML = '已完成';
|
|
|
+ resultElement.innerHTML = '<div class="success-message">🎉 充值成功!</div>';
|
|
|
+
|
|
|
+ // 停止轮询
|
|
|
+ clearInterval(pollingInterval);
|
|
|
+ pollingInterval = null;
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('查询任务状态失败:', error);
|
|
|
+ }
|
|
|
+ }, 5000); // 每5秒查询一次
|
|
|
+ }
|
|
|
+
|
|
|
+ // 页面加载完成后的初始化
|
|
|
+ document.addEventListener('DOMContentLoaded', function() {
|
|
|
+ console.log('GPT充值页面加载完成');
|
|
|
+ });
|
|
|
+ </script>
|
|
|
+</body>
|
|
|
+</html>
|