785 lines
26 KiB
HTML
785 lines
26 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>柯大鸭 抽奖验证工具</title>
|
||
<style>
|
||
:root {
|
||
--primary: #6366F1;
|
||
--primary-light: #818CF8;
|
||
--success: #10B981;
|
||
--error: #EF4444;
|
||
--warning: #F59E0B;
|
||
--bg: #0F172A;
|
||
--bg-card: #1E293B;
|
||
--bg-input: #334155;
|
||
--text: #F8FAFC;
|
||
--text-muted: #94A3B8;
|
||
--border: #475569;
|
||
}
|
||
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
min-height: 100vh;
|
||
padding: 24px;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.container {
|
||
max-width: 800px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
header {
|
||
text-align: center;
|
||
margin-bottom: 32px;
|
||
padding: 24px;
|
||
background: linear-gradient(135deg, var(--primary) 0%, #8B5CF6 100%);
|
||
border-radius: 16px;
|
||
box-shadow: 0 8px 32px rgba(99, 102, 241, 0.3);
|
||
}
|
||
|
||
header h1 {
|
||
font-size: 28px;
|
||
font-weight: 700;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
header p {
|
||
opacity: 0.9;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.card {
|
||
background: var(--bg-card);
|
||
border-radius: 12px;
|
||
padding: 24px;
|
||
margin-bottom: 20px;
|
||
border: 1px solid var(--border);
|
||
}
|
||
|
||
.card-title {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
margin-bottom: 16px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.tabs {
|
||
display: flex;
|
||
gap: 8px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.tab {
|
||
flex: 1;
|
||
padding: 12px 16px;
|
||
background: var(--bg-input);
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
color: var(--text-muted);
|
||
cursor: pointer;
|
||
text-align: center;
|
||
font-size: 14px;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.tab.active {
|
||
background: var(--primary);
|
||
border-color: var(--primary);
|
||
color: #fff;
|
||
}
|
||
|
||
.tab:hover:not(.active) {
|
||
background: var(--border);
|
||
}
|
||
|
||
.input-group {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.input-group label {
|
||
display: block;
|
||
font-size: 14px;
|
||
color: var(--text-muted);
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.input-group input,
|
||
.input-group textarea,
|
||
.input-group select {
|
||
width: 100%;
|
||
padding: 12px 16px;
|
||
background: var(--bg-input);
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
color: var(--text);
|
||
font-size: 14px;
|
||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||
transition: border-color 0.2s;
|
||
}
|
||
|
||
.input-group textarea {
|
||
min-height: 150px;
|
||
resize: vertical;
|
||
}
|
||
|
||
.input-group input:focus,
|
||
.input-group textarea:focus {
|
||
outline: none;
|
||
border-color: var(--primary);
|
||
}
|
||
|
||
.input-row {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 16px;
|
||
}
|
||
|
||
.btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
padding: 14px 28px;
|
||
border: none;
|
||
border-radius: 8px;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.btn-primary {
|
||
background: linear-gradient(135deg, var(--primary) 0%, #8B5CF6 100%);
|
||
color: #fff;
|
||
width: 100%;
|
||
}
|
||
|
||
.btn-primary:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 8px 24px rgba(99, 102, 241, 0.4);
|
||
}
|
||
|
||
.btn-primary:active {
|
||
transform: translateY(0);
|
||
}
|
||
|
||
.result-card {
|
||
display: none;
|
||
}
|
||
|
||
.result-card.show {
|
||
display: block;
|
||
}
|
||
|
||
.result-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 16px;
|
||
border-radius: 8px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.result-header.success {
|
||
background: rgba(16, 185, 129, 0.15);
|
||
border: 1px solid var(--success);
|
||
}
|
||
|
||
.result-header.error {
|
||
background: rgba(239, 68, 68, 0.15);
|
||
border: 1px solid var(--error);
|
||
}
|
||
|
||
.result-icon {
|
||
font-size: 32px;
|
||
}
|
||
|
||
.result-title {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.result-subtitle {
|
||
font-size: 14px;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.detail-section {
|
||
background: var(--bg);
|
||
border-radius: 8px;
|
||
padding: 16px;
|
||
margin-top: 16px;
|
||
}
|
||
|
||
.detail-section h4 {
|
||
font-size: 14px;
|
||
color: var(--text-muted);
|
||
margin-bottom: 12px;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
|
||
.detail-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
padding: 8px 0;
|
||
border-bottom: 1px solid var(--border);
|
||
font-size: 14px;
|
||
}
|
||
|
||
.detail-row:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.detail-row .label {
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.detail-row .value {
|
||
font-family: 'Monaco', 'Menlo', monospace;
|
||
color: var(--text);
|
||
word-break: break-all;
|
||
text-align: right;
|
||
max-width: 60%;
|
||
}
|
||
|
||
.prize-info {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 20px;
|
||
padding: 24px;
|
||
background: linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(139, 92, 246, 0.1) 100%);
|
||
border-radius: 12px;
|
||
margin-top: 16px;
|
||
}
|
||
|
||
.prize-image {
|
||
width: 120px;
|
||
height: 120px;
|
||
border-radius: 12px;
|
||
object-fit: cover;
|
||
background: var(--bg);
|
||
border: 2px solid var(--border);
|
||
}
|
||
|
||
.prize-details {
|
||
flex: 1;
|
||
}
|
||
|
||
.prize-name {
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
color: var(--text);
|
||
margin-bottom: 8px;
|
||
line-height: 1.3;
|
||
}
|
||
|
||
.prize-id-label {
|
||
font-size: 14px;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.prize-id-label span {
|
||
font-family: 'Monaco', 'Menlo', monospace;
|
||
color: var(--primary-light);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.help-text {
|
||
font-size: 12px;
|
||
color: var(--text-muted);
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.config-status {
|
||
margin-top: 12px;
|
||
padding: 10px 14px;
|
||
border-radius: 6px;
|
||
font-size: 13px;
|
||
display: none;
|
||
}
|
||
|
||
.config-status.valid {
|
||
display: block;
|
||
background: rgba(16, 185, 129, 0.15);
|
||
border: 1px solid var(--success);
|
||
color: var(--success);
|
||
}
|
||
|
||
.config-status.invalid {
|
||
display: block;
|
||
background: rgba(239, 68, 68, 0.15);
|
||
border: 1px solid var(--error);
|
||
color: var(--error);
|
||
}
|
||
|
||
.step-indicator {
|
||
display: inline-block;
|
||
width: 24px;
|
||
height: 24px;
|
||
border-radius: 50%;
|
||
background: var(--primary);
|
||
color: #fff;
|
||
text-align: center;
|
||
line-height: 24px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
margin-right: 8px;
|
||
}
|
||
|
||
.hidden {
|
||
display: none !important;
|
||
}
|
||
|
||
footer {
|
||
text-align: center;
|
||
padding: 24px;
|
||
color: var(--text-muted);
|
||
font-size: 12px;
|
||
}
|
||
|
||
footer a {
|
||
color: var(--primary-light);
|
||
text-decoration: none;
|
||
}
|
||
|
||
@media (max-width: 600px) {
|
||
body {
|
||
padding: 16px;
|
||
}
|
||
|
||
.input-row {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
header h1 {
|
||
font-size: 22px;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
|
||
<body>
|
||
<div class="container">
|
||
<header>
|
||
<h1>🎰 柯大鸭 抽奖验证工具</h1>
|
||
<p>离线验证抽奖结果公平性 · 无需网络连接</p>
|
||
</header>
|
||
|
||
<!-- 步骤1: 活动配置 -->
|
||
<div class="card">
|
||
<div class="card-title">📦 步骤 1: 导入活动配置</div>
|
||
<p class="help-text" style="margin-bottom: 12px;">从后台活动管理页面复制完整配置 JSON</p>
|
||
|
||
<div class="input-group">
|
||
<label>活动配置 JSON(包含奖品名称和图片)</label>
|
||
<textarea id="activityConfig" placeholder='{
|
||
"weights": "1:100,2:500",
|
||
"rewards": [
|
||
{"id": 1, "name": "奖品A", "image": "...", "weight": 100}
|
||
]
|
||
}'></textarea>
|
||
</div>
|
||
|
||
<div id="configStatus" class="config-status"></div>
|
||
</div>
|
||
|
||
<!-- 步骤2: 抽奖凭证 -->
|
||
<div class="card">
|
||
<div class="card-title">🎫 步骤 2: 导入抽奖凭证</div>
|
||
<p class="help-text" style="margin-bottom: 12px;">从小程序订单详情复制验证凭据 JSON</p>
|
||
|
||
<div class="input-group">
|
||
<label>验证凭据 JSON</label>
|
||
<textarea id="jsonInput" placeholder='{
|
||
"seed": "aabbccdd...",
|
||
"issue_id": 1001,
|
||
"user_id": 12345,
|
||
"salt": "1234abcd"
|
||
}'></textarea>
|
||
</div>
|
||
|
||
<button class="btn btn-primary" onclick="verify()">
|
||
🔍 开始验证
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 结果区域 -->
|
||
<div id="resultsContainer"></div>
|
||
|
||
<footer>
|
||
<p>柯大鸭 抽奖公平性验证工具 v1.1</p>
|
||
<p>采用 HMAC-SHA256 承诺机制 · 完全离线运行</p>
|
||
</footer>
|
||
</div>
|
||
|
||
<script>
|
||
// Hex 解码
|
||
function hexToBytes(hex) {
|
||
const bytes = new Uint8Array(hex.length / 2);
|
||
for (let i = 0; i < hex.length; i += 2) {
|
||
bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
|
||
}
|
||
return bytes;
|
||
}
|
||
|
||
// BigEndian Uint64
|
||
function bigEndianUint64(buffer) {
|
||
const view = new DataView(buffer);
|
||
const high = view.getUint32(0);
|
||
const low = view.getUint32(4);
|
||
return BigInt(high) * BigInt(0x100000000) + BigInt(low);
|
||
}
|
||
|
||
// HMAC-SHA256
|
||
async function hmacSha256(keyHex, message) {
|
||
const keyBytes = hexToBytes(keyHex);
|
||
const encoder = new TextEncoder();
|
||
const msgBytes = encoder.encode(message);
|
||
|
||
const cryptoKey = await crypto.subtle.importKey(
|
||
'raw',
|
||
keyBytes,
|
||
{ name: 'HMAC', hash: 'SHA-256' },
|
||
false,
|
||
['sign']
|
||
);
|
||
|
||
const signature = await crypto.subtle.sign('HMAC', cryptoKey, msgBytes);
|
||
return new Uint8Array(signature);
|
||
}
|
||
|
||
// 解析奖品配置(自动排序)
|
||
function parseRewards(str) {
|
||
if (!str) return [];
|
||
const list = str.split(',').map(part => {
|
||
const [id, value] = part.trim().split(':');
|
||
return { id: parseInt(id), weight: parseInt(value), count: parseInt(value) };
|
||
}).filter(r => r.id && r.weight);
|
||
|
||
// 关键:按ID排序以匹配后端逻辑
|
||
list.sort((a, b) => a.id - b.id);
|
||
return list;
|
||
}
|
||
|
||
// 全局存储活动配置
|
||
let activityConfig = null;
|
||
|
||
// 验证并显示活动配置状态
|
||
function validateConfig() {
|
||
const input = document.getElementById('activityConfig');
|
||
const status = document.getElementById('configStatus');
|
||
const configStr = input.value.trim();
|
||
|
||
if (!configStr) {
|
||
status.className = 'config-status';
|
||
status.textContent = '';
|
||
activityConfig = null;
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const config = JSON.parse(configStr);
|
||
|
||
// 检查是否有 weights 字段或 rewards 数组
|
||
if (config.weights && config.rewards && Array.isArray(config.rewards)) {
|
||
activityConfig = config;
|
||
// 如果config里没有weights字符串,我们自己生成一个(按ID排序)
|
||
// 但通常应该有。为了保险,重新解析并排序
|
||
const rewards = parseRewards(config.weights);
|
||
// 更新weights字符串为排序后的
|
||
activityConfig.weights = rewards.map(r => `${r.id}:${r.weight}`).join(',');
|
||
|
||
const totalWeight = rewards.reduce((sum, r) => sum + r.weight, 0);
|
||
status.className = 'config-status valid';
|
||
status.textContent = `✅ 已识别 ${config.rewards.length} 个奖品,总权重: ${totalWeight}`;
|
||
} else if (config.weights) {
|
||
// 只有 weights 字符串
|
||
const rewards = parseRewards(config.weights);
|
||
if (rewards.length > 0) {
|
||
activityConfig = { weights: config.weights, rewards: rewards };
|
||
// 更新weights字符串为排序后的
|
||
activityConfig.weights = rewards.map(r => `${r.id}:${r.weight}`).join(',');
|
||
|
||
const totalWeight = rewards.reduce((sum, r) => sum + r.weight, 0);
|
||
status.className = 'config-status valid';
|
||
status.textContent = `✅ 已识别 ${rewards.length} 个奖品,总权重: ${totalWeight}`;
|
||
} else {
|
||
throw new Error('无效的权重配置');
|
||
}
|
||
} else {
|
||
throw new Error('缺少 weights 字段');
|
||
}
|
||
} catch (e) {
|
||
// 尝试作为简单的权重字符串解析
|
||
const rewards = parseRewards(configStr);
|
||
if (rewards.length > 0) {
|
||
const sortedWeightsStr = rewards.map(r => `${r.id}:${r.weight}`).join(',');
|
||
activityConfig = { weights: sortedWeightsStr, rewards: rewards };
|
||
const totalWeight = rewards.reduce((sum, r) => sum + r.weight, 0);
|
||
status.className = 'config-status valid';
|
||
status.textContent = `✅ 已识别 ${rewards.length} 个奖品,总权重: ${totalWeight}`;
|
||
} else {
|
||
status.className = 'config-status invalid';
|
||
status.textContent = '❌ 配置格式无效,请粘贴后台导出的 JSON';
|
||
activityConfig = null;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 初始化事件监听
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
const configInput = document.getElementById('activityConfig');
|
||
configInput.addEventListener('input', validateConfig);
|
||
configInput.addEventListener('paste', function () {
|
||
setTimeout(validateConfig, 10);
|
||
});
|
||
});
|
||
|
||
// 无限赏验证
|
||
async function verifyUnlimited(seed, issueId, userId, salt, weightsStr, rewardsInfo) {
|
||
const rewards = parseRewards(weightsStr);
|
||
if (rewards.length === 0) {
|
||
throw new Error('奖品权重配置无效');
|
||
}
|
||
|
||
const totalWeight = rewards.reduce((sum, r) => sum + r.weight, 0);
|
||
if (totalWeight <= 0) {
|
||
throw new Error('总权重必须大于 0');
|
||
}
|
||
|
||
// 构建 payload 并计算 HMAC
|
||
const payload = `draw:issue:${issueId}|user:${userId}|salt:${salt}`;
|
||
const hmac = await hmacSha256(seed, payload);
|
||
|
||
// 提取随机数
|
||
const randValue = bigEndianUint64(hmac.buffer) % BigInt(totalWeight);
|
||
const rnd = Number(randValue);
|
||
|
||
// 选择奖品
|
||
let acc = 0;
|
||
let pickedReward = null;
|
||
let rangeInfo = '';
|
||
|
||
for (const r of rewards) {
|
||
acc += r.weight;
|
||
if (rnd < acc) {
|
||
pickedReward = r;
|
||
rangeInfo = `${acc - r.weight} - ${acc}`;
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 查找奖品详细信息
|
||
let prizeName = '';
|
||
let prizeImage = '';
|
||
if (pickedReward && rewardsInfo) {
|
||
// rewardsInfo 可能有 id 字段也可能没有,尝试匹配 id
|
||
const info = rewardsInfo.find(ri => (ri.id || ri.ID) === pickedReward.id);
|
||
if (info) {
|
||
prizeName = info.name || info.product_name || '';
|
||
// 处理图片(可能是JSON数组字符串)
|
||
const img = info.image || info.product_image_url;
|
||
if (img) {
|
||
try {
|
||
const images = JSON.parse(img);
|
||
prizeImage = Array.isArray(images) ? images[0] : img;
|
||
} catch {
|
||
prizeImage = img;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return {
|
||
mode: 'unlimited',
|
||
success: true,
|
||
rewardId: pickedReward ? pickedReward.id : 0,
|
||
prizeName: prizeName,
|
||
prizeImage: prizeImage,
|
||
details: [
|
||
{ label: '验证模式', value: '无限赏 (Weighted Random)' },
|
||
{ label: 'Payload', value: payload },
|
||
{ label: '总权重', value: totalWeight.toString() },
|
||
{ label: '随机数', value: rnd.toString() },
|
||
{ label: '命中区间', value: rangeInfo },
|
||
{ label: '奖品数量', value: rewards.length.toString() }
|
||
]
|
||
};
|
||
}
|
||
|
||
|
||
// 主验证函数
|
||
async function verify() {
|
||
try {
|
||
const resultsContainer = document.getElementById('resultsContainer');
|
||
resultsContainer.innerHTML = ''; // 清空之前结果
|
||
|
||
// 步骤1: 获取活动配置 (全局)
|
||
const globalWeightsStr = activityConfig ? activityConfig.weights : '';
|
||
const globalRewardsInfo = activityConfig ? activityConfig.rewards : [];
|
||
|
||
if (!globalWeightsStr) {
|
||
// 如果没提供全局配置,必须确保凭证里有配置
|
||
}
|
||
|
||
// 步骤2: 获取抽奖凭证
|
||
const jsonStr = document.getElementById('jsonInput').value.trim();
|
||
if (!jsonStr) {
|
||
throw new Error('请导入抽奖凭证(步骤2)');
|
||
}
|
||
|
||
let paramsList;
|
||
try {
|
||
const parsed = JSON.parse(jsonStr);
|
||
if (Array.isArray(parsed)) {
|
||
paramsList = parsed;
|
||
} else {
|
||
paramsList = [parsed];
|
||
}
|
||
} catch (e) {
|
||
throw new Error('凭证 JSON 格式无效');
|
||
}
|
||
|
||
// 批量验证
|
||
let allResults = [];
|
||
for (let i = 0; i < paramsList.length; i++) {
|
||
const params = paramsList[i];
|
||
try {
|
||
// 验证必要参数
|
||
if (!params.seed) throw new Error('缺少 seed');
|
||
if (!params.issue_id && params.issue_id !== 0) throw new Error('缺少 issue_id');
|
||
|
||
// 决定使用的权重配置
|
||
let currentWeightsStr = params.weights || globalWeightsStr;
|
||
if (!currentWeightsStr) {
|
||
throw new Error('未找到奖品权重配置 (请在步骤1导入或确保凭证包含配置)');
|
||
}
|
||
|
||
// 执行验证
|
||
const result = await verifyUnlimited(
|
||
params.seed,
|
||
params.issue_id,
|
||
params.user_id || 0,
|
||
params.salt || '',
|
||
currentWeightsStr,
|
||
globalRewardsInfo // 优先用全局的图片信息
|
||
);
|
||
|
||
// 附加索引
|
||
result.index = i + 1;
|
||
result.drawIndex = params.draw_index || (i + 1);
|
||
allResults.push(result);
|
||
|
||
} catch (err) {
|
||
allResults.push({
|
||
index: i + 1,
|
||
success: false,
|
||
error: err.message
|
||
});
|
||
}
|
||
}
|
||
|
||
// 显示所有结果
|
||
showResults(allResults);
|
||
|
||
} catch (err) {
|
||
// 全局错误(如JSON解析失败)
|
||
const resultsContainer = document.getElementById('resultsContainer');
|
||
resultsContainer.innerHTML = `
|
||
<div class="card result-card show">
|
||
<div class="result-header error">
|
||
<span class="result-icon">❌</span>
|
||
<div>
|
||
<div class="result-title">验证无法执行</div>
|
||
<div class="result-subtitle">${err.message}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
// 滚动到结果
|
||
resultsContainer.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||
}
|
||
}
|
||
|
||
// 显示结果列表
|
||
function showResults(results) {
|
||
const container = document.getElementById('resultsContainer');
|
||
container.innerHTML = '';
|
||
|
||
results.forEach(res => {
|
||
const card = document.createElement('div');
|
||
card.className = 'card result-card show';
|
||
card.style.marginBottom = '20px';
|
||
|
||
if (res.success) {
|
||
const detailHtml = res.details.map(d => `
|
||
<div class="detail-row">
|
||
<span class="label">${d.label}</span>
|
||
<span class="value">${d.value}</span>
|
||
</div>
|
||
`).join('');
|
||
|
||
const imgHtml = res.prizeImage
|
||
? `<img class="prize-image" src="${res.prizeImage}" alt="奖品图片" style="display:block">`
|
||
: `<img class="prize-image" style="display:none">`;
|
||
|
||
const nameHtml = res.prizeName || ('奖品 #' + res.rewardId);
|
||
|
||
card.innerHTML = `
|
||
<div class="card-title">📊 第 ${res.drawIndex} 抽验证结果</div>
|
||
<div class="result-header success">
|
||
<span class="result-icon">✅</span>
|
||
<div>
|
||
<div class="result-title">验证通过</div>
|
||
<div class="result-subtitle">ID: ${res.rewardId} - ${nameHtml}</div>
|
||
</div>
|
||
</div>
|
||
<div class="prize-info">
|
||
${imgHtml}
|
||
<div class="prize-details">
|
||
<div class="prize-name">${nameHtml}</div>
|
||
<div class="prize-id-label">奖品 ID: <span>${res.rewardId}</span></div>
|
||
</div>
|
||
</div>
|
||
<div class="detail-section">
|
||
<h4>计算详情</h4>
|
||
${detailHtml}
|
||
</div>
|
||
`;
|
||
} else {
|
||
card.innerHTML = `
|
||
<div class="card-title">📊 第 ${res.index} 抽</div>
|
||
<div class="result-header error">
|
||
<span class="result-icon">❌</span>
|
||
<div>
|
||
<div class="result-title">验证失败</div>
|
||
<div class="result-subtitle">${res.error}</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
container.appendChild(card);
|
||
});
|
||
|
||
// 滚动到结果
|
||
container.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||
}
|
||
</script>
|
||
</body>
|
||
|
||
</html> |